@jxburros/ai-nugget 0.3.1 → 0.6.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 (183) hide show
  1. package/README.md +134 -271
  2. package/UPGRADING.md +158 -0
  3. package/dist/adapters/engines/anthropic.d.ts +7 -1
  4. package/dist/adapters/engines/anthropic.js +64 -37
  5. package/dist/adapters/engines/base.d.ts +29 -2
  6. package/dist/adapters/engines/base.js +52 -8
  7. package/dist/adapters/engines/google.d.ts +17 -1
  8. package/dist/adapters/engines/google.js +93 -29
  9. package/dist/adapters/engines/ollama.d.ts +2 -1
  10. package/dist/adapters/engines/ollama.js +51 -15
  11. package/dist/adapters/engines/openaiChat.d.ts +2 -1
  12. package/dist/adapters/engines/openaiChat.js +60 -30
  13. package/dist/adapters/profiles.d.ts +21 -1
  14. package/dist/adapters/profiles.js +12 -2
  15. package/dist/agent/loop.d.ts +43 -1
  16. package/dist/agent/loop.js +120 -13
  17. package/dist/cjs/adapters/engines/anthropic.js +308 -0
  18. package/dist/cjs/adapters/engines/base.js +147 -0
  19. package/dist/cjs/adapters/engines/google.js +284 -0
  20. package/dist/cjs/adapters/engines/ollama.js +199 -0
  21. package/dist/cjs/adapters/engines/openaiChat.js +261 -0
  22. package/dist/cjs/adapters/index.js +22 -0
  23. package/dist/cjs/adapters/profiles.js +125 -0
  24. package/dist/cjs/agent/index.js +18 -0
  25. package/dist/cjs/agent/loop.js +372 -0
  26. package/dist/cjs/agent/tools.js +51 -0
  27. package/dist/cjs/connect.js +27 -0
  28. package/dist/cjs/errors.js +110 -0
  29. package/dist/cjs/handler.js +561 -0
  30. package/dist/cjs/index.js +27 -0
  31. package/dist/cjs/json.js +121 -0
  32. package/dist/cjs/keys.js +81 -0
  33. package/dist/cjs/package.json +3 -0
  34. package/dist/cjs/policy.js +42 -0
  35. package/dist/cjs/redact.js +78 -0
  36. package/dist/cjs/tokens.js +22 -0
  37. package/dist/cjs/transport.js +172 -0
  38. package/dist/cjs/types.js +6 -0
  39. package/dist/cjs/util.js +112 -0
  40. package/dist/connect.d.ts +31 -0
  41. package/dist/connect.js +25 -0
  42. package/dist/errors.js +25 -1
  43. package/dist/handler.d.ts +61 -1
  44. package/dist/handler.js +247 -61
  45. package/dist/index.d.ts +1 -0
  46. package/dist/index.js +1 -0
  47. package/dist/keys.js +1 -4
  48. package/dist/redact.d.ts +18 -0
  49. package/dist/redact.js +29 -1
  50. package/dist/transport.d.ts +3 -1
  51. package/dist/transport.js +21 -1
  52. package/dist/types.d.ts +96 -2
  53. package/dist/util.d.ts +16 -0
  54. package/dist/util.js +50 -0
  55. package/nugget/VERSION.txt +2 -2
  56. package/nugget/dist/adapters/engines/anthropic.d.ts +14 -0
  57. package/nugget/dist/adapters/engines/anthropic.d.ts.map +1 -0
  58. package/nugget/dist/adapters/engines/anthropic.js +305 -0
  59. package/nugget/dist/adapters/engines/anthropic.js.map +1 -0
  60. package/nugget/dist/adapters/engines/base.d.ts +50 -0
  61. package/nugget/dist/adapters/engines/base.d.ts.map +1 -0
  62. package/nugget/dist/adapters/engines/base.js +137 -0
  63. package/nugget/dist/adapters/engines/base.js.map +1 -0
  64. package/nugget/dist/adapters/engines/google.d.ts +24 -0
  65. package/nugget/dist/adapters/engines/google.d.ts.map +1 -0
  66. package/nugget/dist/adapters/engines/google.js +281 -0
  67. package/nugget/dist/adapters/engines/google.js.map +1 -0
  68. package/nugget/dist/adapters/engines/ollama.d.ts +11 -0
  69. package/nugget/dist/adapters/engines/ollama.d.ts.map +1 -0
  70. package/nugget/dist/adapters/engines/ollama.js +196 -0
  71. package/nugget/dist/adapters/engines/ollama.js.map +1 -0
  72. package/nugget/dist/adapters/engines/openaiChat.d.ts +16 -0
  73. package/nugget/dist/adapters/engines/openaiChat.d.ts.map +1 -0
  74. package/nugget/dist/adapters/engines/openaiChat.js +258 -0
  75. package/nugget/dist/adapters/engines/openaiChat.js.map +1 -0
  76. package/nugget/dist/adapters/index.d.ts +5 -0
  77. package/nugget/dist/adapters/index.d.ts.map +1 -0
  78. package/nugget/dist/adapters/index.js +17 -0
  79. package/nugget/dist/adapters/index.js.map +1 -0
  80. package/nugget/dist/adapters/profiles.d.ts +62 -0
  81. package/nugget/dist/adapters/profiles.d.ts.map +1 -0
  82. package/nugget/dist/adapters/profiles.js +121 -0
  83. package/nugget/dist/adapters/profiles.js.map +1 -0
  84. package/nugget/dist/agent/index.d.ts +3 -0
  85. package/nugget/dist/agent/index.d.ts.map +1 -0
  86. package/nugget/dist/agent/index.js +3 -0
  87. package/nugget/dist/agent/index.js.map +1 -0
  88. package/nugget/dist/agent/loop.d.ts +112 -0
  89. package/nugget/dist/agent/loop.d.ts.map +1 -0
  90. package/nugget/dist/agent/loop.js +370 -0
  91. package/nugget/dist/agent/loop.js.map +1 -0
  92. package/nugget/dist/agent/tools.d.ts +29 -0
  93. package/nugget/dist/agent/tools.d.ts.map +1 -0
  94. package/nugget/dist/agent/tools.js +48 -0
  95. package/nugget/dist/agent/tools.js.map +1 -0
  96. package/nugget/dist/cjs/adapters/engines/anthropic.js +308 -0
  97. package/nugget/dist/cjs/adapters/engines/base.js +147 -0
  98. package/nugget/dist/cjs/adapters/engines/google.js +284 -0
  99. package/nugget/dist/cjs/adapters/engines/ollama.js +199 -0
  100. package/nugget/dist/cjs/adapters/engines/openaiChat.js +261 -0
  101. package/nugget/dist/cjs/adapters/index.js +22 -0
  102. package/nugget/dist/cjs/adapters/profiles.js +125 -0
  103. package/nugget/dist/cjs/agent/index.js +18 -0
  104. package/nugget/dist/cjs/agent/loop.js +372 -0
  105. package/nugget/dist/cjs/agent/tools.js +51 -0
  106. package/nugget/dist/cjs/connect.js +27 -0
  107. package/nugget/dist/cjs/errors.js +110 -0
  108. package/nugget/dist/cjs/handler.js +561 -0
  109. package/nugget/dist/cjs/index.js +27 -0
  110. package/nugget/dist/cjs/json.js +121 -0
  111. package/nugget/dist/cjs/keys.js +81 -0
  112. package/nugget/dist/cjs/package.json +3 -0
  113. package/nugget/dist/cjs/policy.js +42 -0
  114. package/nugget/dist/cjs/redact.js +78 -0
  115. package/nugget/dist/cjs/tokens.js +22 -0
  116. package/nugget/dist/cjs/transport.js +172 -0
  117. package/nugget/dist/cjs/types.js +6 -0
  118. package/nugget/dist/cjs/util.js +112 -0
  119. package/nugget/dist/connect.d.ts +31 -0
  120. package/nugget/dist/connect.d.ts.map +1 -0
  121. package/nugget/dist/connect.js +25 -0
  122. package/nugget/dist/connect.js.map +1 -0
  123. package/nugget/dist/errors.d.ts +22 -0
  124. package/nugget/dist/errors.d.ts.map +1 -0
  125. package/nugget/dist/errors.js +104 -0
  126. package/nugget/dist/errors.js.map +1 -0
  127. package/nugget/dist/handler.d.ts +108 -0
  128. package/nugget/dist/handler.d.ts.map +1 -0
  129. package/nugget/dist/handler.js +557 -0
  130. package/nugget/dist/handler.js.map +1 -0
  131. package/nugget/dist/index.d.ts +12 -0
  132. package/nugget/dist/index.d.ts.map +1 -0
  133. package/nugget/dist/index.js +12 -0
  134. package/nugget/dist/index.js.map +1 -0
  135. package/nugget/dist/json.d.ts +7 -0
  136. package/nugget/dist/json.d.ts.map +1 -0
  137. package/nugget/dist/json.js +114 -0
  138. package/nugget/dist/json.js.map +1 -0
  139. package/nugget/dist/keys.d.ts +7 -0
  140. package/nugget/dist/keys.d.ts.map +1 -0
  141. package/nugget/dist/keys.js +75 -0
  142. package/nugget/dist/keys.js.map +1 -0
  143. package/nugget/dist/policy.d.ts +6 -0
  144. package/nugget/dist/policy.d.ts.map +1 -0
  145. package/nugget/dist/policy.js +37 -0
  146. package/nugget/dist/policy.js.map +1 -0
  147. package/nugget/dist/redact.d.ts +26 -0
  148. package/nugget/dist/redact.d.ts.map +1 -0
  149. package/nugget/dist/redact.js +74 -0
  150. package/nugget/dist/redact.js.map +1 -0
  151. package/nugget/dist/tokens.d.ts +5 -0
  152. package/nugget/dist/tokens.d.ts.map +1 -0
  153. package/nugget/dist/tokens.js +18 -0
  154. package/nugget/dist/tokens.js.map +1 -0
  155. package/nugget/dist/transport.d.ts +30 -0
  156. package/nugget/dist/transport.d.ts.map +1 -0
  157. package/nugget/dist/transport.js +165 -0
  158. package/nugget/dist/transport.js.map +1 -0
  159. package/nugget/dist/types.d.ts +285 -0
  160. package/nugget/dist/types.d.ts.map +1 -0
  161. package/nugget/dist/types.js +4 -0
  162. package/nugget/dist/types.js.map +1 -0
  163. package/nugget/dist/util.d.ts +24 -0
  164. package/nugget/dist/util.d.ts.map +1 -0
  165. package/nugget/dist/util.js +101 -0
  166. package/nugget/dist/util.js.map +1 -0
  167. package/nugget/src/adapters/engines/anthropic.ts +62 -37
  168. package/nugget/src/adapters/engines/base.ts +50 -7
  169. package/nugget/src/adapters/engines/google.ts +92 -29
  170. package/nugget/src/adapters/engines/ollama.ts +51 -17
  171. package/nugget/src/adapters/engines/openaiChat.ts +61 -32
  172. package/nugget/src/adapters/profiles.ts +53 -3
  173. package/nugget/src/agent/loop.ts +158 -14
  174. package/nugget/src/connect.ts +48 -0
  175. package/nugget/src/errors.ts +25 -1
  176. package/nugget/src/handler.ts +286 -59
  177. package/nugget/src/index.ts +1 -0
  178. package/nugget/src/keys.ts +1 -5
  179. package/nugget/src/redact.ts +32 -1
  180. package/nugget/src/transport.ts +20 -1
  181. package/nugget/src/types.ts +96 -1
  182. package/nugget/src/util.ts +55 -0
  183. package/package.json +20 -6
