@stacksjs/ai 0.70.88 → 0.70.90

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 (45) hide show
  1. package/dist/agents/claude/index.d.ts +29 -0
  2. package/dist/agents/claude/index.js +197 -0
  3. package/dist/agents/index.d.ts +6 -0
  4. package/dist/agents/index.js +1 -0
  5. package/dist/buddy.d.ts +67 -0
  6. package/dist/buddy.js +393 -0
  7. package/dist/drivers/anthropic/index.d.ts +40 -0
  8. package/dist/drivers/anthropic/index.js +276 -0
  9. package/dist/drivers/claude-agent-sdk/index.d.ts +40 -0
  10. package/dist/drivers/claude-agent-sdk/index.js +198 -0
  11. package/dist/drivers/index.d.ts +13 -0
  12. package/dist/drivers/index.js +4 -0
  13. package/dist/drivers/ollama/index.d.ts +93 -0
  14. package/dist/drivers/ollama/index.js +332 -0
  15. package/dist/drivers/openai/index.d.ts +66 -0
  16. package/dist/drivers/openai/index.js +351 -0
  17. package/dist/image.d.ts +83 -0
  18. package/dist/image.js +375 -0
  19. package/dist/index.d.ts +39 -0
  20. package/dist/index.js +16 -0
  21. package/dist/mcp.d.ts +115 -0
  22. package/dist/mcp.js +361 -0
  23. package/dist/personalization.d.ts +118 -0
  24. package/dist/personalization.js +244 -0
  25. package/dist/search.d.ts +101 -0
  26. package/dist/search.js +316 -0
  27. package/dist/text.d.ts +10 -0
  28. package/dist/text.js +51 -0
  29. package/dist/types.d.ts +185 -0
  30. package/dist/types.js +0 -0
  31. package/dist/utils/client-bedrock-runtime.d.ts +16 -0
  32. package/dist/utils/client-bedrock-runtime.js +17 -0
  33. package/dist/utils/client-bedrock.d.ts +27 -0
  34. package/dist/utils/client-bedrock.js +20 -0
  35. package/dist/utils/model-access.d.ts +1 -0
  36. package/dist/utils/model-access.js +21 -0
  37. package/dist/utils/retry.d.ts +38 -0
  38. package/dist/utils/retry.js +39 -0
  39. package/dist/utils/tokens.d.ts +50 -0
  40. package/dist/utils/tokens.js +59 -0
  41. package/dist/utils/usage.d.ts +56 -0
  42. package/dist/utils/usage.js +27 -0
  43. package/dist/utils/vision.d.ts +22 -0
  44. package/dist/utils/vision.js +54 -0
  45. package/package.json +1 -1
