@jxburros/ai-nugget 0.4.1 → 0.7.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 (187) hide show
  1. package/README.md +158 -382
  2. package/UPGRADING.md +182 -0
  3. package/dist/adapters/engines/anthropic.d.ts +7 -1
  4. package/dist/adapters/engines/anthropic.js +82 -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 +7 -1
  8. package/dist/adapters/engines/google.js +80 -29
  9. package/dist/adapters/engines/ollama.d.ts +2 -1
  10. package/dist/adapters/engines/ollama.js +57 -15
  11. package/dist/adapters/engines/openaiChat.d.ts +8 -1
  12. package/dist/adapters/engines/openaiChat.js +109 -29
  13. package/dist/adapters/profiles.d.ts +21 -1
  14. package/dist/adapters/profiles.js +12 -2
  15. package/dist/agent/loop.d.ts +45 -1
  16. package/dist/agent/loop.js +117 -12
  17. package/dist/cjs/adapters/engines/anthropic.js +326 -0
  18. package/dist/cjs/adapters/engines/base.js +147 -0
  19. package/dist/cjs/adapters/engines/google.js +291 -0
  20. package/dist/cjs/adapters/engines/ollama.js +205 -0
  21. package/dist/cjs/adapters/engines/openaiChat.js +312 -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 +373 -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 +586 -0
  30. package/dist/cjs/index.js +28 -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/reasoning.js +159 -0
  36. package/dist/cjs/redact.js +78 -0
  37. package/dist/cjs/tokens.js +22 -0
  38. package/dist/cjs/transport.js +172 -0
  39. package/dist/cjs/types.js +6 -0
  40. package/dist/cjs/util.js +126 -0
  41. package/dist/errors.js +25 -1
  42. package/dist/handler.d.ts +67 -1
  43. package/dist/handler.js +266 -61
  44. package/dist/index.d.ts +1 -0
  45. package/dist/index.js +1 -0
  46. package/dist/reasoning.d.ts +45 -0
  47. package/dist/reasoning.js +154 -0
  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 +112 -2
  53. package/dist/util.d.ts +22 -0
  54. package/dist/util.js +59 -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 +323 -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 +288 -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 +202 -0
  71. package/nugget/dist/adapters/engines/ollama.js.map +1 -0
  72. package/nugget/dist/adapters/engines/openaiChat.d.ts +22 -0
  73. package/nugget/dist/adapters/engines/openaiChat.d.ts.map +1 -0
  74. package/nugget/dist/adapters/engines/openaiChat.js +308 -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 +114 -0
  89. package/nugget/dist/agent/loop.d.ts.map +1 -0
  90. package/nugget/dist/agent/loop.js +371 -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 +326 -0
  97. package/nugget/dist/cjs/adapters/engines/base.js +147 -0
  98. package/nugget/dist/cjs/adapters/engines/google.js +291 -0
  99. package/nugget/dist/cjs/adapters/engines/ollama.js +205 -0
  100. package/nugget/dist/cjs/adapters/engines/openaiChat.js +312 -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 +373 -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 +586 -0
  109. package/nugget/dist/cjs/index.js +28 -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/reasoning.js +159 -0
  115. package/nugget/dist/cjs/redact.js +78 -0
  116. package/nugget/dist/cjs/tokens.js +22 -0
  117. package/nugget/dist/cjs/transport.js +172 -0
  118. package/nugget/dist/cjs/types.js +6 -0
  119. package/nugget/dist/cjs/util.js +126 -0
  120. package/nugget/dist/connect.d.ts +31 -0
  121. package/nugget/dist/connect.d.ts.map +1 -0
  122. package/nugget/dist/connect.js +25 -0
  123. package/nugget/dist/connect.js.map +1 -0
  124. package/nugget/dist/errors.d.ts +22 -0
  125. package/nugget/dist/errors.d.ts.map +1 -0
  126. package/nugget/dist/errors.js +104 -0
  127. package/nugget/dist/errors.js.map +1 -0
  128. package/nugget/dist/handler.d.ts +114 -0
  129. package/nugget/dist/handler.d.ts.map +1 -0
  130. package/nugget/dist/handler.js +582 -0
  131. package/nugget/dist/handler.js.map +1 -0
  132. package/nugget/dist/index.d.ts +13 -0
  133. package/nugget/dist/index.d.ts.map +1 -0
  134. package/nugget/dist/index.js +13 -0
  135. package/nugget/dist/index.js.map +1 -0
  136. package/nugget/dist/json.d.ts +7 -0
  137. package/nugget/dist/json.d.ts.map +1 -0
  138. package/nugget/dist/json.js +114 -0
  139. package/nugget/dist/json.js.map +1 -0
  140. package/nugget/dist/keys.d.ts +7 -0
  141. package/nugget/dist/keys.d.ts.map +1 -0
  142. package/nugget/dist/keys.js +75 -0
  143. package/nugget/dist/keys.js.map +1 -0
  144. package/nugget/dist/policy.d.ts +6 -0
  145. package/nugget/dist/policy.d.ts.map +1 -0
  146. package/nugget/dist/policy.js +37 -0
  147. package/nugget/dist/policy.js.map +1 -0
  148. package/nugget/dist/reasoning.d.ts +45 -0
  149. package/nugget/dist/reasoning.d.ts.map +1 -0
  150. package/nugget/dist/reasoning.js +154 -0
  151. package/nugget/dist/reasoning.js.map +1 -0
  152. package/nugget/dist/redact.d.ts +26 -0
  153. package/nugget/dist/redact.d.ts.map +1 -0
  154. package/nugget/dist/redact.js +74 -0
  155. package/nugget/dist/redact.js.map +1 -0
  156. package/nugget/dist/tokens.d.ts +5 -0
  157. package/nugget/dist/tokens.d.ts.map +1 -0
  158. package/nugget/dist/tokens.js +18 -0
  159. package/nugget/dist/tokens.js.map +1 -0
  160. package/nugget/dist/transport.d.ts +30 -0
  161. package/nugget/dist/transport.d.ts.map +1 -0
  162. package/nugget/dist/transport.js +165 -0
  163. package/nugget/dist/transport.js.map +1 -0
  164. package/nugget/dist/types.d.ts +301 -0
  165. package/nugget/dist/types.d.ts.map +1 -0
  166. package/nugget/dist/types.js +4 -0
  167. package/nugget/dist/types.js.map +1 -0
  168. package/nugget/dist/util.d.ts +31 -0
  169. package/nugget/dist/util.d.ts.map +1 -0
  170. package/nugget/dist/util.js +114 -0
  171. package/nugget/dist/util.js.map +1 -0
  172. package/nugget/src/adapters/engines/anthropic.ts +78 -37
  173. package/nugget/src/adapters/engines/base.ts +50 -7
  174. package/nugget/src/adapters/engines/google.ts +79 -29
  175. package/nugget/src/adapters/engines/ollama.ts +56 -17
  176. package/nugget/src/adapters/engines/openaiChat.ts +104 -30
  177. package/nugget/src/adapters/profiles.ts +53 -3
  178. package/nugget/src/agent/loop.ts +156 -13
  179. package/nugget/src/errors.ts +25 -1
  180. package/nugget/src/handler.ts +308 -59
  181. package/nugget/src/index.ts +1 -0
  182. package/nugget/src/reasoning.ts +174 -0
  183. package/nugget/src/redact.ts +32 -1
  184. package/nugget/src/transport.ts +20 -1
  185. package/nugget/src/types.ts +113 -1
  186. package/nugget/src/util.ts +64 -0
  187. package/package.json +18 -5