@@ -1,8 +1,10 @@
1
1
  import { AIError } from '../../errors.js';
2
2
  import { estimatedUsage } from '../../tokens.js';
3
3
  import { fetchJson, ndjsonLines, postResponse } from '../../transport.js';
4
- import { asNumber, asRecord, asString, textFromMessages } from '../../util.js';
5
- import { DEFAULT_TIMEOUT_MS, streamError, streamTimeout } from './base.js';
4
+ import { applyProviderOptions, asNumber, asRecord, asString, joinUrl, mapWithConcurrency, textFromMessages } from '../../util.js';
5
+ import { DEFAULT_TIMEOUT_MS, parseArgs, randomId, streamAnomaly, streamError, streamTimeout } from './base.js';
6
+ /** Max concurrent /api/show probes when listing models — enough to be fast, few enough to be polite to a local daemon. */
7
+ const SHOW_CONCURRENCY = 6;
6
8
  export class OllamaAdapter {
7
9
  provider;
8
10
  constructor(provider) {
@@ -27,14 +29,19 @@ export class OllamaAdapter {
27
29
  let inputTokens;
28
30
  let outputTokens;
29
31
  let doneReason;
32
+ let sawTerminal = false;
30
33
  const toolCalls = [];
31
34
  const timeout = streamTimeout(conn, req.signal);
32
35
  yield { type: 'start', callId: '', provider: conn.provider, model: req.model };
33
36
  try {
34
- const res = await postResponse(`${conn.baseUrl}/api/chat`, body(req), conn.headers, timeout.signal, conn.provider);
37
+ const res = await postResponse(joinUrl(conn.baseUrl, '/api/chat'), body(req), conn.headers, timeout.signal, conn.provider);
35
38
  for await (const value of ndjsonLines(res)) {
39
+ timeout.bump();
36
40
  const record = asRecord(value);
37
41
  const message = asRecord(record?.message);
42
+ const thinking = asString(message?.thinking);
43
+ if (thinking)
44
+ yield { type: 'reasoning', text: thinking };
38
45
  const piece = asString(message?.content);
39
46
  if (piece) {
40
47
  if (firstTokenMs === null)
@@ -45,6 +52,8 @@ export class OllamaAdapter {
45
52
  inputTokens = asNumber(record?.prompt_eval_count) ?? inputTokens;
46
53
  outputTokens = asNumber(record?.eval_count) ?? outputTokens;
47
54
  doneReason = asString(record?.done_reason) ?? doneReason;
55
+ if (record?.done === true)
56
+ sawTerminal = true;
48
57
  const calls = Array.isArray(message?.tool_calls) ? message.tool_calls : [];
49
58
  for (const callValue of calls) {
50
59
  const fn = asRecord(asRecord(callValue)?.function);
@@ -52,13 +61,18 @@ export class OllamaAdapter {
52
61
  const toolCall = {
53
62
  id: randomId(),
54
63
  name: asString(fn?.name) ?? 'unknown',
55
- arguments: args,
64
+ // Ollama itself sends a native object, but llama.cpp-style backends
65
+ // behind the same protocol send a JSON *string* — coerce either into
66
+ // the object `validateToolArgs` expects.
67
+ arguments: parseArgs(args),
56
68
  raw: typeof args === 'string' ? args : JSON.stringify(args),
57
69
  };
58
70
  toolCalls.push(toolCall);
59
71
  yield { type: 'tool_call', call: toolCall };
60
72
  }
61
73
  }
74
+ if (!sawTerminal)
75
+ yield streamAnomaly('NDJSON stream ended without a done record');
62
76
  const hasTools = toolCalls.length > 0;
63
77
  yield { type: 'done', result: {
64
78
  text,
@@ -86,19 +100,39 @@ export class OllamaAdapter {
86
100
  });
87
101
  const models = Array.isArray(asRecord(data)?.models) ? asRecord(data).models : [];
88
102
  const ids = models.map((model) => asString(asRecord(model)?.name) ?? asString(asRecord(model)?.model) ?? '').filter(Boolean);
89
- // Probe /api/show per model for context window + capabilities (best effort, sequential
90
- // to avoid overwhelming a local Ollama instance).
91
- const results = [];
92
- for (const id of ids) {
103
+ // Probe /api/show per model for context window + capabilities (best effort),
104
+ // with bounded concurrency so a large model list resolves quickly without
105
+ // opening an unbounded number of sockets against a local daemon.
106
+ return mapWithConcurrency(ids, SHOW_CONCURRENCY, async (id) => {
93
107
  const info = { id, source: { provider: conn.provider, connectionId: conn.id, baseUrl: conn.baseUrl } };
94
108
  const probed = await this.showModel(conn, id).catch(() => undefined);
95
109
  if (probed?.contextWindow !== undefined)
96
110
  info.contextWindow = probed.contextWindow;
97
111
  if (probed?.capabilities)
98
112
  info.capabilities = probed.capabilities;
99
- results.push(info);
100
- }
101
- return results;
113
+ return info;
114
+ });
115
+ }
116
+ async embed(conn, req) {
117
+ const inputs = Array.isArray(req.input) ? req.input : [req.input];
118
+ const { data } = await fetchJson(joinUrl(conn.baseUrl, '/api/embed'), {
119
+ method: 'POST',
120
+ headers: { 'content-type': 'application/json', ...conn.headers },
121
+ body: JSON.stringify(applyProviderOptions({ model: req.model, input: inputs }, req.providerOptions)),
122
+ timeoutMs: conn.timeoutMs ?? DEFAULT_TIMEOUT_MS,
123
+ provider: conn.provider,
124
+ });
125
+ const record = asRecord(data);
126
+ const rows = Array.isArray(record?.embeddings) ? record.embeddings : [];
127
+ const embeddings = rows.map((row) => (Array.isArray(row) ? row.filter((n) => typeof n === 'number') : []));
128
+ const inputTokens = asNumber(record?.prompt_eval_count);
129
+ return {
130
+ embeddings,
131
+ model: req.model,
132
+ usage: { inputTokens, outputTokens: 0, estimated: inputTokens === undefined },
133
+ source: { provider: conn.provider, connectionId: conn.id, baseUrl: conn.baseUrl },
134
+ raw: data,
135
+ };
102
136
  }
103
137
  async showModel(conn, model) {
104
138
  const { data } = await fetchJson(`${conn.baseUrl}/api/show`, {
@@ -130,7 +164,7 @@ export class OllamaAdapter {
130
164
  }
131
165
  }
132
166
  function body(req) {
133
- return {
167
+ const base = {
134
168
  model: req.model,
135
169
  messages: req.messages.map(toOllamaMessage),
136
170
  stream: true,
@@ -138,6 +172,11 @@ function body(req) {
138
172
  format: req.responseFormat?.type === 'json' ? (req.responseFormat.schema ?? 'json') : undefined,
139
173
  tools: req.tools?.map((tool) => ({ type: 'function', function: { name: tool.name, description: tool.description, parameters: tool.parameters } })),
140
174
  };
175
+ // providerOptions reaches Ollama's real request fields — `options.num_ctx`,
176
+ // `options.num_keep`, top-level `keep_alive`, `think`, etc. `options` is
177
+ // merged one level deep so num_ctx joins the samplers above rather than
178
+ // replacing them.
179
+ return applyProviderOptions(base, req.providerOptions, ['options']);
141
180
  }
142
181
  function toOllamaMessage(m) {
143
182
  const images = typeof m.content === 'string' ? undefined : m.content.filter((part) => part.type === 'image').map((part) => part.imageBase64).filter(Boolean);
@@ -154,7 +193,4 @@ function toOllamaMessage(m) {
154
193
  }
155
194
  return message;
156
195
  }
157
- function randomId() {
158
- return globalThis.crypto?.randomUUID?.() ?? `tool_${Date.now()}_${Math.random().toString(36).slice(2)}`;
159
- }
160
196
  //# sourceMappingURL=ollama.js.map
@@ -1,4 +1,4 @@
1
- import type { ChatRequest, ChatResult, ProviderAdapter, ResolvedConnection, StreamEvent } from '../../types.js';
1
+ import type { ChatRequest, ChatResult, EmbedRequest, EmbedResult, ProviderAdapter, ResolvedConnection, StreamEvent } from '../../types.js';
2
2
  import type { ProviderProfile } from '../profiles.js';
3
3
  export declare class OpenAIChatAdapter implements ProviderAdapter {
4
4
  private profile;
@@ -11,5 +11,6 @@ export declare class OpenAIChatAdapter implements ProviderAdapter {
11
11
  ok: boolean;
12
12
  detail?: string;
13
13
  }>;
14
+ embed(conn: ResolvedConnection, req: EmbedRequest): Promise<EmbedResult>;
14
15
  }
15
16
  //# sourceMappingURL=openaiChat.d.ts.map
@@ -1,8 +1,8 @@
1
1
  import { AIError } from '../../errors.js';
2
2
  import { estimatedUsage } from '../../tokens.js';
3
- import { postResponse, sseLines } from '../../transport.js';
4
- import { asNumber, asRecord, asString, textFromMessages } from '../../util.js';
5
- import { health, listOpenModels, streamError, streamTimeout } from './base.js';
3
+ import { fetchJson, postResponse, sseLines } from '../../transport.js';
4
+ import { applyProviderOptions, asNumber, asRecord, asString, joinUrl, textFromMessages } from '../../util.js';
5
+ import { DEFAULT_TIMEOUT_MS, health, listOpenModels, parseArgs, randomId, safeParse, streamAnomaly, streamError, streamTimeout } from './base.js';
6
6
  export class OpenAIChatAdapter {
7
7
  profile;
8
8
  provider;
@@ -34,7 +34,7 @@ export class OpenAIChatAdapter {
34
34
  const timeout = streamTimeout(conn, req.signal);
35
35
  yield { type: 'start', callId: '', provider: conn.provider, model: req.model };
36
36
  try {
37
- const res = await postResponse(urlFor(conn, this.profile, req.model), openAiBody(req, this.profile), conn.headers, timeout.signal, conn.provider);
37
+ const res = await postResponse(urlFor(conn, this.profile, req), openAiBody(req, this.profile), conn.headers, timeout.signal, conn.provider);
38
38
  const contentType = res.headers.get('content-type') ?? '';
39
39
  if (!contentType.includes('text/event-stream')) {
40
40
  // Server ignored stream:true (or is a buffered gateway) — recover the whole body.
@@ -47,6 +47,8 @@ export class OpenAIChatAdapter {
47
47
  outputTokens = parsed.outputTokens;
48
48
  finish = parsed.finish;
49
49
  sawTerminal = true;
50
+ if (parsed.reasoning)
51
+ yield { type: 'reasoning', text: parsed.reasoning };
50
52
  if (text)
51
53
  yield { type: 'delta', text };
52
54
  for (const call of toolCalls)
@@ -55,6 +57,7 @@ export class OpenAIChatAdapter {
55
57
  else {
56
58
  const partialTools = new Map();
57
59
  for await (const line of sseLines(res)) {
60
+ timeout.bump();
58
61
  const chunk = safeParse(line);
59
62
  const record = asRecord(chunk);
60
63
  if (!record)
@@ -68,6 +71,11 @@ export class OpenAIChatAdapter {
68
71
  if (choice?.finish_reason)
69
72
  sawTerminal = true;
70
73
  const delta = asRecord(choice?.delta);
74
+ // Reasoning tokens (DeepSeek `reasoning_content`, others `reasoning`)
75
+ // ride their own channel so they don't contaminate the answer text.
76
+ const reasoning = asString(delta?.reasoning_content) ?? asString(delta?.reasoning);
77
+ if (reasoning)
78
+ yield { type: 'reasoning', text: reasoning };
71
79
  const piece = asString(delta?.content);
72
80
  if (piece) {
73
81
  if (firstTokenMs === null)
@@ -96,9 +104,8 @@ export class OpenAIChatAdapter {
96
104
  for (const call of toolCalls)
97
105
  yield { type: 'tool_call', call };
98
106
  }
99
- if (!sawTerminal) {
100
- yield { type: 'context', kind: 'stream_anomaly', data: { reason: 'stream ended without a finish_reason' } };
101
- }
107
+ if (!sawTerminal)
108
+ yield streamAnomaly('stream ended without a finish_reason');
102
109
  const result = makeResult(conn, req, text, toolCalls, finish, started, firstTokenMs, inputTokens, outputTokens);
103
110
  yield { type: 'done', result };
104
111
  }
@@ -115,6 +122,34 @@ export class OpenAIChatAdapter {
115
122
  health(conn) {
116
123
  return health(conn, this.profile);
117
124
  }
125
+ async embed(conn, req) {
126
+ const inputs = Array.isArray(req.input) ? req.input : [req.input];
127
+ const { data } = await fetchJson(joinUrl(conn.baseUrl, '/embeddings'), {
128
+ method: 'POST',
129
+ headers: { 'content-type': 'application/json', ...conn.headers },
130
+ body: JSON.stringify(applyProviderOptions({ model: req.model, input: inputs }, req.providerOptions)),
131
+ timeoutMs: conn.timeoutMs ?? DEFAULT_TIMEOUT_MS,
132
+ provider: conn.provider,
133
+ });
134
+ const record = asRecord(data);
135
+ const rows = Array.isArray(record?.data) ? record.data : [];
136
+ // Order by the `index` each row reports, falling back to array order.
137
+ const byIndex = rows
138
+ .map((row, i) => ({ index: asNumber(asRecord(row)?.index) ?? i, embedding: asEmbedding(asRecord(row)?.embedding) }))
139
+ .sort((a, b) => a.index - b.index);
140
+ const usage = asRecord(record?.usage);
141
+ const inputTokens = asNumber(usage?.prompt_tokens);
142
+ return {
143
+ embeddings: byIndex.map((row) => row.embedding),
144
+ model: req.model,
145
+ usage: { inputTokens, outputTokens: 0, estimated: inputTokens === undefined },
146
+ source: { provider: conn.provider, connectionId: conn.id, baseUrl: conn.baseUrl },
147
+ raw: data,
148
+ };
149
+ }
150
+ }
151
+ function asEmbedding(value) {
152
+ return Array.isArray(value) ? value.filter((n) => typeof n === 'number') : [];
118
153
  }
119
154
  function openAiBody(req, profile) {
120
155
  const body = {
@@ -134,7 +169,12 @@ function openAiBody(req, profile) {
134
169
  };
135
170
  if (req.maxTokens !== undefined)
136
171
  body[profile.quirks?.maxTokensParam ?? 'max_tokens'] = req.maxTokens;
137
- return body;
172
+ // providerOptions carries OpenAI-native fields the nugget doesn't model
173
+ // (`reasoning_effort`, `parallel_tool_calls`, `seed`, `logprobs`, …). `apiVersion`
174
+ // is consumed by urlFor for Azure and stripped here so it never hits the body.
175
+ const { apiVersion, ...passthrough } = req.providerOptions ?? {};
176
+ void apiVersion;
177
+ return applyProviderOptions(body, Object.keys(passthrough).length ? passthrough : undefined);
138
178
  }
139
179
  function responseFormatFor(req, profile) {
140
180
  if (req.responseFormat?.type !== 'json')
@@ -157,11 +197,18 @@ function toOpenAiMessage(message) {
157
197
  tool_calls: message.toolCalls?.map((call) => ({ id: call.id, type: 'function', function: { name: call.name, arguments: call.raw ?? JSON.stringify(call.arguments) } })),
158
198
  };
159
199
  }
160
- function urlFor(conn, profile, model) {
200
+ function urlFor(conn, profile, req) {
161
201
  if (profile.quirks?.urlTemplate) {
162
- return profile.quirks.urlTemplate.replace('{baseUrl}', conn.baseUrl).replace('{model}', encodeURIComponent(model));
202
+ // `{apiVersion}` (Azure) is filled from a per-call override, then the
203
+ // profile default — so a retired Azure api-version is fixable without a
204
+ // library release: pass `providerOptions: { apiVersion: '2025-01-01-preview' }`.
205
+ const apiVersion = asString(req.providerOptions?.apiVersion) ?? profile.quirks.apiVersion ?? '';
206
+ return profile.quirks.urlTemplate
207
+ .replace('{baseUrl}', conn.baseUrl)
208
+ .replace('{model}', encodeURIComponent(req.model))
209
+ .replace('{apiVersion}', apiVersion);
163
210
  }
164
- return `${conn.baseUrl}/chat/completions`;
211
+ return joinUrl(conn.baseUrl, '/chat/completions');
165
212
  }
166
213
  function parseOpenAiResponse(raw) {
167
214
  const record = asRecord(raw);
@@ -169,9 +216,11 @@ function parseOpenAiResponse(raw) {
169
216
  const choice = asRecord(Array.isArray(record?.choices) ? record.choices[0] : undefined);
170
217
  const message = asRecord(choice?.message);
171
218
  const text = asString(message?.content) ?? asString(record?.text) ?? '';
219
+ const reasoning = asString(message?.reasoning_content) ?? asString(message?.reasoning);
172
220
  const calls = Array.isArray(message?.tool_calls) ? message.tool_calls : [];
173
221
  return {
174
222
  text,
223
+ reasoning,
175
224
  finish: asString(choice?.finish_reason),
176
225
  toolCalls: calls.map((value) => {
177
226
  const call = asRecord(value);
@@ -206,23 +255,4 @@ function mapFinish(finish, hasToolCalls) {
206
255
  return 'content_filter';
207
256
  return 'stop';
208
257
  }
209
- function parseArgs(raw) {
210
- try {
211
- return raw ? JSON.parse(raw) : {};
212
- }
213
- catch {
214
- return {};
215
- }
216
- }
217
- function safeParse(line) {
218
- try {
219
- return JSON.parse(line);
220
- }
221
- catch {
222
- return undefined;
223
- }
224
- }
225
- function randomId() {
226
- return globalThis.crypto?.randomUUID?.() ?? `tool_${Date.now()}_${Math.random().toString(36).slice(2)}`;
227
- }
228
258
  //# sourceMappingURL=openaiChat.js.map
@@ -32,11 +32,31 @@ export interface ProviderProfile {
32
32
  maxTokensRequired?: boolean;
33
33
  maxTokensParam?: 'max_tokens' | 'max_completion_tokens';
34
34
  supportsJsonSchema?: boolean;
35
+ /**
36
+ * Default value for the `{apiVersion}` token in `urlTemplate` (Azure OpenAI).
37
+ * Overridable per call via `ChatRequest.providerOptions.apiVersion`, so a
38
+ * retired Azure api-version never requires a library release.
39
+ */
40
+ apiVersion?: string;
41
+ /**
42
+ * Provider honors an `Idempotency-Key` request header. When set, the handler
43
+ * attaches one key per logical call and reuses it across retries, so a retry
44
+ * after a connection drops post-generation is deduped by the provider rather
45
+ * than billed twice. Left off unless the provider documents the header —
46
+ * sending an unknown header is harmless, but claiming the guarantee is not.
47
+ */
48
+ supportsIdempotencyKey?: boolean;
35
49
  };
36
50
  listModelsPath?: string;
37
51
  healthPath?: string;
38
52
  }
39
- export declare const PROVIDER_PROFILES: Record<string, ProviderProfile>;
53
+ /**
54
+ * The provider keys that have a profile in {@link PROVIDER_PROFILES}. Used to
55
+ * type `Connection.provider` so IDEs autocomplete valid names and typos are a
56
+ * compile error rather than a silent fall-through to `openai-compat`.
57
+ */
58
+ export type KnownProvider = 'openai' | 'azure-openai' | 'openrouter' | 'groq' | 'deepseek' | 'mistral' | 'together' | 'fireworks' | 'cerebras' | 'moonshot' | 'cohere' | 'perplexity' | 'lmstudio' | 'llamacpp' | 'vllm' | 'ollama' | 'anthropic' | 'google' | 'openai-compat';
59
+ export declare const PROVIDER_PROFILES: Record<KnownProvider, ProviderProfile> & Record<string, ProviderProfile>;
40
60
  export declare function profileFor(provider: string, baseUrl?: string): ProviderProfile;
41
61
  export declare function applyAuth(profile: ProviderProfile, apiKey: string | null, headers: Record<string, string>): Record<string, string>;
42
62
  //# sourceMappingURL=profiles.d.ts.map
@@ -7,13 +7,13 @@ export const PROVIDER_PROFILES = {
7
7
  auth: 'bearer',
8
8
  listModelsPath: '/models',
9
9
  capabilities: HOSTED_CLOUD,
10
- quirks: { supportsUsageInStream: true, maxTokensParam: 'max_completion_tokens', supportsJsonSchema: true },
10
+ quirks: { supportsUsageInStream: true, maxTokensParam: 'max_completion_tokens', supportsJsonSchema: true, supportsIdempotencyKey: true },
11
11
  },
12
12
  'azure-openai': {
13
13
  engine: 'openaiChat',
14
14
  auth: 'api-key-header',
15
15
  capabilities: HOSTED_CLOUD,
16
- quirks: { urlTemplate: '{baseUrl}/openai/deployments/{model}/chat/completions?api-version=2024-10-21', maxTokensParam: 'max_completion_tokens', supportsJsonSchema: true },
16
+ quirks: { urlTemplate: '{baseUrl}/openai/deployments/{model}/chat/completions?api-version={apiVersion}', apiVersion: '2024-10-21', maxTokensParam: 'max_completion_tokens', supportsJsonSchema: true },
17
17
  },
18
18
  openrouter: {
19
19
  engine: 'openaiChat',
@@ -29,6 +29,12 @@ export const PROVIDER_PROFILES = {
29
29
  mistral: { engine: 'openaiChat', defaultBaseUrl: 'https://api.mistral.ai/v1', auth: 'bearer', listModelsPath: '/models', capabilities: HOSTED_CLOUD },
30
30
  together: { engine: 'openaiChat', defaultBaseUrl: 'https://api.together.xyz/v1', auth: 'bearer', listModelsPath: '/models', capabilities: HOSTED_CLOUD },
31
31
  fireworks: { engine: 'openaiChat', defaultBaseUrl: 'https://api.fireworks.ai/inference/v1', auth: 'bearer', listModelsPath: '/models', capabilities: HOSTED_CLOUD },
32
+ cerebras: { engine: 'openaiChat', defaultBaseUrl: 'https://api.cerebras.ai/v1', auth: 'bearer', listModelsPath: '/models', capabilities: HOSTED_CLOUD },
33
+ moonshot: { engine: 'openaiChat', defaultBaseUrl: 'https://api.moonshot.ai/v1', auth: 'bearer', listModelsPath: '/models', capabilities: HOSTED_CLOUD },
34
+ // Cohere's OpenAI-compatible surface lives under /compatibility/v1.
35
+ cohere: { engine: 'openaiChat', defaultBaseUrl: 'https://api.cohere.ai/compatibility/v1', auth: 'bearer', listModelsPath: '/models', capabilities: HOSTED_CLOUD },
36
+ // Perplexity has no public model-listing endpoint, so listModels resolves empty.
37
+ perplexity: { engine: 'openaiChat', defaultBaseUrl: 'https://api.perplexity.ai', auth: 'bearer', capabilities: HOSTED_CLOUD },
32
38
  lmstudio: {
33
39
  engine: 'openaiChat',
34
40
  defaultBaseUrl: 'http://localhost:1234/v1',
@@ -86,6 +92,10 @@ export const PROVIDER_PROFILES = {
86
92
  // Unknown endpoint — usually pointed at a local/self-hosted server, so
87
93
  // assume the conservative local-runtime defaults rather than the hosted-cloud ones.
88
94
  capabilities: LOCAL_RUNTIME,
95
+ // Most servers reached through this escape hatch (JX Runtime and other
96
+ // local OpenAI-compatible runtimes) don't require a key at all; keep
97
+ // `auth: 'bearer'` for the ones that do (a key is only sent if provided).
98
+ quirks: { keyOptional: true },
89
99
  },
90
100
  };
91
101
  export function profileFor(provider, baseUrl) {
@@ -1,5 +1,5 @@
1
1
  import type { AIHandler } from '../handler.js';
2
- import type { ChatMessage, Connection, StreamEvent, ToolCall, Usage } from '../types.js';
2
+ import type { AIErrorKind, ChatMessage, Connection, StreamEvent, ToolCall, Usage } from '../types.js';
3
3
  import type { ToolSpec } from './tools.js';
4
4
  export interface AgentOptions {
5
5
  handler: AIHandler;
@@ -18,12 +18,43 @@ export interface AgentOptions {
18
18
  * dependent, not protocol-guaranteed.
19
19
  */
20
20
  toolMode?: 'native' | 'promptJson' | 'auto';
21
+ /**
22
+ * Discovered capabilities of the specific model (e.g. Ollama `/api/show`
23
+ * `["tools"]`). When `toolMode` is `auto`, a `tools` capability here upgrades a
24
+ * local model to `native` tool-calling instead of the profile's conservative
25
+ * `promptJson` default — so a capable local model isn't forced onto the fallback.
26
+ */
27
+ modelCapabilities?: string[];
28
+ /** Sampler/decoding options forwarded to every model turn (parity with a plain chat call). */
29
+ temperature?: number;
30
+ maxTokens?: number;
31
+ topP?: number;
32
+ stopSequences?: string[];
33
+ /** Provider-native passthrough forwarded to every turn — see {@link ChatRequest.providerOptions}. */
34
+ providerOptions?: Record<string, unknown>;
21
35
  budget?: {
22
36
  maxSteps?: number;
23
37
  maxTokens?: number;
24
38
  deadlineMs?: number;
25
39
  };
26
40
  approval?: ApprovalGate;
41
+ /**
42
+ * Which tools require the approval gate. `sideEffectsOnly` (default) gates only
43
+ * tools declared `sideEffects: true`; `all` gates every tool — so `tool_denied`
44
+ * is reachable without marking each tool, and a consumer can require approval
45
+ * for a whole run in one place.
46
+ */
47
+ approvalMode?: 'sideEffectsOnly' | 'all';
48
+ /**
49
+ * Bounds and framing applied to each tool result before it re-enters the
50
+ * model's context. `maxChars` truncates oversized results (with a notice) so a
51
+ * single large return can't blow the next turn's window; `wrapUntrusted` fences
52
+ * the result in an `<untrusted_tool_output>` envelope. Both are opt-in.
53
+ */
54
+ toolResult?: {
55
+ maxChars?: number;
56
+ wrapUntrusted?: boolean;
57
+ };
27
58
  onEvent?: (e: AgentEvent) => void;
28
59
  signal?: AbortSignal;
29
60
  metadata?: Record<string, unknown>;
@@ -38,6 +69,9 @@ export type ApprovalGate = (req: {
38
69
  export type AgentEvent = StreamEvent | {
39
70
  type: 'step_start';
40
71
  step: number;
72
+ } | {
73
+ type: 'tool_mode';
74
+ mode: 'native' | 'promptJson';
41
75
  } | {
42
76
  type: 'tool_start';
43
77
  step: number;
@@ -63,6 +97,14 @@ export interface AgentResult {
63
97
  usage: Usage;
64
98
  steps: number;
65
99
  stopReason: 'finished' | 'max_steps' | 'budget' | 'deadline' | 'canceled' | 'error';
100
+ /**
101
+ * Populated when `stopReason` is `error`: the failure that ended the run, so
102
+ * the caller can see what went wrong without replaying the event stream.
103
+ */
104
+ error?: {
105
+ kind: AIErrorKind;
106
+ message: string;
107
+ };
66
108
  }
67
109
  export declare function runAgent(opts: AgentOptions): AsyncIterable<AgentEvent> & {
68
110
  result: Promise<AgentResult>;