package/dist/text.js ADDED
@@ -0,0 +1,51 @@
1
+ import { invokeModel } from "./utils/client-bedrock-runtime";
2
+ const DEFAULT_MODEL = "amazon.titan-text-express-v1";
3
+ function resolveModel(override) {
4
+ if (override)
5
+ return override;
6
+ return globalThis.config?.ai?.bedrock?.model || process.env.BEDROCK_MODEL_ID || DEFAULT_MODEL;
7
+ }
8
+ export async function summarize(text, options = {}) {
9
+ const { maxTokenCount = 512, temperature = 0, topP = 0.9, modelId } = options;
10
+ try {
11
+ const response = await invokeModel({
12
+ modelId: resolveModel(modelId),
13
+ contentType: "application/json",
14
+ accept: "*/*",
15
+ body: JSON.stringify({
16
+ inputText: `Summarize the following text: ${text}`,
17
+ textGenerationConfig: {
18
+ maxTokenCount,
19
+ stopSequences: [],
20
+ temperature,
21
+ topP
22
+ }
23
+ })
24
+ });
25
+ return JSON.parse(new TextDecoder().decode(response.body)).results[0].outputText;
26
+ } catch (error) {
27
+ throw Error(`Error summarizing text: ${error.message}`);
28
+ }
29
+ }
30
+ export async function ask(question, options = {}) {
31
+ const { maxTokenCount = 512, temperature = 0, topP = 0.9, modelId } = options;
32
+ try {
33
+ const response = await invokeModel({
34
+ modelId: resolveModel(modelId),
35
+ contentType: "application/json",
36
+ accept: "*/*",
37
+ body: JSON.stringify({
38
+ inputText: question,
39
+ textGenerationConfig: {
40
+ maxTokenCount,
41
+ stopSequences: [],
42
+ temperature,
43
+ topP
44
+ }
45
+ })
46
+ });
47
+ return JSON.parse(new TextDecoder().decode(response.body)).results[0].outputText;
48
+ } catch (error) {
49
+ throw Error(`Error asking question: ${error.message}`);
50
+ }
51
+ }
@@ -0,0 +1,185 @@
1
+ /**
2
+ * AI Module Types
3
+ *
4
+ * Shared type definitions for AI drivers and agents.
5
+ */
6
+ export declare interface AIMessage {
7
+ role: 'user' | 'assistant' | 'system'
8
+ content: string | AIMessageContent[]
9
+ }
10
+ export declare interface AIMessageContent {
11
+ type: 'text' | 'image_url' | 'image'
12
+ text?: string
13
+ image_url?: { url: string, detail?: 'auto' | 'low' | 'high' }
14
+ source?: { type: 'base64', media_type: string, data: string }
15
+ }
16
+ export declare interface AIDriver {
17
+ name: string
18
+ process: (command: string, context: string, history: AIMessage[]) => Promise<string>
19
+ stream?: (command: string, context: string, history: AIMessage[]) => AsyncGenerator<string>
20
+ embed?: (input: string | string[]) => Promise<number[] | number[][]>
21
+ }
22
+ export declare interface AIDriverConfig {
23
+ apiKey?: string
24
+ baseUrl?: string
25
+ model?: string
26
+ maxTokens?: number
27
+ }
28
+ export declare interface StreamingResult {
29
+ stream: ReadableStream<Uint8Array>
30
+ fullResponse: Promise<string>
31
+ }
32
+ export declare interface EmbeddingResult {
33
+ embedding: number[]
34
+ index: number
35
+ object: string
36
+ }
37
+ export declare interface EmbeddingsResponse {
38
+ data: EmbeddingResult[]
39
+ model: string
40
+ usage: {
41
+ prompt_tokens: number
42
+ total_tokens: number
43
+ }
44
+ }
45
+ /**
46
+ * Tool / function definition that the model can call back into.
47
+ * Cross-provider shape: OpenAI's `tools[]` and Anthropic's `tools[]`
48
+ * map to this same structure via the JSON Schema for parameters.
49
+ */
50
+ export declare interface AITool {
51
+ name: string
52
+ description?: string
53
+ parameters?: Record<string, unknown>
54
+ }
55
+ export declare interface ChatCompletionOptions {
56
+ model?: string
57
+ maxTokens?: number
58
+ temperature?: number
59
+ topP?: number
60
+ stop?: string | string[]
61
+ stream?: boolean
62
+ tools?: AITool[]
63
+ toolChoice?: 'auto' | 'required' | 'none' | { name: string }
64
+ responseFormat?: AIResponseFormat
65
+ }
66
+ export declare interface AIResult {
67
+ content: string
68
+ model: string
69
+ usage?: {
70
+ promptTokens: number
71
+ completionTokens: number
72
+ totalTokens: number
73
+ }
74
+ finishReason?: string
75
+ }
76
+ export declare interface ClaudeAPIResponse {
77
+ content: Array<{ type: string, text: string }>
78
+ }
79
+ export declare interface OpenAIAPIResponse {
80
+ choices: Array<{ message: { content: string } }>
81
+ }
82
+ export declare interface OllamaAPIResponse {
83
+ message: { content: string }
84
+ }
85
+ export declare interface ClaudeStreamEvent {
86
+ type: string
87
+ subtype?: string
88
+ message?: {
89
+ content: Array<{
90
+ type: string
91
+ text?: string
92
+ name?: string
93
+ input?: Record<string, unknown>
94
+ }>
95
+ }
96
+ delta?: { text?: string }
97
+ result?: string
98
+ index?: number
99
+ content_block?: {
100
+ type: string
101
+ text?: string
102
+ }
103
+ }
104
+ // Buddy Types
105
+ export declare interface RepoState {
106
+ path: string
107
+ name: string
108
+ branch: string
109
+ hasChanges: boolean
110
+ lastCommit?: string
111
+ }
112
+ export declare interface GitHubCredentials {
113
+ token: string
114
+ username: string
115
+ name: string
116
+ email: string
117
+ }
118
+ export declare interface BuddyState {
119
+ repo: RepoState | null
120
+ conversationHistory: AIMessage[]
121
+ currentDriver: string
122
+ github: GitHubCredentials | null
123
+ }
124
+ export declare interface BuddyConfig {
125
+ workDir: string
126
+ commitMessage: string
127
+ ollamaHost: string
128
+ ollamaModel: string
129
+ }
130
+ export declare interface BuddyApiKeys {
131
+ anthropic?: string
132
+ openai?: string
133
+ claudeCliHost?: string
134
+ }
135
+ // Image types
136
+ export declare interface ImageGenerationConfig {
137
+ provider: 'openai'
138
+ model?: string
139
+ apiKey?: string
140
+ }
141
+ // Search/RAG types
142
+ export declare interface SearchConfig {
143
+ embeddingProvider: 'openai' | 'ollama'
144
+ embeddingModel?: string
145
+ generationProvider?: 'anthropic' | 'openai' | 'ollama'
146
+ generationModel?: string
147
+ }
148
+ // MCP types
149
+ export declare interface MCPConfig {
150
+ servers: Array<{
151
+ name: string
152
+ command?: string
153
+ args?: string[]
154
+ url?: string
155
+ env?: Record<string, string>
156
+ }>
157
+ }
158
+ // AI module config (used by @stacksjs/config)
159
+ export declare interface AIConfig {
160
+ default?: string
161
+ models?: string[]
162
+ drivers?: {
163
+ anthropic?: AIDriverConfig & { anthropicVersion?: string }
164
+ openai?: AIDriverConfig & { embeddingModel?: string }
165
+ ollama?: AIDriverConfig & { host?: string; embeddingModel?: string }
166
+ }
167
+ image?: ImageGenerationConfig
168
+ search?: SearchConfig
169
+ mcp?: MCPConfig
170
+ }
171
+ /**
172
+ * Structured-output / JSON-mode response format. Modeled after
173
+ * OpenAI's `response_format` but the Anthropic driver maps it to
174
+ * the tools-as-json pattern internally (stacksjs/stacks#1878 A-1).
175
+ */
176
+ export type AIResponseFormat = | { type: 'text' }
177
+ | { type: 'json_object' }
178
+ | {
179
+ type: 'json_schema'
180
+ json_schema: {
181
+ name: string
182
+ schema: Record<string, unknown>
183
+ strict?: boolean
184
+ }
185
+ }
package/dist/types.js ADDED
File without changes
@@ -0,0 +1,16 @@
1
+ import type { InvokeModelCommandInput, InvokeModelCommandOutput, InvokeModelWithResponseStreamCommandInput, InvokeModelWithResponseStreamCommandOutput } from '@stacksjs/ts-cloud/aws';
2
+ export type { InvokeModelCommandInput, InvokeModelWithResponseStreamCommandInput };
3
+ /*
4
+ * Invoke Model
5
+ * @param {InvokeModelCommandInput} params
6
+ * @returns {Promise<InvokeModelCommandOutput>}
7
+ * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/BedrockRuntime.html#invokeModel-property
8
+ */
9
+ export declare function invokeModel(params: InvokeModelCommandInput): Promise<InvokeModelCommandOutput>;
10
+ /*
11
+ * Invoke Model With Response Stream
12
+ * @param {InvokeModelWithResponseStreamCommandInput} params
13
+ * @returns {Promise<InvokeModelWithResponseStreamCommandOutput>}
14
+ * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/BedrockRuntime.html#invokeModelWithResponseStream-property
15
+ */
16
+ export declare function invokeModelWithResponseStream(params: InvokeModelWithResponseStreamCommandInput): Promise<InvokeModelWithResponseStreamCommandOutput>;
@@ -0,0 +1,17 @@
1
+ import process from "node:process";
2
+ let _client = null;
3
+ async function getClient() {
4
+ if (_client)
5
+ return _client;
6
+ const mod = await import("@stacksjs/ts-cloud/aws");
7
+ if (!mod?.BedrockRuntimeClient)
8
+ throw Error("@stacksjs/ts-cloud/aws does not export BedrockRuntimeClient \u2014 rebuild ts-cloud or remove the AI dependency.");
9
+ _client = new mod.BedrockRuntimeClient(process.env.REGION || "us-east-1");
10
+ return _client;
11
+ }
12
+ export async function invokeModel(params) {
13
+ return (await getClient()).invokeModel(params);
14
+ }
15
+ export async function invokeModelWithResponseStream(params) {
16
+ return (await getClient()).invokeModelWithResponseStream(params);
17
+ }
@@ -0,0 +1,27 @@
1
+ import type { CreateModelCustomizationJobCommandInput, CreateModelCustomizationJobCommandOutput, GetModelCustomizationJobCommandInput, GetModelCustomizationJobCommandOutput, ListFoundationModelsCommandInput, ListFoundationModelsCommandOutput } from '@stacksjs/ts-cloud/aws';
2
+ export type {
3
+ CreateModelCustomizationJobCommandInput,
4
+ GetModelCustomizationJobCommandInput,
5
+ ListFoundationModelsCommandInput,
6
+ };
7
+ /*
8
+ * Create Model Customization Job
9
+ * @param {CreateModelCustomizationJobCommandInput} params
10
+ * @returns {Promise<CreateModelCustomizationJobCommandOutput>}
11
+ * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Bedrock.html#CreateModelCustomizationJob-property
12
+ */
13
+ export declare function createModelCustomizationJob(param: CreateModelCustomizationJobCommandInput): Promise<CreateModelCustomizationJobCommandOutput>;
14
+ /*
15
+ * Get Model Customization Job
16
+ * @param {GetModelCustomizationJobCommandInput} params
17
+ * @returns {Promise<GetModelCustomizationJobCommandOutput>}
18
+ * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Bedrock.html#getModelCustomizationJob-property
19
+ */
20
+ export declare function getModelCustomizationJob(params: GetModelCustomizationJobCommandInput): Promise<GetModelCustomizationJobCommandOutput>;
21
+ /*
22
+ * List Foundation Models
23
+ * @param {ListFoundationModelsCommandInput} params
24
+ * @returns {Promise<ListFoundationModelsCommandOutput>}
25
+ * @see https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Bedrock.html#listFoundationModels-property
26
+ */
27
+ export declare function listFoundationModels(params: ListFoundationModelsCommandInput): Promise<ListFoundationModelsCommandOutput>;
@@ -0,0 +1,20 @@
1
+ import process from "node:process";
2
+ let _client = null;
3
+ async function getClient() {
4
+ if (_client)
5
+ return _client;
6
+ const mod = await import("@stacksjs/ts-cloud/aws");
7
+ if (!mod?.BedrockClient)
8
+ throw Error("@stacksjs/ts-cloud/aws does not export BedrockClient \u2014 rebuild ts-cloud or remove the AI dependency.");
9
+ _client = new mod.BedrockClient(process.env.REGION || "us-east-1");
10
+ return _client;
11
+ }
12
+ export async function createModelCustomizationJob(param) {
13
+ return (await getClient()).createModelCustomizationJob(param);
14
+ }
15
+ export async function getModelCustomizationJob(params) {
16
+ return (await getClient()).getModelCustomizationJob(params);
17
+ }
18
+ export async function listFoundationModels(params) {
19
+ return (await getClient()).listFoundationModels(params);
20
+ }
@@ -0,0 +1 @@
1
+ export declare function requestModelAccess(): Promise<void>;
@@ -0,0 +1,21 @@
1
+ import { log } from "@stacksjs/cli";
2
+ import { ai } from "@stacksjs/config";
3
+ async function getBedrockClient() {
4
+ const mod = await import("@stacksjs/ts-cloud/aws");
5
+ if (!mod?.BedrockClient)
6
+ throw Error("@stacksjs/ts-cloud/aws does not export BedrockClient \u2014 rebuild ts-cloud or remove the AI dependency.");
7
+ return new mod.BedrockClient("us-east-1");
8
+ }
9
+ export async function requestModelAccess() {
10
+ const client = await getBedrockClient(), models = ai.models;
11
+ if (!models)
12
+ throw Error("No AI models found. Please set ./config/ai.ts values.");
13
+ for (const model of models)
14
+ try {
15
+ log.info(`Requesting access to model ${model}`);
16
+ const data = await client.requestModelAccess({ modelId: model });
17
+ log.info(`Response for model ${model}:`, data);
18
+ } catch (error) {
19
+ log.error(`Error requesting access to model ${model}:`, error);
20
+ }
21
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Fetch with automatic retry on 429 / 5xx. Returns the final
3
+ * `Response` — the caller checks `.ok` and parses the body as usual.
4
+ *
5
+ * Does NOT retry network-level errors (connection reset, DNS
6
+ * failure) — those throw synchronously and the caller's existing
7
+ * try/catch handles them.
8
+ */
9
+ export declare function fetchWithRetry(input: RequestInfo | URL, init?: RequestInit, config?: RetryConfig): Promise<Response>;
10
+ /**
11
+ * Retry helper for AI driver HTTP calls (stacksjs/stacks#1878 A-5).
12
+ *
13
+ * Background: OpenAI / Anthropic / etc. routinely return 429 (rate
14
+ * limit) and 5xx (capacity / overloaded) responses with a
15
+ * `Retry-After` header indicating when the caller should try again.
16
+ * The pre-fix AI drivers threw immediately on any non-2xx, surfacing
17
+ * transient capacity issues as hard user-facing failures.
18
+ *
19
+ * This helper wraps `fetch()` with:
20
+ * - Honor `Retry-After` (seconds or HTTP-date) for 429 + 503
21
+ * - Exponential backoff + jitter for other 5xx
22
+ * - Cap at `maxRetries` attempts (default 3)
23
+ * - Surface the final non-recoverable response to the caller
24
+ *
25
+ * No retry on 4xx other than 429 — those are caller bugs (bad API
26
+ * key, malformed request) that won't clear with more retries.
27
+ */
28
+ /**
29
+ * Configurable retry policy. Defaults are tuned for the typical
30
+ * "Anthropic returned 429, try again in 3s" case without being so
31
+ * aggressive that a permanent outage hangs the request loop for
32
+ * minutes.
33
+ */
34
+ export declare interface RetryConfig {
35
+ maxRetries?: number
36
+ baseDelayMs?: number
37
+ maxDelayMs?: number
38
+ }
@@ -0,0 +1,39 @@
1
+ const DEFAULT_RETRY = {
2
+ maxRetries: 3,
3
+ baseDelayMs: 500,
4
+ maxDelayMs: 30000
5
+ };
6
+ export async function fetchWithRetry(input, init, config = {}) {
7
+ const cfg = { ...DEFAULT_RETRY, ...config };
8
+ let lastResponse;
9
+ for (let attempt = 0;attempt <= cfg.maxRetries; attempt++) {
10
+ lastResponse = await fetch(input, init);
11
+ if (lastResponse.ok)
12
+ return lastResponse;
13
+ if (lastResponse.status < 500 && lastResponse.status !== 429)
14
+ return lastResponse;
15
+ if (attempt === cfg.maxRetries)
16
+ return lastResponse;
17
+ const backoff = parseRetryAfter(lastResponse.headers.get("Retry-After")) ?? exponentialBackoff(attempt, cfg), delay = Math.min(backoff, cfg.maxDelayMs);
18
+ try {
19
+ await lastResponse.text().catch(() => {});
20
+ } catch {}
21
+ await new Promise((resolve) => setTimeout(resolve, delay));
22
+ }
23
+ return lastResponse;
24
+ }
25
+ function parseRetryAfter(header) {
26
+ if (!header)
27
+ return null;
28
+ const seconds = Number.parseInt(header, 10);
29
+ if (Number.isFinite(seconds) && String(seconds) === header.trim())
30
+ return Math.max(0, seconds * 1000);
31
+ const dateMs = Date.parse(header);
32
+ if (Number.isFinite(dateMs))
33
+ return Math.max(0, dateMs - Date.now());
34
+ return null;
35
+ }
36
+ function exponentialBackoff(attempt, cfg) {
37
+ const cap = Math.min(cfg.baseDelayMs * 2 ** attempt, cfg.maxDelayMs);
38
+ return Math.random() * cap;
39
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Estimate the number of tokens in `text` for the given model.
3
+ * Heuristic only — for exact counts, use the provider's tokenizer.
4
+ *
5
+ * @example
6
+ * ```ts
7
+ * if (estimateTokens(prompt, 'gpt-4o') > 100_000) {
8
+ * throw new Error('prompt too long; consider chunking')
9
+ * }
10
+ * ```
11
+ */
12
+ export declare function estimateTokens(text: string, model?: string): number;
13
+ /**
14
+ * Estimate total tokens for a chat-completion request: sum of
15
+ * every message's content plus a fixed per-message overhead
16
+ * (matches the rough "+4 per message + 2 for the conversation"
17
+ * heuristic OpenAI's docs publish).
18
+ */
19
+ export declare function estimateMessageTokens(messages: Array<{ role: string, content: string | unknown }>, model?: string): number;
20
+ /**
21
+ * Inspect `text` for common prompt-injection patterns. Returns
22
+ * `{ ok, matched, cleaned }`. Apps decide what to do with the
23
+ * result — reject the request (`if (!result.ok) throw...`), pass
24
+ * the cleaned text on (`useText(result.cleaned)`), or just log
25
+ * for audit while letting the original through.
26
+ *
27
+ * **Limits:** this is heuristic. Adversarial inputs can paraphrase
28
+ * around any specific pattern. Use as a cheap first filter; for
29
+ * real defense, isolate the user input from the system prompt
30
+ * structurally (different roles, JSON-mode for the system layer)
31
+ * and guard the output side too.
32
+ *
33
+ * @example
34
+ * ```ts
35
+ * const check = sanitizePrompt(userInput)
36
+ * if (!check.ok) {
37
+ * log.warn('possible injection attempt', { patterns: check.matched })
38
+ * // option A: reject
39
+ * throw new HttpError(400, 'invalid input')
40
+ * // option B: pass cleaned
41
+ * await chat([{ role: 'user', content: check.cleaned }])
42
+ * }
43
+ * ```
44
+ */
45
+ export declare function sanitizePrompt(text: string): SanitizeResult;
46
+ export declare interface SanitizeResult {
47
+ ok: boolean
48
+ matched: string[]
49
+ cleaned: string
50
+ }
@@ -0,0 +1,59 @@
1
+ function charsPerToken(model) {
2
+ const m = model.toLowerCase();
3
+ if (m.startsWith("gpt-"))
4
+ return 3.5;
5
+ if (m.startsWith("claude"))
6
+ return 3.5;
7
+ return 3.5;
8
+ }
9
+ export function estimateTokens(text, model = "gpt-4o") {
10
+ if (!text)
11
+ return 0;
12
+ const ratio = charsPerToken(model);
13
+ return Math.max(1, Math.ceil(text.length / ratio));
14
+ }
15
+ export function estimateMessageTokens(messages, model = "gpt-4o") {
16
+ const PER_MESSAGE_OVERHEAD = 4;
17
+ let total = 2;
18
+ for (const msg of messages) {
19
+ total += PER_MESSAGE_OVERHEAD;
20
+ if (typeof msg.content === "string")
21
+ total += estimateTokens(msg.content, model);
22
+ else if (Array.isArray(msg.content)) {
23
+ for (const block of msg.content)
24
+ if (block.type === "text" && block.text)
25
+ total += estimateTokens(block.text, model);
26
+ else if (block.type === "image" || block.type === "image_url")
27
+ total += 100;
28
+ }
29
+ }
30
+ return total;
31
+ }
32
+ const INJECTION_PATTERNS = [
33
+ /\bignore\s+(?:all\s+)?(?:previous|prior|above)\s+instructions?\b/i,
34
+ /\bdisregard\s+(?:all\s+)?(?:previous|prior|above)\b/i,
35
+ /\bforget\s+(?:everything|all)\s+(?:you|i)\b/i,
36
+ /\byou\s+are\s+now\s+a\s+\w+/i,
37
+ /\bnew\s+instructions?:\s*/i,
38
+ /\bsystem\s*[:>]\s*/i,
39
+ /\b(?:reveal|show|print|output|display)\s+(?:your|the)\s+(?:system\s+)?prompt\b/i,
40
+ /<\s*\/?\s*system\s*>/i,
41
+ /\[INST\]|\[\/INST\]/i,
42
+ /^\s*###\s+(?:instruction|system)/im
43
+ ];
44
+ export function sanitizePrompt(text) {
45
+ if (!text)
46
+ return { ok: !0, matched: [], cleaned: text };
47
+ const matched = [];
48
+ let cleaned = text;
49
+ for (const pattern of INJECTION_PATTERNS)
50
+ if (pattern.test(cleaned)) {
51
+ matched.push(pattern.toString());
52
+ cleaned = cleaned.replace(pattern, "[redacted]");
53
+ }
54
+ return {
55
+ ok: matched.length === 0,
56
+ matched,
57
+ cleaned
58
+ };
59
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Register a usage reporter. Returns an `unregister` callback for
3
+ * apps that want to swap reporters at runtime (test setup/teardown,
4
+ * tenant isolation, etc.).
5
+ */
6
+ export declare function onUsage(reporter: UsageReporter): () => void;
7
+ /**
8
+ * Drop every registered reporter. Useful for tests.
9
+ */
10
+ export declare function clearUsageReporters(): void;
11
+ /**
12
+ * Emit a usage record to every registered reporter. Called by
13
+ * driver completion paths after the response lands. Reporter
14
+ * errors are caught + logged so a misbehaving sink doesn't
15
+ * propagate up to the caller.
16
+ */
17
+ export declare function recordUsage(record: UsageRecord): void;
18
+ /**
19
+ * Snapshot the currently-registered reporters. Useful for tests
20
+ * to assert behavior without exposing the internal array.
21
+ */
22
+ export declare function listUsageReporters(): readonly UsageReporter[];
23
+ /**
24
+ * AI usage tracking (stacksjs/stacks#1878 A-6).
25
+ *
26
+ * Background: `AIResult.usage` returns token counts per-call but
27
+ * nothing aggregates them. Apps that want "this user has spent
28
+ * $X this month" build the aggregation themselves — wiring a
29
+ * listener on every model invocation, persisting the running
30
+ * total, etc.
31
+ *
32
+ * This module ships a singleton recorder that drivers emit to on
33
+ * each completion. Apps install one or more `UsageReporter`
34
+ * functions that get called with `{ provider, model, prompt_tokens,
35
+ * completion_tokens, timestamp, durationMs }` and decide what to
36
+ * do (store to DB, push to Datadog, etc.). Default behavior with
37
+ * no reporter is a no-op — the framework doesn't impose a sink.
38
+ */
39
+ export declare interface UsageRecord {
40
+ provider: string
41
+ model: string
42
+ promptTokens: number
43
+ completionTokens: number
44
+ totalTokens: number
45
+ durationMs: number
46
+ timestamp: number
47
+ metadata?: Record<string, unknown>
48
+ }
49
+ /**
50
+ * A reporter is called once per recorded completion. Multiple
51
+ * reporters can be installed simultaneously; they fire in
52
+ * registration order. Reporters MUST NOT throw — errors are
53
+ * caught and logged but otherwise ignored so a flaky metrics
54
+ * sink doesn't break the user's completion call.
55
+ */
56
+ export type UsageReporter = (record: UsageRecord) => void | Promise<void>;
@@ -0,0 +1,27 @@
1
+ const reporters = [];
2
+ export function onUsage(reporter) {
3
+ reporters.push(reporter);
4
+ return () => {
5
+ const idx = reporters.indexOf(reporter);
6
+ if (idx >= 0)
7
+ reporters.splice(idx, 1);
8
+ };
9
+ }
10
+ export function clearUsageReporters() {
11
+ reporters.length = 0;
12
+ }
13
+ export function recordUsage(record) {
14
+ for (const reporter of reporters)
15
+ try {
16
+ const result = reporter(record);
17
+ if (result && typeof result.then === "function")
18
+ result.catch((err) => {
19
+ console.error("[ai/usage] reporter rejected:", err);
20
+ });
21
+ } catch (err) {
22
+ console.error("[ai/usage] reporter threw:", err);
23
+ }
24
+ }
25
+ export function listUsageReporters() {
26
+ return reporters;
27
+ }
@@ -0,0 +1,22 @@
1
+ import type { AIMessage, AIMessageContent } from '../types';
2
+ /**
3
+ * Normalize an entire `messages` array for the requested provider.
4
+ * Messages whose `content` is a plain string pass through unchanged.
5
+ * Messages with a content array get each block translated.
6
+ */
7
+ export declare function normalizeMessagesForProvider(messages: AIMessage[], provider: 'openai' | 'anthropic'): AIMessage[];
8
+ /**
9
+ * Convenience: convert a single command + optional image inputs into
10
+ * an `AIMessage` content array suitable for `chat()` calls. Used by
11
+ * the higher-level `text()` / `chat()` helpers when a user passes
12
+ * `{ command, images }` together.
13
+ *
14
+ * @example
15
+ * ```ts
16
+ * const content = buildMessageWithImages('What is in this image?', [
17
+ * { url: 'https://example.com/cat.jpg' },
18
+ * ])
19
+ * await chat([{ role: 'user', content }])
20
+ * ```
21
+ */
22
+ export declare function buildMessageWithImages(command: string, images: Array<{ url?: string, dataBase64?: string, mediaType?: string, detail?: 'auto' | 'low' | 'high' }>): AIMessageContent[];