@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.
Files changed (109) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +178 -0
  3. package/dist/agent.d.ts +66 -0
  4. package/dist/agent.js +97 -0
  5. package/dist/architecture.d.ts +74 -0
  6. package/dist/architecture.js +105 -0
  7. package/dist/embedding.d.ts +60 -0
  8. package/dist/embedding.js +21 -0
  9. package/dist/embeddings/gemini.d.ts +24 -0
  10. package/dist/embeddings/gemini.js +71 -0
  11. package/dist/embeddings/index.d.ts +4 -0
  12. package/dist/embeddings/index.js +4 -0
  13. package/dist/embeddings/openai.d.ts +18 -0
  14. package/dist/embeddings/openai.js +27 -0
  15. package/dist/embeddings/openrouter.d.ts +21 -0
  16. package/dist/embeddings/openrouter.js +54 -0
  17. package/dist/events.d.ts +144 -0
  18. package/dist/events.js +110 -0
  19. package/dist/ids.d.ts +14 -0
  20. package/dist/ids.js +33 -0
  21. package/dist/index.d.ts +25 -0
  22. package/dist/index.js +25 -0
  23. package/dist/inspect/client.d.ts +2 -0
  24. package/dist/inspect/client.js +1326 -0
  25. package/dist/inspect/css.d.ts +2 -0
  26. package/dist/inspect/css.js +164 -0
  27. package/dist/inspect/index.d.ts +9 -0
  28. package/dist/inspect/index.js +12 -0
  29. package/dist/inspect/page.d.ts +3 -0
  30. package/dist/inspect/page.js +93 -0
  31. package/dist/inspect/report.d.ts +54 -0
  32. package/dist/inspect/report.js +62 -0
  33. package/dist/kernel.d.ts +178 -0
  34. package/dist/kernel.js +929 -0
  35. package/dist/memory-stores/factory.d.ts +15 -0
  36. package/dist/memory-stores/factory.js +29 -0
  37. package/dist/memory-stores/file.d.ts +37 -0
  38. package/dist/memory-stores/file.js +174 -0
  39. package/dist/memory-stores/in-memory.d.ts +17 -0
  40. package/dist/memory-stores/in-memory.js +80 -0
  41. package/dist/memory-stores/index.d.ts +4 -0
  42. package/dist/memory-stores/index.js +5 -0
  43. package/dist/memory.d.ts +115 -0
  44. package/dist/memory.js +213 -0
  45. package/dist/model.d.ts +25 -0
  46. package/dist/model.js +2 -0
  47. package/dist/models/anthropic.d.ts +36 -0
  48. package/dist/models/anthropic.js +293 -0
  49. package/dist/models/factory.d.ts +202 -0
  50. package/dist/models/factory.js +533 -0
  51. package/dist/models/gemini.d.ts +45 -0
  52. package/dist/models/gemini.js +368 -0
  53. package/dist/models/index.d.ts +7 -0
  54. package/dist/models/index.js +7 -0
  55. package/dist/models/openai-chat.d.ts +19 -0
  56. package/dist/models/openai-chat.js +203 -0
  57. package/dist/models/openai-responses.d.ts +31 -0
  58. package/dist/models/openai-responses.js +258 -0
  59. package/dist/models/openrouter.d.ts +44 -0
  60. package/dist/models/openrouter.js +230 -0
  61. package/dist/payload-stores/factory.d.ts +15 -0
  62. package/dist/payload-stores/factory.js +30 -0
  63. package/dist/payload-stores/file.d.ts +29 -0
  64. package/dist/payload-stores/file.js +112 -0
  65. package/dist/payload-stores/in-memory.d.ts +17 -0
  66. package/dist/payload-stores/in-memory.js +40 -0
  67. package/dist/payload-stores/index.d.ts +4 -0
  68. package/dist/payload-stores/index.js +5 -0
  69. package/dist/payload.d.ts +89 -0
  70. package/dist/payload.js +181 -0
  71. package/dist/project/config.d.ts +302 -0
  72. package/dist/project/config.js +290 -0
  73. package/dist/project/index.d.ts +4 -0
  74. package/dist/project/index.js +4 -0
  75. package/dist/project/load.d.ts +142 -0
  76. package/dist/project/load.js +430 -0
  77. package/dist/project/refs.d.ts +23 -0
  78. package/dist/project/refs.js +70 -0
  79. package/dist/prompt.d.ts +66 -0
  80. package/dist/prompt.js +62 -0
  81. package/dist/runner.d.ts +103 -0
  82. package/dist/runner.js +537 -0
  83. package/dist/services.d.ts +23 -0
  84. package/dist/services.js +44 -0
  85. package/dist/skill-providers/factory.d.ts +16 -0
  86. package/dist/skill-providers/factory.js +26 -0
  87. package/dist/skill-providers/file.d.ts +44 -0
  88. package/dist/skill-providers/file.js +209 -0
  89. package/dist/skill-providers/index.d.ts +4 -0
  90. package/dist/skill-providers/index.js +5 -0
  91. package/dist/skill-providers/static.d.ts +13 -0
  92. package/dist/skill-providers/static.js +47 -0
  93. package/dist/skills.d.ts +102 -0
  94. package/dist/skills.js +232 -0
  95. package/dist/state.d.ts +86 -0
  96. package/dist/state.js +49 -0
  97. package/dist/tools/exa.d.ts +23 -0
  98. package/dist/tools/exa.js +389 -0
  99. package/dist/tools/index.d.ts +4 -0
  100. package/dist/tools/index.js +6 -0
  101. package/dist/tools/sandbox.d.ts +172 -0
  102. package/dist/tools/sandbox.js +602 -0
  103. package/dist/tools/workspace.d.ts +53 -0
  104. package/dist/tools/workspace.js +1034 -0
  105. package/dist/trajectory.d.ts +213 -0
  106. package/dist/trajectory.js +232 -0
  107. package/dist/types.d.ts +195 -0
  108. package/dist/types.js +134 -0
  109. 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,7 @@
1
+ export * from './anthropic.ts';
2
+ export * from './factory.ts';
3
+ export * from './gemini.ts';
4
+ export * from './openai-chat.ts';
5
+ export * from './openai-responses.ts';
6
+ export * from './openrouter.ts';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,7 @@
1
+ export * from "./anthropic.js";
2
+ export * from "./factory.js";
3
+ export * from "./gemini.js";
4
+ export * from "./openai-chat.js";
5
+ export * from "./openai-responses.js";
6
+ export * from "./openrouter.js";
7
+ //# sourceMappingURL=index.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