@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,368 @@
|
|
|
1
|
+
import { zeroUsage, } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Google's `ThinkingLevel` is a *string* enum, so its members are just their
|
|
4
|
+
* own names on the wire. Spelling them out keeps `@google/genai` out of this
|
|
5
|
+
* module's runtime imports — an enum is a value, and importing one would load
|
|
6
|
+
* the whole SDK for anyone who merely imports the library. The cast is what an
|
|
7
|
+
* enum costs: a literal is not assignable to a nominal enum type, even when it
|
|
8
|
+
* is the same string.
|
|
9
|
+
*/
|
|
10
|
+
const THINKING_LEVELS = {
|
|
11
|
+
minimal: 'MINIMAL',
|
|
12
|
+
low: 'LOW',
|
|
13
|
+
medium: 'MEDIUM',
|
|
14
|
+
high: 'HIGH',
|
|
15
|
+
};
|
|
16
|
+
/** `FunctionCallingConfigMode`, spelled out for the same reason. */
|
|
17
|
+
const CALLING_MODES = {
|
|
18
|
+
any: 'ANY',
|
|
19
|
+
none: 'NONE',
|
|
20
|
+
auto: 'AUTO',
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* Ids this adapter invented. The Gemini API returns function calls without one
|
|
24
|
+
* — Vertex does populate `id` — but the runner pairs results to calls by id, so
|
|
25
|
+
* one has to exist. The prefix is how a synthesized id is recognised on the way
|
|
26
|
+
* back out and dropped, leaving the API to pair by function name as it expects.
|
|
27
|
+
*/
|
|
28
|
+
const SYNTHETIC_ID = 'gemini-call-';
|
|
29
|
+
/**
|
|
30
|
+
* The `Model` contract over `generateContent`.
|
|
31
|
+
*
|
|
32
|
+
* Google publishes an OpenAI-compatible endpoint, and reaching Gemini through
|
|
33
|
+
* it costs nothing but a base url. What it costs *afterwards* is everything
|
|
34
|
+
* this file exists for: thinking budgets and thought summaries, thought
|
|
35
|
+
* signatures (without which Gemini 3 function calling is rejected outright),
|
|
36
|
+
* cached-content accounting, and the block reasons that come back when a safety
|
|
37
|
+
* filter fires. None of those have an OpenAI field to be carried in.
|
|
38
|
+
*/
|
|
39
|
+
export class GeminiModel {
|
|
40
|
+
id;
|
|
41
|
+
#client;
|
|
42
|
+
#options;
|
|
43
|
+
#nextId = 0;
|
|
44
|
+
constructor(id, client, options = {}) {
|
|
45
|
+
this.id = id;
|
|
46
|
+
this.#client = client;
|
|
47
|
+
this.#options = options;
|
|
48
|
+
}
|
|
49
|
+
async generate(req) {
|
|
50
|
+
const res = await this.#client.models.generateContent(this.#params(req));
|
|
51
|
+
const read = emptyRead();
|
|
52
|
+
for (const part of candidateParts(res)) {
|
|
53
|
+
this.#readPart(part, read);
|
|
54
|
+
}
|
|
55
|
+
return this.#finish(read, res);
|
|
56
|
+
}
|
|
57
|
+
async stream(req, onDelta) {
|
|
58
|
+
const stream = await this.#client.models.generateContentStream(this.#params(req));
|
|
59
|
+
const read = emptyRead();
|
|
60
|
+
let last;
|
|
61
|
+
for await (const chunk of stream) {
|
|
62
|
+
last = chunk;
|
|
63
|
+
for (const part of candidateParts(chunk)) {
|
|
64
|
+
this.#readPart(part, read, onDelta);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return this.#finish(read, last);
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Folds one part into the accumulator, emitting deltas when streaming.
|
|
71
|
+
* Shared by both paths so a streamed run and a plain one cannot disagree
|
|
72
|
+
* about what a part meant.
|
|
73
|
+
*/
|
|
74
|
+
#readPart(part, into, onDelta) {
|
|
75
|
+
if (part.text !== undefined) {
|
|
76
|
+
// Thought summaries arrive as ordinary text parts flagged
|
|
77
|
+
// `thought`; the flag is the only thing separating reasoning from
|
|
78
|
+
// the answer.
|
|
79
|
+
if (part.thought) {
|
|
80
|
+
into.thinking += part.text;
|
|
81
|
+
onDelta?.({ type: 'thinking_delta', delta: part.text });
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
into.text += part.text;
|
|
85
|
+
onDelta?.({ type: 'text_delta', delta: part.text });
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const call = part.functionCall;
|
|
90
|
+
if (!call) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const id = this.#callId(call.id);
|
|
94
|
+
const name = call.name ?? '';
|
|
95
|
+
const args = JSON.stringify(call.args ?? {});
|
|
96
|
+
// Gemini 3 requires the signature back on the replayed call and fails
|
|
97
|
+
// the request without it, so it rides along on the call rather than in
|
|
98
|
+
// an adapter-side cache that a handoff or a restart would lose.
|
|
99
|
+
into.toolCalls.push({ id, name, args, signature: part.thoughtSignature });
|
|
100
|
+
// Arguments are not streamed incrementally by this API: a call arrives
|
|
101
|
+
// whole, so detection and the arguments are one event apart and
|
|
102
|
+
// `argsSoFar` is complete the moment it is first seen.
|
|
103
|
+
onDelta?.({ type: 'tool_call_detected', callId: id, name });
|
|
104
|
+
onDelta?.({ type: 'tool_args_delta', callId: id, name, delta: args, argsSoFar: args });
|
|
105
|
+
}
|
|
106
|
+
#finish(read, res) {
|
|
107
|
+
return {
|
|
108
|
+
text: read.text,
|
|
109
|
+
thinking: read.thinking || undefined,
|
|
110
|
+
toolCalls: read.toolCalls,
|
|
111
|
+
usage: toUsage(res?.usageMetadata),
|
|
112
|
+
stopReason: toStopReason(res?.candidates?.[0]?.finishReason, read.toolCalls.length > 0),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
#params(req) {
|
|
116
|
+
return {
|
|
117
|
+
model: this.id,
|
|
118
|
+
contents: toContents(req.messages),
|
|
119
|
+
config: this.#config(req),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
#config(req) {
|
|
123
|
+
const o = this.#options;
|
|
124
|
+
const thinking = o.thinkingBudget !== undefined || o.thinkingLevel !== undefined
|
|
125
|
+
? {
|
|
126
|
+
thinkingBudget: o.thinkingBudget,
|
|
127
|
+
thinkingLevel: o.thinkingLevel && THINKING_LEVELS[o.thinkingLevel],
|
|
128
|
+
includeThoughts: o.includeThoughts ?? true,
|
|
129
|
+
}
|
|
130
|
+
: { includeThoughts: o.includeThoughts ?? true };
|
|
131
|
+
return {
|
|
132
|
+
// Unlike the other two vendors this is a *config* field rather than
|
|
133
|
+
// a turn, so a system prompt never occupies a position in the
|
|
134
|
+
// conversation and cannot be reordered by history trimming.
|
|
135
|
+
systemInstruction: req.system,
|
|
136
|
+
maxOutputTokens: o.maxTokens,
|
|
137
|
+
thinkingConfig: thinking,
|
|
138
|
+
abortSignal: req.signal,
|
|
139
|
+
tools: req.tools.length
|
|
140
|
+
? [
|
|
141
|
+
{
|
|
142
|
+
functionDeclarations: req.tools.map((t) => ({
|
|
143
|
+
name: t.name,
|
|
144
|
+
description: t.description,
|
|
145
|
+
// The `parameters` field takes Google's own
|
|
146
|
+
// trimmed-down Schema; this one takes the JSON
|
|
147
|
+
// Schema the rest of the runtime already speaks.
|
|
148
|
+
parametersJsonSchema: t.parameters,
|
|
149
|
+
})),
|
|
150
|
+
},
|
|
151
|
+
]
|
|
152
|
+
: undefined,
|
|
153
|
+
toolConfig: req.tools.length
|
|
154
|
+
? { functionCallingConfig: { mode: toCallingMode(req.toolChoice) } }
|
|
155
|
+
: undefined,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/** Mints an id for a call the API left unidentified. */
|
|
159
|
+
#callId(id) {
|
|
160
|
+
return id ?? `${SYNTHETIC_ID}${this.#nextId++}`;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function emptyRead() {
|
|
164
|
+
return { text: '', thinking: '', toolCalls: [] };
|
|
165
|
+
}
|
|
166
|
+
function candidateParts(res) {
|
|
167
|
+
return res.candidates?.[0]?.content?.parts ?? [];
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Internal messages → Gemini contents.
|
|
171
|
+
*
|
|
172
|
+
* The role vocabulary is `user` and `model`, with tool results carried as
|
|
173
|
+
* `functionResponse` parts inside a *user* turn — so, as with Anthropic,
|
|
174
|
+
* adjacent turns of the same role are merged rather than emitted one per
|
|
175
|
+
* message. The user side is buffered rather than merged in place, because
|
|
176
|
+
* Gemini constrains the shape of the turn answering a set of tool calls in two
|
|
177
|
+
* ways that the message order does not respect on its own: see `flushUser`.
|
|
178
|
+
*/
|
|
179
|
+
function toContents(messages) {
|
|
180
|
+
const out = [];
|
|
181
|
+
let results = [];
|
|
182
|
+
let plain = [];
|
|
183
|
+
/**
|
|
184
|
+
* Emits the pending user turn, tool results first and on their own.
|
|
185
|
+
*
|
|
186
|
+
* Every `functionResponse` answering one model turn has to arrive in a
|
|
187
|
+
* single `Content` holding nothing else. Both halves of that are enforced
|
|
188
|
+
* by the API and neither is hypothetical here: a `load_skills` node lands
|
|
189
|
+
* between the `skill_load` result and the results of the calls issued
|
|
190
|
+
* alongside it, which naively yields `[response][text][response]` and
|
|
191
|
+
* breaks the rule twice over. The API's two complaints are worth writing
|
|
192
|
+
* down, since neither describes what is actually wrong:
|
|
193
|
+
*
|
|
194
|
+
* - splitting the responses gets `Please ensure that the number of function
|
|
195
|
+
* response parts is equal to the number of function call parts`
|
|
196
|
+
* - mixing text in with them gets `Requests ending with a model turn are
|
|
197
|
+
* not supported`
|
|
198
|
+
*
|
|
199
|
+
* Deferring the text to a turn of its own satisfies both. Nothing is lost:
|
|
200
|
+
* the parts still sit between the same two model turns, in the same order
|
|
201
|
+
* relative to everything the model reads as prose.
|
|
202
|
+
*/
|
|
203
|
+
const flushUser = () => {
|
|
204
|
+
if (results.length) {
|
|
205
|
+
out.push({ role: 'user', parts: results });
|
|
206
|
+
results = [];
|
|
207
|
+
}
|
|
208
|
+
if (plain.length) {
|
|
209
|
+
out.push({ role: 'user', parts: plain });
|
|
210
|
+
plain = [];
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
const pushModel = (parts) => {
|
|
214
|
+
if (!parts.length) {
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
flushUser();
|
|
218
|
+
const last = out.at(-1);
|
|
219
|
+
if (last?.role === 'model') {
|
|
220
|
+
last.parts?.push(...parts);
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
out.push({ role: 'model', parts });
|
|
224
|
+
};
|
|
225
|
+
for (const m of messages) {
|
|
226
|
+
switch (m.role) {
|
|
227
|
+
case 'system':
|
|
228
|
+
// A mid-conversation system message is a memory recall, and
|
|
229
|
+
// hoisting it into `systemInstruction` would move it away from
|
|
230
|
+
// the point it was injected at. A user turn keeps the position.
|
|
231
|
+
plain.push({ text: m.content });
|
|
232
|
+
break;
|
|
233
|
+
case 'user':
|
|
234
|
+
plain.push(...m.content.map(toPart));
|
|
235
|
+
break;
|
|
236
|
+
case 'assistant': {
|
|
237
|
+
const parts = [];
|
|
238
|
+
if (m.content) {
|
|
239
|
+
parts.push({ text: m.content });
|
|
240
|
+
}
|
|
241
|
+
for (const c of m.toolCalls ?? []) {
|
|
242
|
+
parts.push({
|
|
243
|
+
functionCall: {
|
|
244
|
+
id: wireId(c.id),
|
|
245
|
+
name: c.name,
|
|
246
|
+
args: parseArgs(c.args),
|
|
247
|
+
},
|
|
248
|
+
// Gemini 3 rejects a replayed call whose signature is
|
|
249
|
+
// missing. One produced by a different Gemini model is
|
|
250
|
+
// still accepted — the backend handles compatibility —
|
|
251
|
+
// so a handoff between two of them replays cleanly.
|
|
252
|
+
thoughtSignature: c.signature,
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
pushModel(parts);
|
|
256
|
+
break;
|
|
257
|
+
}
|
|
258
|
+
case 'tool':
|
|
259
|
+
results.push({
|
|
260
|
+
functionResponse: {
|
|
261
|
+
id: wireId(m.callId),
|
|
262
|
+
name: m.name,
|
|
263
|
+
// The API wants an object, not a string. `output`
|
|
264
|
+
// and `error` are the conventional keys, and the
|
|
265
|
+
// distinction is otherwise lost.
|
|
266
|
+
response: m.isError ? { error: m.content } : { output: m.content },
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
break;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
flushUser();
|
|
273
|
+
return out;
|
|
274
|
+
}
|
|
275
|
+
/** Drops ids this adapter invented, so the API pairs by function name instead. */
|
|
276
|
+
function wireId(id) {
|
|
277
|
+
return id.startsWith(SYNTHETIC_ID) ? undefined : id;
|
|
278
|
+
}
|
|
279
|
+
/** Only text and inline media have first-class parts; the rest degrade to text. */
|
|
280
|
+
function toPart(part) {
|
|
281
|
+
if (part.type === 'text') {
|
|
282
|
+
return { text: part.text };
|
|
283
|
+
}
|
|
284
|
+
const inline = readDataUri(part.url);
|
|
285
|
+
if (inline) {
|
|
286
|
+
return { inlineData: inline };
|
|
287
|
+
}
|
|
288
|
+
// A remote uri has to be one the service can fetch — a Files API or GCS
|
|
289
|
+
// reference — and needs its mime type declared, which a bare url does not
|
|
290
|
+
// carry. Without one there is nothing to send but the url itself.
|
|
291
|
+
return part.mimeType
|
|
292
|
+
? { fileData: { fileUri: part.url, mimeType: part.mimeType } }
|
|
293
|
+
: { text: `[${part.type}] ${part.url}` };
|
|
294
|
+
}
|
|
295
|
+
const DATA_URI = /^data:([^;,]+);base64,(.*)$/s;
|
|
296
|
+
function readDataUri(url) {
|
|
297
|
+
const match = DATA_URI.exec(url);
|
|
298
|
+
return match ? { mimeType: match[1], data: match[2] } : undefined;
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Tool arguments travel as the raw JSON string the model produced, because that
|
|
302
|
+
* is what the trajectory stores and what other providers want back verbatim.
|
|
303
|
+
* Gemini wants the parsed object, and a malformed one is the model's error to
|
|
304
|
+
* see rather than a crash here.
|
|
305
|
+
*/
|
|
306
|
+
function parseArgs(args) {
|
|
307
|
+
if (!args.trim()) {
|
|
308
|
+
return {};
|
|
309
|
+
}
|
|
310
|
+
try {
|
|
311
|
+
const parsed = JSON.parse(args);
|
|
312
|
+
return parsed && typeof parsed === 'object' ? parsed : {};
|
|
313
|
+
}
|
|
314
|
+
catch {
|
|
315
|
+
return { _raw: args };
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function toCallingMode(choice) {
|
|
319
|
+
switch (choice) {
|
|
320
|
+
case 'required':
|
|
321
|
+
return CALLING_MODES.any;
|
|
322
|
+
case 'none':
|
|
323
|
+
return CALLING_MODES.none;
|
|
324
|
+
default:
|
|
325
|
+
return CALLING_MODES.auto;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
/**
|
|
329
|
+
* `promptTokenCount` already includes whatever the cache served, matching this
|
|
330
|
+
* runtime's definition of `cachedInputTokens` as a subset. Thinking is the
|
|
331
|
+
* opposite: `thoughtsTokenCount` is reported *beside* `candidatesTokenCount`
|
|
332
|
+
* rather than inside it, so it is added in to keep `reasoningTokens` a subset
|
|
333
|
+
* of `outputTokens`.
|
|
334
|
+
*/
|
|
335
|
+
function toUsage(u) {
|
|
336
|
+
if (!u) {
|
|
337
|
+
return undefined;
|
|
338
|
+
}
|
|
339
|
+
const thoughts = u.thoughtsTokenCount ?? 0;
|
|
340
|
+
return {
|
|
341
|
+
...zeroUsage(),
|
|
342
|
+
inputTokens: u.promptTokenCount ?? 0,
|
|
343
|
+
cachedInputTokens: u.cachedContentTokenCount ?? 0,
|
|
344
|
+
outputTokens: (u.candidatesTokenCount ?? 0) + thoughts,
|
|
345
|
+
reasoningTokens: thoughts,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Gemini has no finish reason for "stopped to call a tool" — a turn ending in
|
|
350
|
+
* function calls still reports `STOP`. The presence of calls is therefore the
|
|
351
|
+
* only signal, and it has to be applied here rather than left to the kernel's
|
|
352
|
+
* fallback, which only fires when a stop reason is missing entirely.
|
|
353
|
+
*/
|
|
354
|
+
function toStopReason(reason, toolCalls) {
|
|
355
|
+
switch (reason) {
|
|
356
|
+
case 'MAX_TOKENS':
|
|
357
|
+
return 'length';
|
|
358
|
+
case 'SAFETY':
|
|
359
|
+
case 'RECITATION':
|
|
360
|
+
case 'BLOCKLIST':
|
|
361
|
+
case 'PROHIBITED_CONTENT':
|
|
362
|
+
case 'SPII':
|
|
363
|
+
return 'content_filter';
|
|
364
|
+
default:
|
|
365
|
+
return toolCalls ? 'tool_calls' : 'stop';
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
//# sourceMappingURL=gemini.js.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type OpenAI from 'openai';
|
|
2
|
+
import type { StreamDelta } from '../events.ts';
|
|
3
|
+
import type { Model, ModelRequest, ModelResponse } from '../model.ts';
|
|
4
|
+
/**
|
|
5
|
+
* Provider-specific knobs. Reasoning effort is deliberately *not* part of
|
|
6
|
+
* `ModelRequest`: every provider spells it differently (and most models ignore
|
|
7
|
+
* it), so it belongs to the concrete adapter rather than the `Model` contract.
|
|
8
|
+
*/
|
|
9
|
+
export interface OpenAIModelOptions {
|
|
10
|
+
reasoningEffort?: OpenAI.ReasoningEffort;
|
|
11
|
+
}
|
|
12
|
+
export declare class OpenAIModel implements Model {
|
|
13
|
+
#private;
|
|
14
|
+
readonly id: string;
|
|
15
|
+
constructor(id: string, client: OpenAI, options?: OpenAIModelOptions);
|
|
16
|
+
generate(req: ModelRequest): Promise<ModelResponse>;
|
|
17
|
+
stream(req: ModelRequest, onDelta: (d: StreamDelta) => void): Promise<ModelResponse>;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=openai-chat.d.ts.map
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
var _a;
|
|
2
|
+
import { zeroUsage } from "../types.js";
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// OpenAI chat-completions adapter
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
function toStopReason(finish) {
|
|
7
|
+
switch (finish) {
|
|
8
|
+
case 'tool_calls':
|
|
9
|
+
case 'function_call':
|
|
10
|
+
return 'tool_calls';
|
|
11
|
+
case 'length':
|
|
12
|
+
return 'length';
|
|
13
|
+
case 'content_filter':
|
|
14
|
+
return 'content_filter';
|
|
15
|
+
default:
|
|
16
|
+
return 'stop';
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
export class OpenAIModel {
|
|
20
|
+
id;
|
|
21
|
+
#client;
|
|
22
|
+
#reasoningEffort;
|
|
23
|
+
constructor(id, client, options = {}) {
|
|
24
|
+
this.id = id;
|
|
25
|
+
this.#client = client;
|
|
26
|
+
this.#reasoningEffort = options.reasoningEffort;
|
|
27
|
+
}
|
|
28
|
+
async generate(req) {
|
|
29
|
+
const res = await this.#client.chat.completions.create(this.#params(req), {
|
|
30
|
+
signal: req.signal,
|
|
31
|
+
});
|
|
32
|
+
const choice = res.choices[0];
|
|
33
|
+
const message = choice?.message;
|
|
34
|
+
return {
|
|
35
|
+
text: message?.content ?? '',
|
|
36
|
+
thinking: readReasoning(message),
|
|
37
|
+
toolCalls: (message?.tool_calls ?? []).flatMap((c) => c.type === 'function'
|
|
38
|
+
? [{ id: c.id, name: c.function.name, args: c.function.arguments }]
|
|
39
|
+
: []),
|
|
40
|
+
usage: toUsage(res.usage),
|
|
41
|
+
stopReason: toStopReason(choice?.finish_reason),
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
async stream(req, onDelta) {
|
|
45
|
+
const stream = await this.#client.chat.completions.create({ ...this.#params(req), stream: true, stream_options: { include_usage: true } }, { signal: req.signal });
|
|
46
|
+
let text = '';
|
|
47
|
+
let thinking = '';
|
|
48
|
+
let usage;
|
|
49
|
+
let stopReason;
|
|
50
|
+
// Tool calls arrive as deltas keyed by index, with the id and name in the
|
|
51
|
+
// first fragment and arguments trickling in afterwards.
|
|
52
|
+
const calls = new Map();
|
|
53
|
+
for await (const chunk of stream) {
|
|
54
|
+
if (chunk.usage) {
|
|
55
|
+
usage = toUsage(chunk.usage);
|
|
56
|
+
}
|
|
57
|
+
const choice = chunk.choices[0];
|
|
58
|
+
if (!choice) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (choice.finish_reason) {
|
|
62
|
+
stopReason = toStopReason(choice.finish_reason);
|
|
63
|
+
}
|
|
64
|
+
const delta = choice.delta;
|
|
65
|
+
const reasoning = readReasoning(delta);
|
|
66
|
+
if (reasoning) {
|
|
67
|
+
thinking += reasoning;
|
|
68
|
+
onDelta({ type: 'thinking_delta', delta: reasoning });
|
|
69
|
+
}
|
|
70
|
+
if (delta?.content) {
|
|
71
|
+
text += delta.content;
|
|
72
|
+
onDelta({ type: 'text_delta', delta: delta.content });
|
|
73
|
+
}
|
|
74
|
+
for (const part of delta?.tool_calls ?? []) {
|
|
75
|
+
const known = calls.get(part.index);
|
|
76
|
+
const call = known ?? {
|
|
77
|
+
id: part.id ?? `call_${part.index}`,
|
|
78
|
+
name: part.function?.name ?? '',
|
|
79
|
+
args: '',
|
|
80
|
+
};
|
|
81
|
+
if (!known) {
|
|
82
|
+
calls.set(part.index, call);
|
|
83
|
+
}
|
|
84
|
+
if (part.id) {
|
|
85
|
+
call.id = part.id;
|
|
86
|
+
}
|
|
87
|
+
if (part.function?.name) {
|
|
88
|
+
call.name = part.function.name;
|
|
89
|
+
}
|
|
90
|
+
if (!known || (part.function?.name && !known.name)) {
|
|
91
|
+
onDelta({ type: 'tool_call_detected', callId: call.id, name: call.name });
|
|
92
|
+
}
|
|
93
|
+
const args = part.function?.arguments;
|
|
94
|
+
if (args) {
|
|
95
|
+
call.args += args;
|
|
96
|
+
onDelta({
|
|
97
|
+
type: 'tool_args_delta',
|
|
98
|
+
callId: call.id,
|
|
99
|
+
name: call.name,
|
|
100
|
+
delta: args,
|
|
101
|
+
argsSoFar: call.args,
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
text,
|
|
108
|
+
thinking: thinking || undefined,
|
|
109
|
+
toolCalls: [...calls.entries()].sort((a, b) => a[0] - b[0]).map(([, c]) => c),
|
|
110
|
+
usage,
|
|
111
|
+
stopReason: stopReason ?? (calls.size ? 'tool_calls' : 'stop'),
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
#params(req) {
|
|
115
|
+
const messages = [];
|
|
116
|
+
if (req.system) {
|
|
117
|
+
messages.push({ role: 'system', content: req.system });
|
|
118
|
+
}
|
|
119
|
+
for (const m of req.messages) {
|
|
120
|
+
messages.push(_a.#toOpenAI(m));
|
|
121
|
+
}
|
|
122
|
+
return {
|
|
123
|
+
model: this.id,
|
|
124
|
+
messages,
|
|
125
|
+
reasoning_effort: this.#reasoningEffort,
|
|
126
|
+
tools: req.tools.length
|
|
127
|
+
? req.tools.map((t) => ({
|
|
128
|
+
type: 'function',
|
|
129
|
+
function: {
|
|
130
|
+
name: t.name,
|
|
131
|
+
description: t.description,
|
|
132
|
+
parameters: t.parameters,
|
|
133
|
+
},
|
|
134
|
+
}))
|
|
135
|
+
: undefined,
|
|
136
|
+
tool_choice: req.tools.length ? (req.toolChoice ?? 'auto') : undefined,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Translates one internal message into the provider wire format. Static +
|
|
141
|
+
* private because it is pure and must not be part of the public surface.
|
|
142
|
+
*/
|
|
143
|
+
static #toOpenAI(m) {
|
|
144
|
+
switch (m.role) {
|
|
145
|
+
case 'system':
|
|
146
|
+
return { role: 'system', content: m.content };
|
|
147
|
+
case 'user':
|
|
148
|
+
return {
|
|
149
|
+
role: 'user',
|
|
150
|
+
// Only images have a first-class multimodal representation in
|
|
151
|
+
// chat completions; other media degrade to a text reference
|
|
152
|
+
// so the thread stays valid instead of throwing.
|
|
153
|
+
content: m.content.map((p) => p.type === 'text'
|
|
154
|
+
? { type: 'text', text: p.text }
|
|
155
|
+
: p.type === 'image'
|
|
156
|
+
? { type: 'image_url', image_url: { url: p.url } }
|
|
157
|
+
: { type: 'text', text: `[${p.type}] ${p.url}` }),
|
|
158
|
+
};
|
|
159
|
+
case 'assistant':
|
|
160
|
+
return {
|
|
161
|
+
role: 'assistant',
|
|
162
|
+
// A tool-calling turn usually has no prose; the API wants an
|
|
163
|
+
// explicit null there rather than an empty string.
|
|
164
|
+
content: m.content || null,
|
|
165
|
+
tool_calls: m.toolCalls?.length
|
|
166
|
+
? m.toolCalls.map((c) => ({
|
|
167
|
+
id: c.id,
|
|
168
|
+
type: 'function',
|
|
169
|
+
function: { name: c.name, arguments: c.args },
|
|
170
|
+
}))
|
|
171
|
+
: undefined,
|
|
172
|
+
};
|
|
173
|
+
case 'tool':
|
|
174
|
+
return { role: 'tool', tool_call_id: m.callId, content: m.content };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
_a = OpenAIModel;
|
|
179
|
+
function toUsage(u) {
|
|
180
|
+
if (!u) {
|
|
181
|
+
return undefined;
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
...zeroUsage(),
|
|
185
|
+
inputTokens: u.prompt_tokens,
|
|
186
|
+
cachedInputTokens: u.prompt_tokens_details?.cached_tokens ?? 0,
|
|
187
|
+
outputTokens: u.completion_tokens,
|
|
188
|
+
reasoningTokens: u.completion_tokens_details?.reasoning_tokens ?? 0,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Reasoning text is not part of the chat-completions type surface, but several
|
|
193
|
+
* providers (and OpenAI-compatible gateways) put it on the message or delta.
|
|
194
|
+
*/
|
|
195
|
+
function readReasoning(v) {
|
|
196
|
+
if (v === null || typeof v !== 'object') {
|
|
197
|
+
return undefined;
|
|
198
|
+
}
|
|
199
|
+
const r = v;
|
|
200
|
+
const value = typeof r.reasoning === 'string' ? r.reasoning : r.reasoning_content;
|
|
201
|
+
return typeof value === 'string' && value ? value : undefined;
|
|
202
|
+
}
|
|
203
|
+
//# sourceMappingURL=openai-chat.js.map
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type OpenAI from 'openai';
|
|
2
|
+
import type { StreamDelta } from '../events.ts';
|
|
3
|
+
import type { Model, ModelRequest, ModelResponse } from '../model.ts';
|
|
4
|
+
/**
|
|
5
|
+
* Provider-specific knobs, mirroring `OpenAIModelOptions` but expressed the way
|
|
6
|
+
* the responses API spells them.
|
|
7
|
+
*/
|
|
8
|
+
export interface OpenAIResponsesModelOptions {
|
|
9
|
+
reasoningEffort?: OpenAI.ReasoningEffort;
|
|
10
|
+
/** ask the model for a reasoning summary — the only reasoning text this API exposes */
|
|
11
|
+
reasoningSummary?: 'auto' | 'concise' | 'detailed';
|
|
12
|
+
/**
|
|
13
|
+
* Server-side conversation state. Off by default: the runner already owns
|
|
14
|
+
* the trajectory and replays the full thread on every call, so storing it
|
|
15
|
+
* again would duplicate state rather than help.
|
|
16
|
+
*/
|
|
17
|
+
store?: boolean;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Same `Model` contract as `OpenAIModel`, over `/v1/responses` instead of
|
|
21
|
+
* `/v1/chat/completions`. Reasoning models are the reason to prefer it: effort
|
|
22
|
+
* and summaries are first-class there.
|
|
23
|
+
*/
|
|
24
|
+
export declare class OpenAIResponsesModel implements Model {
|
|
25
|
+
#private;
|
|
26
|
+
readonly id: string;
|
|
27
|
+
constructor(id: string, client: OpenAI, options?: OpenAIResponsesModelOptions);
|
|
28
|
+
generate(req: ModelRequest): Promise<ModelResponse>;
|
|
29
|
+
stream(req: ModelRequest, onDelta: (d: StreamDelta) => void): Promise<ModelResponse>;
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=openai-responses.d.ts.map
|