@@ -1,6 +1,10 @@
1
1
  import type { Redactor } from './types.js';
2
2
 
3
- const SECRET_PATTERNS: RegExp[] = [
3
+ /**
4
+ * Prefixed secret formats. Each pattern is anchored on a vendor prefix that is
5
+ * distinctive enough that a match is a secret, not a coincidence.
6
+ */
7
+ export const SECRET_PATTERNS: RegExp[] = [
4
8
  /sk-[A-Za-z0-9_-]{20,}/g,
5
9
  /sk-proj-[A-Za-z0-9_-]{20,}/g,
6
10
  /sk-ant-[A-Za-z0-9_-]{20,}/g,
@@ -23,12 +27,39 @@ const SECRET_PATTERNS: RegExp[] = [
23
27
  /eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g,
24
28
  ];
25
29
 
30
+ /**
31
+ * Unprefixed/opaque secrets — Azure OpenAI `api-key` values, AWS *secret*
32
+ * access keys, generic bearer/session tokens — carry no distinctive prefix, so
33
+ * matching them on shape alone would redact commit SHAs, content hashes, and
34
+ * base64 payloads. These patterns instead anchor on the *label* that precedes
35
+ * the value (`api-key: …`, `"aws_secret_access_key": "…"`, `password=…`) and
36
+ * redact only the value, keeping the label readable in logs.
37
+ *
38
+ * Exact-match registration (`SessionRedactor.addSecret`, which the handler does
39
+ * for every resolved key) remains the only guaranteed catch for a bare,
40
+ * unlabeled, unprefixed secret.
41
+ */
42
+ export const LABELED_SECRET_PATTERNS: RegExp[] = [
43
+ new RegExp(
44
+ // Lookbehind rather than `\b` so an underscore-joined prefix still matches
45
+ // the label (`aws_secret_access_key`, `azure_api_key`).
46
+ String.raw`(?<![A-Za-z0-9])(api[_-]?key|apikey|secret[_-]?access[_-]?key|access[_-]?token|auth[_-]?token|session[_-]?token|client[_-]?secret|refresh[_-]?token|private[_-]?key|password|passwd)` +
47
+ // Optional closing quote handles JSON keys (`"api_key": "…"`).
48
+ String.raw`(["']?\s*[:=]\s*|\s+)` +
49
+ String.raw`(["']?)([A-Za-z0-9/+_.~-]{16,}={0,2})\3`,
50
+ 'gi',
51
+ ),
52
+ ];
53
+
26
54
  export function createDefaultRedactor(extraSecrets: Iterable<string> = []): Redactor {
27
55
  const secrets = new Set([...extraSecrets].filter((value) => value.length >= 6));
28
56
  return {
29
57
  redact(text: string): string {
30
58
  let output = text;
31
59
  for (const pattern of SECRET_PATTERNS) output = output.replace(pattern, '[REDACTED]');
60
+ for (const pattern of LABELED_SECRET_PATTERNS) {
61
+ output = output.replace(pattern, (_match, label: string, separator: string, quote: string) => `${label}${separator}${quote}[REDACTED]${quote}`);
62
+ }
32
63
  for (const secret of secrets) output = output.split(secret).join('[REDACTED]');
33
64
  return output;
34
65
  },
@@ -1,9 +1,11 @@
1
1
  import { AIError, classify, fromUnknown } from './errors.js';
2
2
 
3
- export function withTimeout(ms: number, external?: AbortSignal): {
3
+ export function withTimeout(ms: number, external?: AbortSignal, idleMs?: number): {
4
4
  signal: AbortSignal;
5
5
  done(): void;
6
6
  timedOut(): boolean;
7
+ /** Reset the idle timer. No-op unless `idleMs` was supplied. Call on each chunk. */
8
+ bump(): void;
7
9
  } {
8
10
  const controller = new AbortController();
9
11
  let didTimeOut = false;
@@ -11,20 +13,37 @@ export function withTimeout(ms: number, external?: AbortSignal): {
11
13
  didTimeOut = true;
12
14
  controller.abort(new AIError(`Request timed out after ${ms}ms`, { kind: 'timeout' }));
13
15
  }, ms);
16
+ // Optional idle timeout: aborts only when no chunk has arrived for `idleMs`,
17
+ // rearmed on every `bump()`. Keeps a long but healthy stream alive while the
18
+ // total `ms` ceiling still bounds a truly stuck call.
19
+ let idleTimer: ReturnType<typeof setTimeout> | undefined;
20
+ const armIdle = (): void => {
21
+ if (idleMs === undefined) return;
22
+ if (idleTimer) clearTimeout(idleTimer);
23
+ idleTimer = setTimeout(() => {
24
+ didTimeOut = true;
25
+ controller.abort(new AIError(`Request idle for ${idleMs}ms`, { kind: 'timeout' }));
26
+ }, idleMs);
27
+ };
14
28
  const onAbort = () => controller.abort(external?.reason);
15
29
  if (external) {
16
30
  if (external.aborted) onAbort();
17
31
  else external.addEventListener('abort', onAbort, { once: true });
18
32
  }
33
+ armIdle();
19
34
  return {
20
35
  signal: controller.signal,
21
36
  done() {
22
37
  clearTimeout(timer);
38
+ if (idleTimer) clearTimeout(idleTimer);
23
39
  external?.removeEventListener('abort', onAbort);
24
40
  },
25
41
  timedOut() {
26
42
  return didTimeOut;
27
43
  },
44
+ bump() {
45
+ armIdle();
46
+ },
28
47
  };
29
48
  }
30
49
 
@@ -1,3 +1,7 @@
1
+ import type { KnownProvider } from './adapters/profiles.js';
2
+
3
+ export type { KnownProvider };
4
+
1
5
  export type Role = 'system' | 'user' | 'assistant' | 'tool';
2
6
 
3
7
  export interface ContentPart {
@@ -29,27 +33,69 @@ export interface ToolSchema {
29
33
  parameters: object;
30
34
  }
31
35
 
36
+ /**
37
+ * How hard a reasoning-capable model should think before answering. The nugget
38
+ * maps one vocabulary onto each provider's own knob (see `docs/providers.md`,
39
+ * "Reasoning effort"): OpenAI `reasoning_effort`, Anthropic `thinking`
40
+ * (`budget_tokens` tiers), Google `generationConfig.thinkingConfig.thinkingBudget`,
41
+ * Ollama `think`. `'none'` disables reasoning where the provider allows it.
42
+ * Unset means the provider's own default — the nugget never sends the knob
43
+ * unasked, because non-reasoning models reject it.
44
+ */
45
+ export type ReasoningEffort = 'none' | 'minimal' | 'low' | 'medium' | 'high';
46
+
32
47
  export interface ChatRequest {
33
48
  model: string;
34
49
  messages: ChatMessage[];
35
50
  temperature?: number;
36
51
  maxTokens?: number;
37
52
  topP?: number;
53
+ /**
54
+ * Reasoning effort for thinking-capable models — see {@link ReasoningEffort}.
55
+ * A provider-native value in `providerOptions` (e.g. `reasoning_effort`,
56
+ * `thinking`, `thinkingConfig`, `think`) wins over this on collision.
57
+ */
58
+ reasoningEffort?: ReasoningEffort;
38
59
  responseFormat?: { type: 'text' } | { type: 'json'; schema?: object };
39
60
  tools?: ToolSchema[];
40
61
  toolChoice?: 'auto' | 'none' | { name: string };
41
62
  stopSequences?: string[];
42
63
  signal?: AbortSignal;
43
64
  metadata?: Record<string, unknown>;
65
+ /**
66
+ * Provider-native request fields the nugget does not model as first-class
67
+ * options — an escape hatch so a caller can reach a capability without waiting
68
+ * for a library release. Merged into the outgoing provider request body:
69
+ * shallow at the top level, and one level deep into the provider's own nested
70
+ * option container (Ollama `options`, Google `generationConfig`). Examples:
71
+ * `{ options: { num_ctx: 8192 }, keep_alive: '30m' }` (Ollama),
72
+ * `{ reasoning_effort: 'high' }` (OpenAI), `{ thinking: { type: 'enabled' } }`
73
+ * (Anthropic), `{ safetySettings: [...] }` (Google). Values here win over the
74
+ * nugget's own fields on a key collision, so use with care.
75
+ */
76
+ providerOptions?: Record<string, unknown>;
44
77
  }
45
78
 
46
79
  export interface Connection {
47
80
  id: string;
48
- provider: string;
81
+ /**
82
+ * A provider with a profile in the table, or any other string — an unknown
83
+ * key with a `baseUrl` resolves through the `openai-compat` escape hatch.
84
+ * The union is there for autocomplete and typo-catching; the `string & {}`
85
+ * arm keeps the escape hatch open.
86
+ */
87
+ provider: KnownProvider | (string & {});
49
88
  baseUrl?: string;
50
89
  keyRef?: KeyRef;
51
90
  timeoutMs?: number;
52
91
  headers?: Record<string, string>;
92
+ /**
93
+ * Idle timeout for streaming calls: aborts the stream only if no bytes arrive
94
+ * for this many milliseconds, resetting on every chunk. Distinct from
95
+ * `timeoutMs`, which bounds the total call lifetime. Set this (and a generous
96
+ * `timeoutMs`) for long local generations that stream slowly but healthily.
97
+ */
98
+ idleTimeoutMs?: number;
53
99
  }
54
100
 
55
101
  export type KeyRef =
@@ -95,6 +141,25 @@ export interface ModelSource {
95
141
  baseUrl?: string;
96
142
  }
97
143
 
144
+ export interface EmbedRequest {
145
+ model: string;
146
+ /** One string, or a batch of strings embedded in a single call. */
147
+ input: string | string[];
148
+ signal?: AbortSignal;
149
+ metadata?: Record<string, unknown>;
150
+ /** Provider-native passthrough, same contract as {@link ChatRequest.providerOptions}. */
151
+ providerOptions?: Record<string, unknown>;
152
+ }
153
+
154
+ export interface EmbedResult {
155
+ /** One vector per input, in input order. */
156
+ embeddings: number[][];
157
+ model: string;
158
+ usage: Usage;
159
+ source: ModelSource;
160
+ raw?: unknown;
161
+ }
162
+
98
163
  export function modelRef(source: ModelSource, model: string): string {
99
164
  return `${source.provider}/${model}`;
100
165
  }
@@ -107,6 +172,8 @@ export type AIErrorKind =
107
172
  | 'server'
108
173
  | 'invalid_request'
109
174
  | 'invalid_response'
175
+ /** 404/410 — endpoint, deployment, or model not found (wrong baseUrl/model, or a misrouted edge). */
176
+ | 'not_found'
110
177
  | 'context_length'
111
178
  | 'canceled'
112
179
  | 'policy_blocked'
@@ -117,6 +184,11 @@ export type AIErrorKind =
117
184
  export type StreamEvent =
118
185
  | { type: 'start'; callId: string; provider: string; model: string }
119
186
  | { type: 'delta'; text: string }
187
+ // Reasoning/thinking tokens surfaced as their own channel so callers can show
188
+ // or discard them without them polluting `delta` (the user-facing answer).
189
+ // Emitted from Anthropic `thinking_delta` and OpenAI-compat `reasoning`/
190
+ // `reasoning_content` deltas; providers that don't stream reasoning never emit it.
191
+ | { type: 'reasoning'; text: string }
120
192
  | { type: 'tool_call'; call: ToolCall }
121
193
  | { type: 'context'; kind: string; data: unknown }
122
194
  | { type: 'retry'; attempt: number; reason: AIErrorKind; delayMs: number }
@@ -129,6 +201,7 @@ export interface ProviderAdapter {
129
201
  stream(conn: ResolvedConnection, req: ChatRequest): AsyncIterable<StreamEvent>;
130
202
  listModels?(conn: ResolvedConnection): Promise<ModelInfo[]>;
131
203
  health?(conn: ResolvedConnection): Promise<{ ok: boolean; detail?: string }>;
204
+ embed?(conn: ResolvedConnection, req: EmbedRequest): Promise<EmbedResult>;
132
205
  }
133
206
 
134
207
  export interface ModelInfo {
@@ -156,6 +229,15 @@ export interface CallInfo {
156
229
  provider: string;
157
230
  model: string;
158
231
  metadata?: Record<string, unknown>;
232
+ /**
233
+ * The fully resolved connection for this call — base URL defaulted from the
234
+ * provider profile and auth headers applied. Present whenever `beforeCall`
235
+ * runs after resolution (chat/stream and probes), letting a hook validate the
236
+ * *effective* endpoint (e.g. a request-time SSRF/DNS-rebinding re-check on the
237
+ * resolved `baseUrl`) without duplicating profile logic. The resolved
238
+ * `apiKey` is intentionally not exposed here.
239
+ */
240
+ resolved?: Omit<ResolvedConnection, 'apiKey'>;
159
241
  }
160
242
 
161
243
  export interface CallRecord {
@@ -172,4 +254,34 @@ export interface CallRecord {
172
254
  metadata?: Record<string, unknown>;
173
255
  promptChars: number;
174
256
  responseChars: number;
257
+ /**
258
+ * Estimated cost of this call in USD, computed by the optional `pricing` hook
259
+ * passed to the handler. Undefined when no pricing hook is configured or the
260
+ * hook returns nothing for this provider/model.
261
+ */
262
+ costUsd?: number;
263
+ }
264
+
265
+ /**
266
+ * Minimal [Standard Schema](https://standardschema.dev) validator surface. Any
267
+ * Zod / Valibot / ArkType schema (v1+) satisfies this via its `~standard`
268
+ * property, so `chatParsed` can validate model output against a schema the app
269
+ * already owns — with zero dependency added to this library.
270
+ */
271
+ export interface StandardSchemaV1<Output = unknown> {
272
+ readonly '~standard': {
273
+ readonly version: 1;
274
+ readonly vendor: string;
275
+ readonly validate: (value: unknown) => StandardSchemaResult<Output> | Promise<StandardSchemaResult<Output>>;
276
+ };
277
+ }
278
+
279
+ export type StandardSchemaResult<Output> =
280
+ | { readonly value: Output; readonly issues?: undefined }
281
+ | { readonly issues: ReadonlyArray<{ readonly message: string; readonly path?: ReadonlyArray<PropertyKey | { readonly key: PropertyKey }> }> };
282
+
283
+ /** Output of {@link AIHandler.chatParsed}: the validated value plus the raw chat result. */
284
+ export interface ParsedResult<T> {
285
+ data: T;
286
+ result: ChatResult;
175
287
  }
@@ -31,6 +31,56 @@ export function globalEnv(): Record<string, string | undefined> {
31
31
  return maybeProcess.process?.env ?? {};
32
32
  }
33
33
 
34
+ /**
35
+ * Merges app-supplied {@link ChatRequest.providerOptions} into a provider
36
+ * request body: shallow at the top level, and one level deep for each key in
37
+ * `nestedKeys` where both sides are plain objects (Ollama `options`, Google
38
+ * `generationConfig`). Values in `providerOptions` win on collision.
39
+ */
40
+ export function applyProviderOptions(
41
+ body: Record<string, unknown>,
42
+ providerOptions: Record<string, unknown> | undefined,
43
+ nestedKeys: readonly string[] = [],
44
+ ): Record<string, unknown> {
45
+ if (!providerOptions) return body;
46
+ const merged: Record<string, unknown> = { ...body };
47
+ for (const [key, value] of Object.entries(providerOptions)) {
48
+ const base = asRecord(merged[key]);
49
+ if (nestedKeys.includes(key) && base && asRecord(value)) {
50
+ merged[key] = { ...base, ...(value as Record<string, unknown>) };
51
+ } else {
52
+ merged[key] = value;
53
+ }
54
+ }
55
+ return merged;
56
+ }
57
+
58
+ /** Joins a base URL and path without producing a double slash, even if `base` ends with `/`. */
59
+ export function joinUrl(base: string, path: string): string {
60
+ return `${base.replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}`;
61
+ }
62
+
63
+ /**
64
+ * Runs `fn` over `items` with at most `limit` in flight at once, preserving
65
+ * input order in the returned array. Used to probe many local models
66
+ * concurrently without opening an unbounded number of sockets.
67
+ */
68
+ export async function mapWithConcurrency<T, R>(items: readonly T[], limit: number, fn: (item: T, index: number) => Promise<R>): Promise<R[]> {
69
+ const results = new Array<R>(items.length);
70
+ let next = 0;
71
+ const workerCount = Math.max(1, Math.min(limit, items.length));
72
+ const worker = async (): Promise<void> => {
73
+ for (;;) {
74
+ const index = next;
75
+ next += 1;
76
+ if (index >= items.length) return;
77
+ results[index] = await fn(items[index]!, index);
78
+ }
79
+ };
80
+ await Promise.all(Array.from({ length: workerCount }, worker));
81
+ return results;
82
+ }
83
+
34
84
  export function sleep(ms: number, signal?: AbortSignal): Promise<void> {
35
85
  if (ms <= 0) return Promise.resolve();
36
86
  return new Promise((resolve, reject) => {
@@ -54,3 +104,17 @@ export function sleep(ms: number, signal?: AbortSignal): Promise<void> {
54
104
  }
55
105
  });
56
106
  }
107
+
108
+ /**
109
+ * Token budgets behind the {@link ReasoningEffort} tiers for providers that
110
+ * express effort as a thinking-token budget (Anthropic `budget_tokens`, Google
111
+ * `thinkingBudget`). `'none'` is 0. Deliberately conservative: they are floors
112
+ * a model may stop under, not ceilings it fills.
113
+ */
114
+ export const REASONING_BUDGET_TOKENS: Record<'none' | 'minimal' | 'low' | 'medium' | 'high', number> = {
115
+ none: 0,
116
+ minimal: 1024,
117
+ low: 2048,
118
+ medium: 8192,
119
+ high: 16384,
120
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxburros/ai-nugget",
3
- "version": "0.4.1",
3
+ "version": "0.7.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Small isomorphic AI model nugget for provider calls, policy, redaction, telemetry, and agent loops.",
@@ -34,37 +34,50 @@
34
34
  ".": {
35
35
  "types": "./dist/index.d.ts",
36
36
  "import": "./dist/index.js",
37
+ "require": "./dist/cjs/index.js",
37
38
  "default": "./dist/index.js"
38
39
  },
39
40
  "./agent": {
40
41
  "types": "./dist/agent/index.d.ts",
41
42
  "import": "./dist/agent/index.js",
43
+ "require": "./dist/cjs/agent/index.js",
42
44
  "default": "./dist/agent/index.js"
43
45
  }
44
46
  },
45
47
  "files": [
46
48
  "dist/**/*.js",
47
49
  "dist/**/*.d.ts",
50
+ "dist/cjs/package.json",
48
51
  "nugget",
49
52
  "README.md",
53
+ "UPGRADING.md",
50
54
  "LICENSE"
51
55
  ],
52
56
  "scripts": {
53
- "build": "tsc -p tsconfig.json",
57
+ "build": "tsc -p tsconfig.json && tsc -p tsconfig.cjs.json && node scripts/finalize-cjs.mjs",
54
58
  "test": "vitest run",
55
59
  "test:browser": "vitest run --config vitest.browser.config.ts",
56
60
  "test:live": "vitest run tests/live-smoke.test.ts",
61
+ "test:coverage": "vitest run --coverage",
57
62
  "typecheck": "tsc -p tsconfig.json --noEmit",
63
+ "lint": "eslint .",
58
64
  "build:nugget": "node scripts/build-nugget.mjs",
59
65
  "demo:steel-chat": "npm run build && node examples/steel-chat/server.mjs",
60
66
  "prepublishOnly": "npm run build && npm run build:nugget"
61
67
  },
62
68
  "devDependencies": {
63
69
  "@types/node": "^22.15.21",
64
- "@vitest/browser": "^4.1.9",
65
- "@vitest/browser-playwright": "^4.1.9",
70
+ "@vitest/browser": "^4.1.10",
71
+ "@vitest/browser-playwright": "^4.1.10",
72
+ "@vitest/coverage-v8": "^4.1.10",
73
+ "eslint": "^9.18.0",
66
74
  "playwright": "^1.61.1",
67
75
  "typescript": "^5.8.3",
68
- "vitest": "^4.1.5"
76
+ "typescript-eslint": "^8.21.0",
77
+ "vitest": "^4.1.10"
78
+ },
79
+ "overrides": {
80
+ "nanoid": "^3.3.17",
81
+ "postcss": "^8.5.23"
69
82
  }
70
83
  }