@tanstack/ai 0.26.1 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/activities/chat/index.d.ts +0 -6
- package/dist/esm/activities/chat/index.js +2 -17
- package/dist/esm/activities/chat/index.js.map +1 -1
- package/dist/esm/activities/chat/middleware/types.d.ts +1 -4
- package/dist/esm/activities/summarize/chat-stream-summarize.js +62 -3
- package/dist/esm/activities/summarize/chat-stream-summarize.js.map +1 -1
- package/dist/esm/logger/internal-logger.d.ts +8 -0
- package/dist/esm/logger/internal-logger.js +15 -0
- package/dist/esm/logger/internal-logger.js.map +1 -1
- package/dist/esm/middlewares/otel.js +30 -6
- package/dist/esm/middlewares/otel.js.map +1 -1
- package/dist/esm/types.d.ts +0 -35
- package/dist/esm/utilities/sampling-keys.d.ts +20 -0
- package/dist/esm/utilities/sampling-keys.js +20 -0
- package/dist/esm/utilities/sampling-keys.js.map +1 -0
- package/package.json +2 -2
- package/skills/ai-core/adapter-configuration/SKILL.md +67 -6
- package/skills/ai-core/adapter-configuration/references/anthropic-adapter.md +6 -3
- package/skills/ai-core/adapter-configuration/references/gemini-adapter.md +3 -0
- package/skills/ai-core/adapter-configuration/references/ollama-adapter.md +10 -1
- package/skills/ai-core/adapter-configuration/references/openai-adapter.md +4 -0
- package/skills/ai-core/chat-experience/SKILL.md +24 -7
- package/skills/ai-core/middleware/SKILL.md +11 -0
- package/src/activities/chat/index.ts +2 -23
- package/src/activities/chat/middleware/types.ts +1 -4
- package/src/activities/summarize/chat-stream-summarize.ts +162 -3
- package/src/logger/internal-logger.ts +18 -0
- package/src/middlewares/otel.ts +48 -6
- package/src/types.ts +0 -35
- package/src/utilities/sampling-keys.ts +28 -0
|
@@ -65,7 +65,7 @@ export interface ChatMiddlewareContext<TContext = unknown> {
|
|
|
65
65
|
systemPrompts: Array<SystemPrompt>;
|
|
66
66
|
/** Names of configured tools, if any */
|
|
67
67
|
toolNames?: Array<string>;
|
|
68
|
-
/** Flattened generation options (
|
|
68
|
+
/** Flattened generation options (metadata) */
|
|
69
69
|
options?: Record<string, unknown> | undefined;
|
|
70
70
|
/** Provider-specific model options */
|
|
71
71
|
modelOptions?: Record<string, unknown> | undefined;
|
|
@@ -91,9 +91,6 @@ export interface ChatMiddlewareConfig {
|
|
|
91
91
|
messages: Array<ModelMessage>;
|
|
92
92
|
systemPrompts: Array<SystemPrompt>;
|
|
93
93
|
tools: Array<Tool>;
|
|
94
|
-
temperature?: number;
|
|
95
|
-
topP?: number;
|
|
96
|
-
maxTokens?: number;
|
|
97
94
|
metadata?: Record<string, unknown> | undefined;
|
|
98
95
|
modelOptions?: Record<string, unknown> | undefined;
|
|
99
96
|
}
|
|
@@ -1,6 +1,53 @@
|
|
|
1
1
|
import { EventType } from "@ag-ui/core";
|
|
2
2
|
import { toRunErrorPayload } from "../error-payload.js";
|
|
3
|
+
import { MAX_TOKENS_KEYS } from "../../utilities/sampling-keys.js";
|
|
3
4
|
import { BaseSummarizeAdapter } from "./adapter.js";
|
|
5
|
+
const MAX_TOKENS_KEY_BY_ADAPTER = {
|
|
6
|
+
openai: "max_output_tokens",
|
|
7
|
+
anthropic: "max_tokens",
|
|
8
|
+
grok: "max_tokens",
|
|
9
|
+
groq: "max_completion_tokens",
|
|
10
|
+
gemini: "maxOutputTokens",
|
|
11
|
+
openrouter: "maxCompletionTokens"
|
|
12
|
+
};
|
|
13
|
+
const KNOWN_MAX_TOKENS_KEYS = MAX_TOKENS_KEYS;
|
|
14
|
+
function isKnownMaxTokensAdapter(adapterName) {
|
|
15
|
+
return adapterName === "ollama" || MAX_TOKENS_KEY_BY_ADAPTER[adapterName] !== void 0;
|
|
16
|
+
}
|
|
17
|
+
function applyDefaultTemperature(adapterName, temperature, modelOptions) {
|
|
18
|
+
const merged = { ...modelOptions };
|
|
19
|
+
if (adapterName === "ollama") {
|
|
20
|
+
const existing = merged.options && typeof merged.options === "object" ? merged.options : void 0;
|
|
21
|
+
if (existing && "temperature" in existing) return merged;
|
|
22
|
+
merged.options = { temperature, ...existing };
|
|
23
|
+
return merged;
|
|
24
|
+
}
|
|
25
|
+
if ("temperature" in merged) return merged;
|
|
26
|
+
merged.temperature = temperature;
|
|
27
|
+
return merged;
|
|
28
|
+
}
|
|
29
|
+
function applyMaxLength(adapterName, maxLength, modelOptions) {
|
|
30
|
+
const merged = { ...modelOptions };
|
|
31
|
+
if (adapterName === "ollama") {
|
|
32
|
+
const callerSetFlatLimit = KNOWN_MAX_TOKENS_KEYS.some(
|
|
33
|
+
(k) => typeof merged[k] === "number"
|
|
34
|
+
);
|
|
35
|
+
const existing = merged.options && typeof merged.options === "object" ? merged.options : void 0;
|
|
36
|
+
if (callerSetFlatLimit || existing && typeof existing.num_predict === "number") {
|
|
37
|
+
return merged;
|
|
38
|
+
}
|
|
39
|
+
merged.options = { num_predict: maxLength, ...existing };
|
|
40
|
+
return merged;
|
|
41
|
+
}
|
|
42
|
+
const key = MAX_TOKENS_KEY_BY_ADAPTER[adapterName];
|
|
43
|
+
if (key === void 0) return merged;
|
|
44
|
+
const callerSetLimit = KNOWN_MAX_TOKENS_KEYS.some(
|
|
45
|
+
(k) => typeof merged[k] === "number"
|
|
46
|
+
);
|
|
47
|
+
if (callerSetLimit) return merged;
|
|
48
|
+
merged[key] = maxLength;
|
|
49
|
+
return merged;
|
|
50
|
+
}
|
|
4
51
|
class ChatStreamSummarizeAdapter extends BaseSummarizeAdapter {
|
|
5
52
|
name;
|
|
6
53
|
textAdapter;
|
|
@@ -110,13 +157,25 @@ class ChatStreamSummarizeAdapter extends BaseSummarizeAdapter {
|
|
|
110
157
|
* still reach the wire layer.
|
|
111
158
|
*/
|
|
112
159
|
buildTextOptions(options, systemPrompt) {
|
|
160
|
+
let working = {
|
|
161
|
+
...options.modelOptions
|
|
162
|
+
};
|
|
163
|
+
working = applyDefaultTemperature(this.name, 0.3, working);
|
|
164
|
+
if (options.maxLength !== void 0) {
|
|
165
|
+
if (!isKnownMaxTokensAdapter(this.name)) {
|
|
166
|
+
options.logger.warn(
|
|
167
|
+
`summarize: maxLength=${options.maxLength} could not be mapped to a provider token key for adapter name "${this.name}" — it was dropped from modelOptions (the prompt still asks the model to stay under it). Construct ChatStreamSummarizeAdapter with a recognised provider name to forward the cap.`,
|
|
168
|
+
{ provider: this.name }
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
working = applyMaxLength(this.name, options.maxLength, working);
|
|
172
|
+
}
|
|
173
|
+
const modelOptions = working;
|
|
113
174
|
return {
|
|
114
175
|
model: options.model,
|
|
115
176
|
messages: [{ role: "user", content: options.text }],
|
|
116
177
|
systemPrompts: [systemPrompt],
|
|
117
|
-
|
|
118
|
-
temperature: 0.3,
|
|
119
|
-
modelOptions: options.modelOptions,
|
|
178
|
+
modelOptions,
|
|
120
179
|
logger: options.logger
|
|
121
180
|
};
|
|
122
181
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chat-stream-summarize.js","sources":["../../../../src/activities/summarize/chat-stream-summarize.ts"],"sourcesContent":["import { EventType } from '@ag-ui/core'\nimport { toRunErrorPayload } from '../error-payload'\nimport { BaseSummarizeAdapter } from './adapter'\nimport type {\n StreamChunk,\n SummarizationOptions,\n SummarizationResult,\n TextOptions,\n} from '../../types'\n\n/**\n * Minimal contract for a text adapter that supports `chatStream`. Lets\n * `ChatStreamSummarizeAdapter` work with any text adapter without coupling\n * to a specific implementation.\n *\n * The provider-options shape is intentionally `any` here — the wrapper only\n * forwards `modelOptions` straight through, so a text adapter with a richer\n * per-model options type (e.g. `ResolveProviderOptions<TModel>`) is still\n * acceptable. Summarize-level type safety is enforced via\n * `SummarizationOptions<TProviderOptions>` on the wrapper itself.\n */\nexport interface ChatStreamCapable {\n chatStream: (options: TextOptions<any>) => AsyncIterable<StreamChunk>\n}\n\n/**\n * Extract the per-model `modelOptions` type a text adapter accepts. Used by\n * provider summarize factories so their `modelOptions` IntelliSense matches\n * what the underlying text adapter actually understands.\n */\nexport type InferTextProviderOptions<TAdapter> = TAdapter extends {\n '~types': { providerOptions: infer P }\n}\n ? P extends object\n ? P\n : object\n : object\n\n/**\n * Summarize adapter that wraps any `ChatStreamCapable` text adapter and\n * prompts it for summarization. Not tied to any wire format.\n */\nexport class ChatStreamSummarizeAdapter<\n TModel extends string,\n TProviderOptions extends object = Record<string, unknown>,\n> extends BaseSummarizeAdapter<TModel, TProviderOptions> {\n readonly name: string\n\n private readonly textAdapter: ChatStreamCapable\n\n constructor(\n textAdapter: ChatStreamCapable,\n model: TModel,\n name: string = 'chat-stream-summarize',\n ) {\n super({}, model)\n this.name = name\n this.textAdapter = textAdapter\n }\n\n async summarize(\n options: SummarizationOptions<TProviderOptions>,\n ): Promise<SummarizationResult> {\n const systemPrompt = this.buildSummarizationPrompt(options)\n\n let summary = ''\n const id = this.generateId()\n let model = options.model\n let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 }\n\n options.logger.request(\n `activity=summarize provider=${this.name} model=${options.model} text-length=${options.text.length} maxLength=${options.maxLength ?? 'unset'}`,\n { provider: this.name, model: options.model },\n )\n\n try {\n for await (const chunk of this.textAdapter.chatStream(\n this.buildTextOptions(options, systemPrompt),\n )) {\n if (chunk.type === 'TEXT_MESSAGE_CONTENT') {\n if (chunk.content) {\n summary = chunk.content\n } else if (chunk.delta) {\n // Append delta only when present — a content-less chunk with no\n // delta would otherwise concat literal `'undefined'`.\n summary += chunk.delta\n }\n model = chunk.model || model\n }\n if (chunk.type === 'RUN_FINISHED') {\n if (chunk.usage) {\n usage = chunk.usage\n }\n }\n // Surface failures: the underlying chatStream emits RUN_ERROR instead\n // of throwing, so without this branch summarize() would return an\n // empty summary and pretend a failed run succeeded.\n if (chunk.type === 'RUN_ERROR') {\n const message =\n (chunk.error && typeof chunk.error.message === 'string'\n ? chunk.error.message\n : null) ?? 'Summarization failed'\n const code =\n chunk.error && typeof chunk.error.code === 'string'\n ? chunk.error.code\n : undefined\n const err = new Error(message)\n if (code) {\n ;(err as Error & { code?: string }).code = code\n }\n throw err\n }\n }\n } catch (error: unknown) {\n // Narrow before logging: raw SDK errors can carry request metadata\n // (including auth headers) which we must never surface to user loggers.\n options.logger.errors(`${this.name}.summarize fatal`, {\n error: toRunErrorPayload(error, `${this.name}.summarize failed`),\n source: `${this.name}.summarize`,\n })\n throw error\n }\n\n return { id, model, summary, usage }\n }\n\n override async *summarizeStream(\n options: SummarizationOptions<TProviderOptions>,\n ): AsyncIterable<StreamChunk> {\n const systemPrompt = this.buildSummarizationPrompt(options)\n\n options.logger.request(\n `activity=summarizeStream provider=${this.name} model=${options.model} text-length=${options.text.length} maxLength=${options.maxLength ?? 'unset'}`,\n { provider: this.name, model: options.model },\n )\n\n const id = this.generateId()\n let summary = ''\n let model = options.model\n let usage: SummarizationResult['usage'] = {\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n }\n\n try {\n for await (const chunk of this.textAdapter.chatStream(\n this.buildTextOptions(options, systemPrompt),\n )) {\n // Accumulate the same way `summarize()` does so consumers see deltas\n // AND the terminal `generation:result` event below carries the same\n // final summary that non-streaming returns.\n if (chunk.type === 'TEXT_MESSAGE_CONTENT') {\n if (chunk.content) {\n summary = chunk.content\n } else if (chunk.delta) {\n summary += chunk.delta\n }\n if (chunk.model) model = chunk.model\n }\n\n // Emit the GenerationClient-shaped result event just before the\n // terminal RUN_FINISHED so subscribers (useSummarize) populate\n // `result` before flipping `status` to success.\n if (chunk.type === 'RUN_FINISHED') {\n if (chunk.usage) usage = chunk.usage\n if (chunk.model) model = chunk.model\n yield {\n type: EventType.CUSTOM,\n name: 'generation:result',\n value: { id, model, summary, usage } satisfies SummarizationResult,\n model,\n timestamp: Date.now(),\n }\n }\n\n yield chunk\n }\n } catch (error: unknown) {\n options.logger.errors(`${this.name}.summarizeStream fatal`, {\n error: toRunErrorPayload(error, `${this.name}.summarizeStream failed`),\n source: `${this.name}.summarizeStream`,\n })\n throw error\n }\n }\n\n /**\n * Build the TextOptions passed to the underlying chatStream. Provider\n * `modelOptions` from the summarize call are forwarded as-is so knobs like\n * Anthropic cache headers, Gemini safety settings, or Ollama tuning params\n * still reach the wire layer.\n */\n protected buildTextOptions(\n options: SummarizationOptions<TProviderOptions>,\n systemPrompt: string,\n ): TextOptions<TProviderOptions> {\n return {\n model: options.model,\n messages: [{ role: 'user', content: options.text }],\n systemPrompts: [systemPrompt],\n maxTokens: options.maxLength,\n temperature: 0.3,\n modelOptions: options.modelOptions,\n logger: options.logger,\n }\n }\n\n protected buildSummarizationPrompt(\n options: SummarizationOptions<TProviderOptions>,\n ): string {\n let prompt = 'You are a professional summarizer. '\n\n switch (options.style) {\n case 'bullet-points':\n prompt += 'Provide a summary in bullet point format. '\n break\n case 'paragraph':\n prompt += 'Provide a summary in paragraph format. '\n break\n case 'concise':\n prompt += 'Provide a very concise summary in 1-2 sentences. '\n break\n case undefined:\n prompt += 'Provide a clear and concise summary. '\n break\n default:\n prompt += 'Provide a clear and concise summary. '\n }\n\n if (options.focus && options.focus.length > 0) {\n prompt += `Focus on the following aspects: ${options.focus.join(', ')}. `\n }\n\n if (options.maxLength) {\n prompt += `Keep the summary under ${options.maxLength} tokens. `\n }\n\n return prompt\n }\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"chat-stream-summarize.js","sources":["../../../../src/activities/summarize/chat-stream-summarize.ts"],"sourcesContent":["import { EventType } from '@ag-ui/core'\nimport { toRunErrorPayload } from '../error-payload'\nimport { MAX_TOKENS_KEYS } from '../../utilities/sampling-keys'\nimport { BaseSummarizeAdapter } from './adapter'\nimport type {\n StreamChunk,\n SummarizationOptions,\n SummarizationResult,\n TextOptions,\n} from '../../types'\n\n/**\n * Minimal contract for a text adapter that supports `chatStream`. Lets\n * `ChatStreamSummarizeAdapter` work with any text adapter without coupling\n * to a specific implementation.\n *\n * The provider-options shape is intentionally `any` here — the wrapper only\n * forwards `modelOptions` straight through, so a text adapter with a richer\n * per-model options type (e.g. `ResolveProviderOptions<TModel>`) is still\n * acceptable. Summarize-level type safety is enforced via\n * `SummarizationOptions<TProviderOptions>` on the wrapper itself.\n */\nexport interface ChatStreamCapable {\n chatStream: (options: TextOptions<any>) => AsyncIterable<StreamChunk>\n}\n\n/**\n * Provider-native max-output-tokens key per summarize-adapter `name`. summarize\n * is provider-agnostic and forwards `modelOptions` opaquely to the wrapped text\n * adapter, so `maxLength` must be written under the exact key the underlying\n * provider reads — no adapter reads a generic `maxTokens`. Ollama is the one\n * exception: it nests sampling under `options`, so it has no entry here and is\n * handled as a special nested case in `applyMaxLength`/`applyDefaultTemperature`.\n *\n * Keep in sync with each adapter's wire mapping:\n * - OpenAI (Responses): `max_output_tokens`\n * - Anthropic / Grok: `max_tokens`\n * - Groq: `max_completion_tokens`\n * - Gemini: `maxOutputTokens`\n * - OpenRouter: `maxCompletionTokens`\n * - Ollama: nested `options.num_predict` (no entry — see `applyMaxLength`)\n */\nconst MAX_TOKENS_KEY_BY_ADAPTER: Record<string, string> = {\n openai: 'max_output_tokens',\n anthropic: 'max_tokens',\n grok: 'max_tokens',\n groq: 'max_completion_tokens',\n gemini: 'maxOutputTokens',\n openrouter: 'maxCompletionTokens',\n}\n\n/**\n * Every flat key any supported provider uses to cap output tokens (plus the\n * generic `maxTokens` spelling no adapter reads). Used to detect a\n * caller-supplied token limit so the summarize default never overrides an\n * explicit caller value. Shared with the OTel middleware via\n * `MAX_TOKENS_KEYS` so the two spelling sets cannot drift.\n */\nconst KNOWN_MAX_TOKENS_KEYS = MAX_TOKENS_KEYS\n\n/**\n * Whether `applyMaxLength` knows how to place a token limit for this adapter\n * `name` (either the nested Ollama shape or a flat provider-native key).\n * Used to surface a warning when `maxLength` would otherwise be silently\n * dropped for an unrecognised adapter name.\n */\nfunction isKnownMaxTokensAdapter(adapterName: string): boolean {\n return (\n adapterName === 'ollama' ||\n MAX_TOKENS_KEY_BY_ADAPTER[adapterName] !== undefined\n )\n}\n\n/**\n * Apply the low-temperature summarize default to a working copy of the\n * caller's `modelOptions`, placed where the wrapped provider actually reads\n * it (nested under `options` for Ollama, flat otherwise). The caller always\n * wins: if they already set `temperature` in that location, it is untouched.\n */\nfunction applyDefaultTemperature(\n adapterName: string,\n temperature: number,\n modelOptions: Record<string, unknown>,\n): Record<string, unknown> {\n const merged: Record<string, unknown> = { ...modelOptions }\n\n if (adapterName === 'ollama') {\n const existing =\n merged.options && typeof merged.options === 'object'\n ? (merged.options as Record<string, unknown>)\n : undefined\n if (existing && 'temperature' in existing) return merged\n merged.options = { temperature, ...existing }\n return merged\n }\n\n if ('temperature' in merged) return merged\n merged.temperature = temperature\n return merged\n}\n\n/**\n * Resolve `maxLength` to the provider-native max-output-tokens key for the\n * given summarize-adapter `name` (this wrapper's OWN `name`, not the wrapped\n * text adapter's) and merge it into a working copy of the caller's\n * `modelOptions`. The caller always wins: if they already set any recognised\n * token-limit key (flat or, for Ollama, nested `options.num_predict`), the\n * default is left untouched. Unknown/unrecognised adapter names fall back to\n * NOT setting a token key (the prompt hint still asks the model to stay under\n * `maxLength`) rather than writing a dead key no provider reads.\n *\n * Caveat (intentional): \"caller wins\" keys off ANY recognised spelling in\n * `KNOWN_MAX_TOKENS_KEYS`, but only the adapter's native key is read on the\n * wire. So a caller who sets a NON-native spelling for this provider — e.g.\n * `maxTokens`, or Anthropic's `max_tokens` against an OpenAI adapter — suppresses\n * the summarize default WITHOUT getting their own value applied either: neither\n * cap reaches the wire. This favours never clobbering a migration leftover over\n * guaranteeing a cap; the prompt-level hint still asks the model to stay under\n * `maxLength`. Rename the key to the provider-native spelling to forward it.\n */\nfunction applyMaxLength(\n adapterName: string,\n maxLength: number,\n modelOptions: Record<string, unknown>,\n): Record<string, unknown> {\n const merged: Record<string, unknown> = { ...modelOptions }\n\n if (adapterName === 'ollama') {\n // Honor a caller-set limit in either shape: a recognised flat key (e.g.\n // left over from a migration) or the nested `options.num_predict`.\n const callerSetFlatLimit = KNOWN_MAX_TOKENS_KEYS.some(\n (k) => typeof merged[k] === 'number',\n )\n const existing =\n merged.options && typeof merged.options === 'object'\n ? (merged.options as Record<string, unknown>)\n : undefined\n if (\n callerSetFlatLimit ||\n (existing && typeof existing.num_predict === 'number')\n ) {\n return merged\n }\n merged.options = { num_predict: maxLength, ...existing }\n return merged\n }\n\n const key = MAX_TOKENS_KEY_BY_ADAPTER[adapterName]\n if (key === undefined) return merged\n\n const callerSetLimit = KNOWN_MAX_TOKENS_KEYS.some(\n (k) => typeof merged[k] === 'number',\n )\n if (callerSetLimit) return merged\n\n merged[key] = maxLength\n return merged\n}\n\n/**\n * Extract the per-model `modelOptions` type a text adapter accepts. Used by\n * provider summarize factories so their `modelOptions` IntelliSense matches\n * what the underlying text adapter actually understands.\n */\nexport type InferTextProviderOptions<TAdapter> = TAdapter extends {\n '~types': { providerOptions: infer P }\n}\n ? P extends object\n ? P\n : object\n : object\n\n/**\n * Summarize adapter that wraps any `ChatStreamCapable` text adapter and\n * prompts it for summarization. Not tied to any wire format.\n */\nexport class ChatStreamSummarizeAdapter<\n TModel extends string,\n TProviderOptions extends object = Record<string, unknown>,\n> extends BaseSummarizeAdapter<TModel, TProviderOptions> {\n readonly name: string\n\n private readonly textAdapter: ChatStreamCapable\n\n constructor(\n textAdapter: ChatStreamCapable,\n model: TModel,\n name: string = 'chat-stream-summarize',\n ) {\n super({}, model)\n this.name = name\n this.textAdapter = textAdapter\n }\n\n async summarize(\n options: SummarizationOptions<TProviderOptions>,\n ): Promise<SummarizationResult> {\n const systemPrompt = this.buildSummarizationPrompt(options)\n\n let summary = ''\n const id = this.generateId()\n let model = options.model\n let usage = { promptTokens: 0, completionTokens: 0, totalTokens: 0 }\n\n options.logger.request(\n `activity=summarize provider=${this.name} model=${options.model} text-length=${options.text.length} maxLength=${options.maxLength ?? 'unset'}`,\n { provider: this.name, model: options.model },\n )\n\n try {\n for await (const chunk of this.textAdapter.chatStream(\n this.buildTextOptions(options, systemPrompt),\n )) {\n if (chunk.type === 'TEXT_MESSAGE_CONTENT') {\n if (chunk.content) {\n summary = chunk.content\n } else if (chunk.delta) {\n // Append delta only when present — a content-less chunk with no\n // delta would otherwise concat literal `'undefined'`.\n summary += chunk.delta\n }\n model = chunk.model || model\n }\n if (chunk.type === 'RUN_FINISHED') {\n if (chunk.usage) {\n usage = chunk.usage\n }\n }\n // Surface failures: the underlying chatStream emits RUN_ERROR instead\n // of throwing, so without this branch summarize() would return an\n // empty summary and pretend a failed run succeeded.\n if (chunk.type === 'RUN_ERROR') {\n const message =\n (chunk.error && typeof chunk.error.message === 'string'\n ? chunk.error.message\n : null) ?? 'Summarization failed'\n const code =\n chunk.error && typeof chunk.error.code === 'string'\n ? chunk.error.code\n : undefined\n const err = new Error(message)\n if (code) {\n ;(err as Error & { code?: string }).code = code\n }\n throw err\n }\n }\n } catch (error: unknown) {\n // Narrow before logging: raw SDK errors can carry request metadata\n // (including auth headers) which we must never surface to user loggers.\n options.logger.errors(`${this.name}.summarize fatal`, {\n error: toRunErrorPayload(error, `${this.name}.summarize failed`),\n source: `${this.name}.summarize`,\n })\n throw error\n }\n\n return { id, model, summary, usage }\n }\n\n override async *summarizeStream(\n options: SummarizationOptions<TProviderOptions>,\n ): AsyncIterable<StreamChunk> {\n const systemPrompt = this.buildSummarizationPrompt(options)\n\n options.logger.request(\n `activity=summarizeStream provider=${this.name} model=${options.model} text-length=${options.text.length} maxLength=${options.maxLength ?? 'unset'}`,\n { provider: this.name, model: options.model },\n )\n\n const id = this.generateId()\n let summary = ''\n let model = options.model\n let usage: SummarizationResult['usage'] = {\n promptTokens: 0,\n completionTokens: 0,\n totalTokens: 0,\n }\n\n try {\n for await (const chunk of this.textAdapter.chatStream(\n this.buildTextOptions(options, systemPrompt),\n )) {\n // Accumulate the same way `summarize()` does so consumers see deltas\n // AND the terminal `generation:result` event below carries the same\n // final summary that non-streaming returns.\n if (chunk.type === 'TEXT_MESSAGE_CONTENT') {\n if (chunk.content) {\n summary = chunk.content\n } else if (chunk.delta) {\n summary += chunk.delta\n }\n if (chunk.model) model = chunk.model\n }\n\n // Emit the GenerationClient-shaped result event just before the\n // terminal RUN_FINISHED so subscribers (useSummarize) populate\n // `result` before flipping `status` to success.\n if (chunk.type === 'RUN_FINISHED') {\n if (chunk.usage) usage = chunk.usage\n if (chunk.model) model = chunk.model\n yield {\n type: EventType.CUSTOM,\n name: 'generation:result',\n value: { id, model, summary, usage } satisfies SummarizationResult,\n model,\n timestamp: Date.now(),\n }\n }\n\n yield chunk\n }\n } catch (error: unknown) {\n options.logger.errors(`${this.name}.summarizeStream fatal`, {\n error: toRunErrorPayload(error, `${this.name}.summarizeStream failed`),\n source: `${this.name}.summarizeStream`,\n })\n throw error\n }\n }\n\n /**\n * Build the TextOptions passed to the underlying chatStream. Provider\n * `modelOptions` from the summarize call are forwarded as-is so knobs like\n * Anthropic cache headers, Gemini safety settings, or Ollama tuning params\n * still reach the wire layer.\n */\n protected buildTextOptions(\n options: SummarizationOptions<TProviderOptions>,\n systemPrompt: string,\n ): TextOptions<TProviderOptions> {\n // Sampling knobs now live in provider-native `modelOptions`. Apply the\n // low-temperature default where the wrapped provider actually reads it\n // (nested under `options` for Ollama, flat otherwise) so callers can still\n // override it. Resolving the placement from this summarize adapter's OWN\n // `name` keeps the default off the wire correctly per provider — a flat\n // `temperature` would be silently dropped by Ollama while still showing up\n // in OTel.\n let working: Record<string, unknown> = {\n ...(options.modelOptions as Record<string, unknown> | undefined),\n }\n working = applyDefaultTemperature(this.name, 0.3, working)\n // `maxLength` must reach the wire under the provider-native token key (it\n // differs per provider, and no adapter reads a generic `maxTokens`).\n // Resolve it from this summarize adapter's `name` (the constructor arg,\n // not the wrapped text adapter's name), never overriding a caller-supplied\n // token limit.\n if (options.maxLength !== undefined) {\n if (!isKnownMaxTokensAdapter(this.name)) {\n options.logger.warn(\n `summarize: maxLength=${options.maxLength} could not be mapped to a provider token key for adapter name \"${this.name}\" — it was dropped from modelOptions (the prompt still asks the model to stay under it). Construct ChatStreamSummarizeAdapter with a recognised provider name to forward the cap.`,\n { provider: this.name },\n )\n }\n working = applyMaxLength(this.name, options.maxLength, working)\n }\n const modelOptions = working as TProviderOptions\n\n return {\n model: options.model,\n messages: [{ role: 'user', content: options.text }],\n systemPrompts: [systemPrompt],\n modelOptions,\n logger: options.logger,\n }\n }\n\n protected buildSummarizationPrompt(\n options: SummarizationOptions<TProviderOptions>,\n ): string {\n let prompt = 'You are a professional summarizer. '\n\n switch (options.style) {\n case 'bullet-points':\n prompt += 'Provide a summary in bullet point format. '\n break\n case 'paragraph':\n prompt += 'Provide a summary in paragraph format. '\n break\n case 'concise':\n prompt += 'Provide a very concise summary in 1-2 sentences. '\n break\n case undefined:\n prompt += 'Provide a clear and concise summary. '\n break\n default:\n prompt += 'Provide a clear and concise summary. '\n }\n\n if (options.focus && options.focus.length > 0) {\n prompt += `Focus on the following aspects: ${options.focus.join(', ')}. `\n }\n\n if (options.maxLength) {\n prompt += `Keep the summary under ${options.maxLength} tokens. `\n }\n\n return prompt\n }\n}\n"],"names":[],"mappings":";;;;AA0CA,MAAM,4BAAoD;AAAA,EACxD,QAAQ;AAAA,EACR,WAAW;AAAA,EACX,MAAM;AAAA,EACN,MAAM;AAAA,EACN,QAAQ;AAAA,EACR,YAAY;AACd;AASA,MAAM,wBAAwB;AAQ9B,SAAS,wBAAwB,aAA8B;AAC7D,SACE,gBAAgB,YAChB,0BAA0B,WAAW,MAAM;AAE/C;AAQA,SAAS,wBACP,aACA,aACA,cACyB;AACzB,QAAM,SAAkC,EAAE,GAAG,aAAA;AAE7C,MAAI,gBAAgB,UAAU;AAC5B,UAAM,WACJ,OAAO,WAAW,OAAO,OAAO,YAAY,WACvC,OAAO,UACR;AACN,QAAI,YAAY,iBAAiB,SAAU,QAAO;AAClD,WAAO,UAAU,EAAE,aAAa,GAAG,SAAA;AACnC,WAAO;AAAA,EACT;AAEA,MAAI,iBAAiB,OAAQ,QAAO;AACpC,SAAO,cAAc;AACrB,SAAO;AACT;AAqBA,SAAS,eACP,aACA,WACA,cACyB;AACzB,QAAM,SAAkC,EAAE,GAAG,aAAA;AAE7C,MAAI,gBAAgB,UAAU;AAG5B,UAAM,qBAAqB,sBAAsB;AAAA,MAC/C,CAAC,MAAM,OAAO,OAAO,CAAC,MAAM;AAAA,IAAA;AAE9B,UAAM,WACJ,OAAO,WAAW,OAAO,OAAO,YAAY,WACvC,OAAO,UACR;AACN,QACE,sBACC,YAAY,OAAO,SAAS,gBAAgB,UAC7C;AACA,aAAO;AAAA,IACT;AACA,WAAO,UAAU,EAAE,aAAa,WAAW,GAAG,SAAA;AAC9C,WAAO;AAAA,EACT;AAEA,QAAM,MAAM,0BAA0B,WAAW;AACjD,MAAI,QAAQ,OAAW,QAAO;AAE9B,QAAM,iBAAiB,sBAAsB;AAAA,IAC3C,CAAC,MAAM,OAAO,OAAO,CAAC,MAAM;AAAA,EAAA;AAE9B,MAAI,eAAgB,QAAO;AAE3B,SAAO,GAAG,IAAI;AACd,SAAO;AACT;AAmBO,MAAM,mCAGH,qBAA+C;AAAA,EAC9C;AAAA,EAEQ;AAAA,EAEjB,YACE,aACA,OACA,OAAe,yBACf;AACA,UAAM,CAAA,GAAI,KAAK;AACf,SAAK,OAAO;AACZ,SAAK,cAAc;AAAA,EACrB;AAAA,EAEA,MAAM,UACJ,SAC8B;AAC9B,UAAM,eAAe,KAAK,yBAAyB,OAAO;AAE1D,QAAI,UAAU;AACd,UAAM,KAAK,KAAK,WAAA;AAChB,QAAI,QAAQ,QAAQ;AACpB,QAAI,QAAQ,EAAE,cAAc,GAAG,kBAAkB,GAAG,aAAa,EAAA;AAEjE,YAAQ,OAAO;AAAA,MACb,+BAA+B,KAAK,IAAI,UAAU,QAAQ,KAAK,gBAAgB,QAAQ,KAAK,MAAM,cAAc,QAAQ,aAAa,OAAO;AAAA,MAC5I,EAAE,UAAU,KAAK,MAAM,OAAO,QAAQ,MAAA;AAAA,IAAM;AAG9C,QAAI;AACF,uBAAiB,SAAS,KAAK,YAAY;AAAA,QACzC,KAAK,iBAAiB,SAAS,YAAY;AAAA,MAAA,GAC1C;AACD,YAAI,MAAM,SAAS,wBAAwB;AACzC,cAAI,MAAM,SAAS;AACjB,sBAAU,MAAM;AAAA,UAClB,WAAW,MAAM,OAAO;AAGtB,uBAAW,MAAM;AAAA,UACnB;AACA,kBAAQ,MAAM,SAAS;AAAA,QACzB;AACA,YAAI,MAAM,SAAS,gBAAgB;AACjC,cAAI,MAAM,OAAO;AACf,oBAAQ,MAAM;AAAA,UAChB;AAAA,QACF;AAIA,YAAI,MAAM,SAAS,aAAa;AAC9B,gBAAM,WACH,MAAM,SAAS,OAAO,MAAM,MAAM,YAAY,WAC3C,MAAM,MAAM,UACZ,SAAS;AACf,gBAAM,OACJ,MAAM,SAAS,OAAO,MAAM,MAAM,SAAS,WACvC,MAAM,MAAM,OACZ;AACN,gBAAM,MAAM,IAAI,MAAM,OAAO;AAC7B,cAAI,MAAM;AACR;AAAE,gBAAkC,OAAO;AAAA,UAC7C;AACA,gBAAM;AAAA,QACR;AAAA,MACF;AAAA,IACF,SAAS,OAAgB;AAGvB,cAAQ,OAAO,OAAO,GAAG,KAAK,IAAI,oBAAoB;AAAA,QACpD,OAAO,kBAAkB,OAAO,GAAG,KAAK,IAAI,mBAAmB;AAAA,QAC/D,QAAQ,GAAG,KAAK,IAAI;AAAA,MAAA,CACrB;AACD,YAAM;AAAA,IACR;AAEA,WAAO,EAAE,IAAI,OAAO,SAAS,MAAA;AAAA,EAC/B;AAAA,EAEA,OAAgB,gBACd,SAC4B;AAC5B,UAAM,eAAe,KAAK,yBAAyB,OAAO;AAE1D,YAAQ,OAAO;AAAA,MACb,qCAAqC,KAAK,IAAI,UAAU,QAAQ,KAAK,gBAAgB,QAAQ,KAAK,MAAM,cAAc,QAAQ,aAAa,OAAO;AAAA,MAClJ,EAAE,UAAU,KAAK,MAAM,OAAO,QAAQ,MAAA;AAAA,IAAM;AAG9C,UAAM,KAAK,KAAK,WAAA;AAChB,QAAI,UAAU;AACd,QAAI,QAAQ,QAAQ;AACpB,QAAI,QAAsC;AAAA,MACxC,cAAc;AAAA,MACd,kBAAkB;AAAA,MAClB,aAAa;AAAA,IAAA;AAGf,QAAI;AACF,uBAAiB,SAAS,KAAK,YAAY;AAAA,QACzC,KAAK,iBAAiB,SAAS,YAAY;AAAA,MAAA,GAC1C;AAID,YAAI,MAAM,SAAS,wBAAwB;AACzC,cAAI,MAAM,SAAS;AACjB,sBAAU,MAAM;AAAA,UAClB,WAAW,MAAM,OAAO;AACtB,uBAAW,MAAM;AAAA,UACnB;AACA,cAAI,MAAM,MAAO,SAAQ,MAAM;AAAA,QACjC;AAKA,YAAI,MAAM,SAAS,gBAAgB;AACjC,cAAI,MAAM,MAAO,SAAQ,MAAM;AAC/B,cAAI,MAAM,MAAO,SAAQ,MAAM;AAC/B,gBAAM;AAAA,YACJ,MAAM,UAAU;AAAA,YAChB,MAAM;AAAA,YACN,OAAO,EAAE,IAAI,OAAO,SAAS,MAAA;AAAA,YAC7B;AAAA,YACA,WAAW,KAAK,IAAA;AAAA,UAAI;AAAA,QAExB;AAEA,cAAM;AAAA,MACR;AAAA,IACF,SAAS,OAAgB;AACvB,cAAQ,OAAO,OAAO,GAAG,KAAK,IAAI,0BAA0B;AAAA,QAC1D,OAAO,kBAAkB,OAAO,GAAG,KAAK,IAAI,yBAAyB;AAAA,QACrE,QAAQ,GAAG,KAAK,IAAI;AAAA,MAAA,CACrB;AACD,YAAM;AAAA,IACR;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQU,iBACR,SACA,cAC+B;AAQ/B,QAAI,UAAmC;AAAA,MACrC,GAAI,QAAQ;AAAA,IAAA;AAEd,cAAU,wBAAwB,KAAK,MAAM,KAAK,OAAO;AAMzD,QAAI,QAAQ,cAAc,QAAW;AACnC,UAAI,CAAC,wBAAwB,KAAK,IAAI,GAAG;AACvC,gBAAQ,OAAO;AAAA,UACb,wBAAwB,QAAQ,SAAS,kEAAkE,KAAK,IAAI;AAAA,UACpH,EAAE,UAAU,KAAK,KAAA;AAAA,QAAK;AAAA,MAE1B;AACA,gBAAU,eAAe,KAAK,MAAM,QAAQ,WAAW,OAAO;AAAA,IAChE;AACA,UAAM,eAAe;AAErB,WAAO;AAAA,MACL,OAAO,QAAQ;AAAA,MACf,UAAU,CAAC,EAAE,MAAM,QAAQ,SAAS,QAAQ,MAAM;AAAA,MAClD,eAAe,CAAC,YAAY;AAAA,MAC5B;AAAA,MACA,QAAQ,QAAQ;AAAA,IAAA;AAAA,EAEpB;AAAA,EAEU,yBACR,SACQ;AACR,QAAI,SAAS;AAEb,YAAQ,QAAQ,OAAA;AAAA,MACd,KAAK;AACH,kBAAU;AACV;AAAA,MACF,KAAK;AACH,kBAAU;AACV;AAAA,MACF,KAAK;AACH,kBAAU;AACV;AAAA,MACF,KAAK;AACH,kBAAU;AACV;AAAA,MACF;AACE,kBAAU;AAAA,IAAA;AAGd,QAAI,QAAQ,SAAS,QAAQ,MAAM,SAAS,GAAG;AAC7C,gBAAU,mCAAmC,QAAQ,MAAM,KAAK,IAAI,CAAC;AAAA,IACvE;AAEA,QAAI,QAAQ,WAAW;AACrB,gBAAU,0BAA0B,QAAQ,SAAS;AAAA,IACvD;AAEA,WAAO;AAAA,EACT;AACF;"}
|
|
@@ -30,4 +30,12 @@ export declare class InternalLogger {
|
|
|
30
30
|
errors(message: string, meta?: Record<string, unknown>): void;
|
|
31
31
|
/** Log outgoing request metadata before an adapter SDK call. */
|
|
32
32
|
request(message: string, meta?: Record<string, unknown>): void;
|
|
33
|
+
/**
|
|
34
|
+
* Log a non-fatal misconfiguration or recoverable anomaly. Gated by the
|
|
35
|
+
* `errors` category — on by default (and when `debug` is unspecified), so
|
|
36
|
+
* silent-drop conditions surface, but still silenced by `debug: false`,
|
|
37
|
+
* which honors the "disable everything including errors" contract. Routes to
|
|
38
|
+
* the underlying logger's `warn` level.
|
|
39
|
+
*/
|
|
40
|
+
warn(message: string, meta?: Record<string, unknown>): void;
|
|
33
41
|
}
|
|
@@ -64,6 +64,21 @@ class InternalLogger {
|
|
|
64
64
|
request(message, meta) {
|
|
65
65
|
this.emit("debug", "request", message, meta);
|
|
66
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* Log a non-fatal misconfiguration or recoverable anomaly. Gated by the
|
|
69
|
+
* `errors` category — on by default (and when `debug` is unspecified), so
|
|
70
|
+
* silent-drop conditions surface, but still silenced by `debug: false`,
|
|
71
|
+
* which honors the "disable everything including errors" contract. Routes to
|
|
72
|
+
* the underlying logger's `warn` level.
|
|
73
|
+
*/
|
|
74
|
+
warn(message, meta) {
|
|
75
|
+
if (!this.categories.errors) return;
|
|
76
|
+
const prefixed = `⚠️ [tanstack-ai:warn] ⚠️ ${message}`;
|
|
77
|
+
try {
|
|
78
|
+
this.logger.warn(prefixed, meta);
|
|
79
|
+
} catch {
|
|
80
|
+
}
|
|
81
|
+
}
|
|
67
82
|
}
|
|
68
83
|
export {
|
|
69
84
|
InternalLogger
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"internal-logger.js","sources":["../../../src/logger/internal-logger.ts"],"sourcesContent":["import type { DebugCategories, Logger } from './types'\n\n/**\n * Fully-resolved categories map. Every flag is a definite boolean (never\n * undefined), produced by `resolveDebugOption` from a `DebugOption`.\n */\nexport type ResolvedCategories = Required<DebugCategories>\n\n/**\n * Package-internal logger wrapper used by every activity and adapter in\n * `@tanstack/ai`. Wraps a user-supplied (or default `ConsoleLogger`) `Logger`\n * plus a fully-resolved per-category map. Each category has a dedicated\n * method that no-ops when its flag is `false`, or prepends a\n * `[tanstack-ai:<category>] ` prefix and calls the underlying logger's\n * `error` (for the `errors` category) or `debug` (for everything else).\n *\n * Not exported from the package root. Adapter packages consume it via the\n * `@tanstack/ai/adapter-internals` subpath export.\n */\n/**\n * Emoji marker per category — bracketing the `[tanstack-ai:<cat>]` tag on\n * both sides makes it trivial to visually pick out a category when scanning\n * dense streaming logs.\n */\nconst CATEGORY_EMOJI: Record<keyof ResolvedCategories, string> = {\n request: '📤',\n provider: '📥',\n output: '📨',\n middleware: '🧩',\n tools: '🔧',\n agentLoop: '🔁',\n config: '⚙️',\n errors: '❌',\n}\n\nexport class InternalLogger {\n constructor(\n private readonly logger: Logger,\n private readonly categories: ResolvedCategories,\n ) {}\n\n /** Whether a category is enabled. Cheap, safe to call on hot paths. */\n isEnabled(category: keyof ResolvedCategories): boolean {\n return this.categories[category]\n }\n\n private emit(\n level: 'debug' | 'error',\n category: keyof ResolvedCategories,\n message: string,\n meta?: Record<string, unknown>,\n ): void {\n if (!this.categories[category]) return\n const emoji = CATEGORY_EMOJI[category]\n const prefixed = `${emoji} [tanstack-ai:${category}] ${emoji} ${message}`\n try {\n if (level === 'error') this.logger.error(prefixed, meta)\n else this.logger.debug(prefixed, meta)\n } catch {\n // User-supplied logger threw; swallow so we never mask the original\n // error that triggered this log call.\n }\n }\n\n /** Log a raw chunk/frame received from a provider SDK. */\n provider(message: string, meta?: Record<string, unknown>): void {\n this.emit('debug', 'provider', message, meta)\n }\n\n /** Log a chunk/result yielded to the consumer after middleware. */\n output(message: string, meta?: Record<string, unknown>): void {\n this.emit('debug', 'output', message, meta)\n }\n\n /** Log inputs/outputs around a middleware hook invocation. Chat-only. */\n middleware(message: string, meta?: Record<string, unknown>): void {\n this.emit('debug', 'middleware', message, meta)\n }\n\n /** Log before/after a tool-call execution. Chat-only. */\n tools(message: string, meta?: Record<string, unknown>): void {\n this.emit('debug', 'tools', message, meta)\n }\n\n /** Log an agent-loop iteration marker or phase transition. Chat-only. */\n agentLoop(message: string, meta?: Record<string, unknown>): void {\n this.emit('debug', 'agentLoop', message, meta)\n }\n\n /** Log a config transform returned by a middleware `onConfig` hook. Chat-only. */\n config(message: string, meta?: Record<string, unknown>): void {\n this.emit('debug', 'config', message, meta)\n }\n\n /**\n * Log a caught error. Defaults to on even when `debug` is unspecified.\n * Uses the underlying logger's `error` level.\n */\n errors(message: string, meta?: Record<string, unknown>): void {\n this.emit('error', 'errors', message, meta)\n }\n\n /** Log outgoing request metadata before an adapter SDK call. */\n request(message: string, meta?: Record<string, unknown>): void {\n this.emit('debug', 'request', message, meta)\n }\n}\n"],"names":[],"mappings":"AAwBA,MAAM,iBAA2D;AAAA,EAC/D,SAAS;AAAA,EACT,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,OAAO;AAAA,EACP,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,QAAQ;AACV;AAEO,MAAM,eAAe;AAAA,EAC1B,YACmB,QACA,YACjB;AAFiB,SAAA,SAAA;AACA,SAAA,aAAA;AAAA,EAChB;AAAA,EAFgB;AAAA,EACA;AAAA;AAAA,EAInB,UAAU,UAA6C;AACrD,WAAO,KAAK,WAAW,QAAQ;AAAA,EACjC;AAAA,EAEQ,KACN,OACA,UACA,SACA,MACM;AACN,QAAI,CAAC,KAAK,WAAW,QAAQ,EAAG;AAChC,UAAM,QAAQ,eAAe,QAAQ;AACrC,UAAM,WAAW,GAAG,KAAK,iBAAiB,QAAQ,KAAK,KAAK,IAAI,OAAO;AACvE,QAAI;AACF,UAAI,UAAU,QAAS,MAAK,OAAO,MAAM,UAAU,IAAI;AAAA,UAClD,MAAK,OAAO,MAAM,UAAU,IAAI;AAAA,IACvC,QAAQ;AAAA,IAGR;AAAA,EACF;AAAA;AAAA,EAGA,SAAS,SAAiB,MAAsC;AAC9D,SAAK,KAAK,SAAS,YAAY,SAAS,IAAI;AAAA,EAC9C;AAAA;AAAA,EAGA,OAAO,SAAiB,MAAsC;AAC5D,SAAK,KAAK,SAAS,UAAU,SAAS,IAAI;AAAA,EAC5C;AAAA;AAAA,EAGA,WAAW,SAAiB,MAAsC;AAChE,SAAK,KAAK,SAAS,cAAc,SAAS,IAAI;AAAA,EAChD;AAAA;AAAA,EAGA,MAAM,SAAiB,MAAsC;AAC3D,SAAK,KAAK,SAAS,SAAS,SAAS,IAAI;AAAA,EAC3C;AAAA;AAAA,EAGA,UAAU,SAAiB,MAAsC;AAC/D,SAAK,KAAK,SAAS,aAAa,SAAS,IAAI;AAAA,EAC/C;AAAA;AAAA,EAGA,OAAO,SAAiB,MAAsC;AAC5D,SAAK,KAAK,SAAS,UAAU,SAAS,IAAI;AAAA,EAC5C;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAO,SAAiB,MAAsC;AAC5D,SAAK,KAAK,SAAS,UAAU,SAAS,IAAI;AAAA,EAC5C;AAAA;AAAA,EAGA,QAAQ,SAAiB,MAAsC;AAC7D,SAAK,KAAK,SAAS,WAAW,SAAS,IAAI;AAAA,EAC7C;AACF;"}
|
|
1
|
+
{"version":3,"file":"internal-logger.js","sources":["../../../src/logger/internal-logger.ts"],"sourcesContent":["import type { DebugCategories, Logger } from './types'\n\n/**\n * Fully-resolved categories map. Every flag is a definite boolean (never\n * undefined), produced by `resolveDebugOption` from a `DebugOption`.\n */\nexport type ResolvedCategories = Required<DebugCategories>\n\n/**\n * Package-internal logger wrapper used by every activity and adapter in\n * `@tanstack/ai`. Wraps a user-supplied (or default `ConsoleLogger`) `Logger`\n * plus a fully-resolved per-category map. Each category has a dedicated\n * method that no-ops when its flag is `false`, or prepends a\n * `[tanstack-ai:<category>] ` prefix and calls the underlying logger's\n * `error` (for the `errors` category) or `debug` (for everything else).\n *\n * Not exported from the package root. Adapter packages consume it via the\n * `@tanstack/ai/adapter-internals` subpath export.\n */\n/**\n * Emoji marker per category — bracketing the `[tanstack-ai:<cat>]` tag on\n * both sides makes it trivial to visually pick out a category when scanning\n * dense streaming logs.\n */\nconst CATEGORY_EMOJI: Record<keyof ResolvedCategories, string> = {\n request: '📤',\n provider: '📥',\n output: '📨',\n middleware: '🧩',\n tools: '🔧',\n agentLoop: '🔁',\n config: '⚙️',\n errors: '❌',\n}\n\nexport class InternalLogger {\n constructor(\n private readonly logger: Logger,\n private readonly categories: ResolvedCategories,\n ) {}\n\n /** Whether a category is enabled. Cheap, safe to call on hot paths. */\n isEnabled(category: keyof ResolvedCategories): boolean {\n return this.categories[category]\n }\n\n private emit(\n level: 'debug' | 'error',\n category: keyof ResolvedCategories,\n message: string,\n meta?: Record<string, unknown>,\n ): void {\n if (!this.categories[category]) return\n const emoji = CATEGORY_EMOJI[category]\n const prefixed = `${emoji} [tanstack-ai:${category}] ${emoji} ${message}`\n try {\n if (level === 'error') this.logger.error(prefixed, meta)\n else this.logger.debug(prefixed, meta)\n } catch {\n // User-supplied logger threw; swallow so we never mask the original\n // error that triggered this log call.\n }\n }\n\n /** Log a raw chunk/frame received from a provider SDK. */\n provider(message: string, meta?: Record<string, unknown>): void {\n this.emit('debug', 'provider', message, meta)\n }\n\n /** Log a chunk/result yielded to the consumer after middleware. */\n output(message: string, meta?: Record<string, unknown>): void {\n this.emit('debug', 'output', message, meta)\n }\n\n /** Log inputs/outputs around a middleware hook invocation. Chat-only. */\n middleware(message: string, meta?: Record<string, unknown>): void {\n this.emit('debug', 'middleware', message, meta)\n }\n\n /** Log before/after a tool-call execution. Chat-only. */\n tools(message: string, meta?: Record<string, unknown>): void {\n this.emit('debug', 'tools', message, meta)\n }\n\n /** Log an agent-loop iteration marker or phase transition. Chat-only. */\n agentLoop(message: string, meta?: Record<string, unknown>): void {\n this.emit('debug', 'agentLoop', message, meta)\n }\n\n /** Log a config transform returned by a middleware `onConfig` hook. Chat-only. */\n config(message: string, meta?: Record<string, unknown>): void {\n this.emit('debug', 'config', message, meta)\n }\n\n /**\n * Log a caught error. Defaults to on even when `debug` is unspecified.\n * Uses the underlying logger's `error` level.\n */\n errors(message: string, meta?: Record<string, unknown>): void {\n this.emit('error', 'errors', message, meta)\n }\n\n /** Log outgoing request metadata before an adapter SDK call. */\n request(message: string, meta?: Record<string, unknown>): void {\n this.emit('debug', 'request', message, meta)\n }\n\n /**\n * Log a non-fatal misconfiguration or recoverable anomaly. Gated by the\n * `errors` category — on by default (and when `debug` is unspecified), so\n * silent-drop conditions surface, but still silenced by `debug: false`,\n * which honors the \"disable everything including errors\" contract. Routes to\n * the underlying logger's `warn` level.\n */\n warn(message: string, meta?: Record<string, unknown>): void {\n if (!this.categories.errors) return\n const prefixed = `⚠️ [tanstack-ai:warn] ⚠️ ${message}`\n try {\n this.logger.warn(prefixed, meta)\n } catch {\n // User-supplied logger threw; swallow so a broken logger never masks the\n // condition we were trying to surface.\n }\n }\n}\n"],"names":[],"mappings":"AAwBA,MAAM,iBAA2D;AAAA,EAC/D,SAAS;AAAA,EACT,UAAU;AAAA,EACV,QAAQ;AAAA,EACR,YAAY;AAAA,EACZ,OAAO;AAAA,EACP,WAAW;AAAA,EACX,QAAQ;AAAA,EACR,QAAQ;AACV;AAEO,MAAM,eAAe;AAAA,EAC1B,YACmB,QACA,YACjB;AAFiB,SAAA,SAAA;AACA,SAAA,aAAA;AAAA,EAChB;AAAA,EAFgB;AAAA,EACA;AAAA;AAAA,EAInB,UAAU,UAA6C;AACrD,WAAO,KAAK,WAAW,QAAQ;AAAA,EACjC;AAAA,EAEQ,KACN,OACA,UACA,SACA,MACM;AACN,QAAI,CAAC,KAAK,WAAW,QAAQ,EAAG;AAChC,UAAM,QAAQ,eAAe,QAAQ;AACrC,UAAM,WAAW,GAAG,KAAK,iBAAiB,QAAQ,KAAK,KAAK,IAAI,OAAO;AACvE,QAAI;AACF,UAAI,UAAU,QAAS,MAAK,OAAO,MAAM,UAAU,IAAI;AAAA,UAClD,MAAK,OAAO,MAAM,UAAU,IAAI;AAAA,IACvC,QAAQ;AAAA,IAGR;AAAA,EACF;AAAA;AAAA,EAGA,SAAS,SAAiB,MAAsC;AAC9D,SAAK,KAAK,SAAS,YAAY,SAAS,IAAI;AAAA,EAC9C;AAAA;AAAA,EAGA,OAAO,SAAiB,MAAsC;AAC5D,SAAK,KAAK,SAAS,UAAU,SAAS,IAAI;AAAA,EAC5C;AAAA;AAAA,EAGA,WAAW,SAAiB,MAAsC;AAChE,SAAK,KAAK,SAAS,cAAc,SAAS,IAAI;AAAA,EAChD;AAAA;AAAA,EAGA,MAAM,SAAiB,MAAsC;AAC3D,SAAK,KAAK,SAAS,SAAS,SAAS,IAAI;AAAA,EAC3C;AAAA;AAAA,EAGA,UAAU,SAAiB,MAAsC;AAC/D,SAAK,KAAK,SAAS,aAAa,SAAS,IAAI;AAAA,EAC/C;AAAA;AAAA,EAGA,OAAO,SAAiB,MAAsC;AAC5D,SAAK,KAAK,SAAS,UAAU,SAAS,IAAI;AAAA,EAC5C;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,OAAO,SAAiB,MAAsC;AAC5D,SAAK,KAAK,SAAS,UAAU,SAAS,IAAI;AAAA,EAC5C;AAAA;AAAA,EAGA,QAAQ,SAAiB,MAAsC;AAC7D,SAAK,KAAK,SAAS,WAAW,SAAS,IAAI;AAAA,EAC7C;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EASA,KAAK,SAAiB,MAAsC;AAC1D,QAAI,CAAC,KAAK,WAAW,OAAQ;AAC7B,UAAM,WAAW,4BAA4B,OAAO;AACpD,QAAI;AACF,WAAK,OAAO,KAAK,UAAU,IAAI;AAAA,IACjC,QAAQ;AAAA,IAGR;AAAA,EACF;AACF;"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SpanStatusCode, SpanKind, trace, context } from "@opentelemetry/api";
|
|
2
|
+
import { MAX_TOKENS_KEYS, NESTED_MAX_TOKENS_KEY } from "../utilities/sampling-keys.js";
|
|
2
3
|
const stateByCtx = /* @__PURE__ */ new WeakMap();
|
|
3
4
|
const DEFAULT_MAX_CONTENT_LENGTH = 1e5;
|
|
4
5
|
const REDACTION_FAILED_SENTINEL = "[redaction_failed]";
|
|
@@ -50,6 +51,14 @@ function messageEventName(role) {
|
|
|
50
51
|
return `gen_ai.${role}.message`;
|
|
51
52
|
}
|
|
52
53
|
}
|
|
54
|
+
function firstNumber(...candidates) {
|
|
55
|
+
for (const candidate of candidates) {
|
|
56
|
+
if (typeof candidate === "number" && Number.isFinite(candidate)) {
|
|
57
|
+
return candidate;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return void 0;
|
|
61
|
+
}
|
|
53
62
|
function errorMessage(err) {
|
|
54
63
|
if (err instanceof Error) return err.message;
|
|
55
64
|
if (typeof err === "string") return err;
|
|
@@ -192,12 +201,27 @@ function otelMiddleware(options) {
|
|
|
192
201
|
"gen_ai.request.model": ctx.model,
|
|
193
202
|
"tanstack.ai.iteration": ctx.iteration
|
|
194
203
|
};
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
204
|
+
const sampling = config.modelOptions ?? {};
|
|
205
|
+
const nestedOptions = sampling["options"] && typeof sampling["options"] === "object" ? sampling["options"] : void 0;
|
|
206
|
+
const samplingTemperature = firstNumber(
|
|
207
|
+
sampling["temperature"],
|
|
208
|
+
nestedOptions?.["temperature"]
|
|
209
|
+
);
|
|
210
|
+
const samplingTopP = firstNumber(
|
|
211
|
+
sampling["top_p"],
|
|
212
|
+
sampling["topP"],
|
|
213
|
+
nestedOptions?.["top_p"]
|
|
214
|
+
);
|
|
215
|
+
const samplingMaxTokens = firstNumber(
|
|
216
|
+
...MAX_TOKENS_KEYS.map((k) => sampling[k]),
|
|
217
|
+
nestedOptions?.[NESTED_MAX_TOKENS_KEY]
|
|
218
|
+
);
|
|
219
|
+
if (samplingTemperature !== void 0)
|
|
220
|
+
baseAttrs["gen_ai.request.temperature"] = samplingTemperature;
|
|
221
|
+
if (samplingTopP !== void 0)
|
|
222
|
+
baseAttrs["gen_ai.request.top_p"] = samplingTopP;
|
|
223
|
+
if (samplingMaxTokens !== void 0)
|
|
224
|
+
baseAttrs["gen_ai.request.max_tokens"] = samplingMaxTokens;
|
|
201
225
|
const baseOptions = {
|
|
202
226
|
kind: SpanKind.CLIENT,
|
|
203
227
|
attributes: baseAttrs
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"otel.js","sources":["../../../src/middlewares/otel.ts"],"sourcesContent":["import {\n SpanKind,\n SpanStatusCode,\n context as otelContext,\n trace as otelTrace,\n} from '@opentelemetry/api'\nimport type {\n AttributeValue,\n Exception,\n Meter,\n Span,\n SpanOptions,\n Tracer,\n} from '@opentelemetry/api'\nimport type {\n ChatMiddleware,\n ChatMiddlewareContext,\n} from '../activities/chat/middleware/types'\n\n/**\n * Scope (role) of an OTel span emitted by this middleware.\n *\n * - `chat` — the root span for a single `chat()` call\n * - `iteration` — one per agent-loop iteration (one model call)\n * - `tool` — one per tool execution inside an iteration\n */\nexport type OtelSpanScope = 'chat' | 'iteration' | 'tool'\n\n/**\n * Alias retained for backwards compatibility. Prefer {@link OtelSpanScope}.\n *\n * @deprecated Use `OtelSpanScope` instead — the name shadows OTel's built-in\n * `SpanKind` which is also imported by integrations of this middleware.\n */\nexport type OtelSpanKind = OtelSpanScope\n\n/**\n * Span metadata passed to `spanNameFormatter`, `attributeEnricher`,\n * `onBeforeSpanStart`, and `onSpanEnd`. Discriminated by `kind` so that\n * tool-only fields narrow automatically inside callback bodies.\n */\nexport type OtelSpanInfo<TScope extends OtelSpanScope = OtelSpanScope> =\n TScope extends 'chat'\n ? { kind: 'chat'; ctx: ChatMiddlewareContext }\n : TScope extends 'iteration'\n ? { kind: 'iteration'; ctx: ChatMiddlewareContext; iteration: number }\n : TScope extends 'tool'\n ? {\n kind: 'tool'\n ctx: ChatMiddlewareContext\n iteration: number\n toolName: string\n toolCallId: string\n }\n : never\n\nexport interface OtelMiddlewareOptions {\n /** OTel `Tracer` used to start root, iteration, and tool spans. */\n tracer: Tracer\n /**\n * Optional OTel `Meter`. When provided, the middleware records\n * `gen_ai.client.operation.duration` and `gen_ai.client.token.usage`\n * histograms. Omit to disable metrics without disabling tracing.\n */\n meter?: Meter\n /**\n * When `true`, prompt and completion content is attached to iteration spans\n * as `gen_ai.*.message` / `gen_ai.choice` events. Defaults to `false` so\n * that PII never lands on a span by accident.\n */\n captureContent?: boolean\n /**\n * Invoked on every captured content string before it lands on a span.\n * Return a redacted version. If this function throws, the middleware emits\n * the literal sentinel `\"[redaction_failed]\"` instead of the original text\n * — it never falls back to raw content.\n */\n redact?: (text: string) => string\n /**\n * Maximum characters kept in the per-iteration assistant text buffer used\n * to emit `gen_ai.choice` events. Extra characters are truncated with a\n * trailing `\"…\"` marker. Defaults to 100 000. Set to `0` to disable the\n * cap. Exporters typically truncate long attribute values anyway.\n */\n maxContentLength?: number\n /** Override the default span name for each `kind`. */\n spanNameFormatter?: (info: OtelSpanInfo) => string\n /** Add extra attributes to each span. */\n attributeEnricher?: (info: OtelSpanInfo) => Record<string, AttributeValue>\n /** Mutate `SpanOptions` immediately before `tracer.startSpan(...)`. */\n onBeforeSpanStart?: (info: OtelSpanInfo, options: SpanOptions) => SpanOptions\n /** Fires just before every `span.end()`. */\n onSpanEnd?: (info: OtelSpanInfo, span: Span) => void\n}\n\ninterface RequestState {\n rootSpan: Span\n currentIterationSpan: Span | null\n toolSpans: Map<string, { span: Span; toolName: string }>\n iterationCount: number\n assistantTextBuffer: string\n assistantTextBufferTruncated: boolean\n startTime: number\n}\n\nconst stateByCtx = new WeakMap<ChatMiddlewareContext, RequestState>()\n\nconst DEFAULT_MAX_CONTENT_LENGTH = 100_000\nconst REDACTION_FAILED_SENTINEL = '[redaction_failed]'\n\nfunction serializeContent(content: unknown): string {\n if (typeof content === 'string') return content\n if (!Array.isArray(content)) return ''\n const parts: Array<string> = []\n for (const part of content) {\n if (!part || typeof part !== 'object') continue\n const type = (part as { type?: string }).type\n switch (type) {\n case 'text':\n parts.push(\n (\n (part as { text?: string }).text ??\n (part as { content?: string }).content ??\n ''\n ).toString(),\n )\n break\n case 'image':\n parts.push('[image]')\n break\n case 'audio':\n parts.push('[audio]')\n break\n case 'video':\n parts.push('[video]')\n break\n case 'document':\n parts.push('[document]')\n break\n case undefined:\n parts.push('[unknown]')\n break\n default:\n parts.push(`[${type}]`)\n }\n }\n return parts.join(' ')\n}\n\nfunction messageEventName(role: string): string {\n switch (role) {\n case 'user':\n return 'gen_ai.user.message'\n case 'assistant':\n return 'gen_ai.assistant.message'\n case 'tool':\n return 'gen_ai.tool.message'\n case 'system':\n return 'gen_ai.system.message'\n default:\n return `gen_ai.${role}.message`\n }\n}\n\nfunction errorMessage(err: unknown): string | undefined {\n if (err instanceof Error) return err.message\n if (typeof err === 'string') return err\n if (err && typeof err === 'object' && 'message' in err) {\n const m = (err as { message?: unknown }).message\n if (typeof m === 'string') return m\n }\n return undefined\n}\n\nfunction errorTypeName(err: unknown): string {\n if (err instanceof Error) return err.name || 'Error'\n if (err && typeof err === 'object' && 'name' in err) {\n const n = (err as { name?: unknown }).name\n if (typeof n === 'string') return n\n }\n return 'Error'\n}\n\nfunction safeCall<T>(label: string, fn: () => T): T | undefined {\n try {\n return fn()\n } catch (err) {\n // Keep middleware non-fatal, but surface callback failures so that broken\n // extension points (attributeEnricher, spanNameFormatter, onSpanEnd, ...)\n // are observable. Matches the guarantee documented in docs/advanced/otel.md.\n console.warn(`[otelMiddleware] ${label} failed`, err)\n return undefined\n }\n}\n\nexport function otelMiddleware(options: OtelMiddlewareOptions): ChatMiddleware {\n const {\n tracer,\n meter,\n captureContent = false,\n redact = (s) => s,\n maxContentLength = DEFAULT_MAX_CONTENT_LENGTH,\n spanNameFormatter,\n attributeEnricher,\n onBeforeSpanStart,\n onSpanEnd,\n } = options\n\n const durationHistogram = meter?.createHistogram(\n 'gen_ai.client.operation.duration',\n {\n description: 'GenAI client operation duration',\n unit: 's',\n },\n )\n const tokenHistogram = meter?.createHistogram('gen_ai.client.token.usage', {\n description: 'GenAI client token usage',\n unit: '{token}',\n })\n\n // Redact user content, failing closed to a sentinel string instead of ever\n // letting raw text through. Callers that pass `captureContent: true` with a\n // third-party PII redactor depend on this invariant.\n const redactContent = (text: string): string => {\n try {\n return redact(text)\n } catch (err) {\n console.warn('[otelMiddleware] otel.redact failed', err)\n return REDACTION_FAILED_SENTINEL\n }\n }\n\n const appendAssistantText = (state: RequestState, delta: string): void => {\n if (maxContentLength > 0) {\n if (state.assistantTextBufferTruncated) return\n const remaining = maxContentLength - state.assistantTextBuffer.length\n if (remaining <= 0) {\n state.assistantTextBufferTruncated = true\n state.assistantTextBuffer += '…'\n return\n }\n if (delta.length > remaining) {\n state.assistantTextBuffer += delta.slice(0, remaining) + '…'\n state.assistantTextBufferTruncated = true\n return\n }\n }\n state.assistantTextBuffer += delta\n }\n\n const closeIterationSpan = (\n state: RequestState,\n ctx: ChatMiddlewareContext,\n ): void => {\n if (!state.currentIterationSpan) return\n const span = state.currentIterationSpan\n const iteration = state.iterationCount - 1\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'iteration', ctx, iteration }, span),\n )\n span.end()\n state.currentIterationSpan = null\n }\n\n return {\n name: 'otel',\n\n onStart(ctx) {\n safeCall('otel.onStart', () => {\n const info: OtelSpanInfo<'chat'> = { kind: 'chat', ctx }\n const name =\n safeCall('otel.spanNameFormatter', () => spanNameFormatter?.(info)) ??\n `chat ${ctx.model}`\n const baseOptions: SpanOptions = {\n kind: SpanKind.INTERNAL,\n attributes: {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.request.model': ctx.model,\n // NOTE: `gen_ai.operation.name` is deliberately NOT set on the\n // root span. The root represents a `chat()` invocation that may\n // span multiple model calls; only iteration spans correspond to\n // a single chat operation. Backends that map `operation.name=chat`\n // to a \"generation\" event (e.g. PostHog LLM Analytics) would\n // otherwise emit a duplicate generation for the wrapper span.\n },\n }\n const spanOptions =\n safeCall('otel.onBeforeSpanStart', () =>\n onBeforeSpanStart?.(info, baseOptions),\n ) ?? baseOptions\n const rootSpan = tracer.startSpan(name, spanOptions)\n\n const enriched = safeCall('otel.attributeEnricher', () =>\n attributeEnricher?.(info),\n )\n if (enriched) rootSpan.setAttributes(enriched)\n\n stateByCtx.set(ctx, {\n rootSpan,\n currentIterationSpan: null,\n toolSpans: new Map(),\n iterationCount: 0,\n assistantTextBuffer: '',\n assistantTextBufferTruncated: false,\n startTime: Date.now(),\n })\n })\n },\n\n onConfig(ctx, config) {\n if (ctx.phase !== 'beforeModel') return\n safeCall('otel.onConfig', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n\n // The previous iteration's span stays open through tool execution and\n // onUsage so that tool spans nest under it and token attributes land\n // on it. Close it here, just before opening the next iteration.\n closeIterationSpan(state, ctx)\n\n const info: OtelSpanInfo<'iteration'> = {\n kind: 'iteration',\n ctx,\n iteration: ctx.iteration,\n }\n const name =\n safeCall('otel.spanNameFormatter', () => spanNameFormatter?.(info)) ??\n `chat ${ctx.model} #${ctx.iteration}`\n\n const baseAttrs: Record<string, AttributeValue> = {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': ctx.model,\n 'tanstack.ai.iteration': ctx.iteration,\n }\n if (config.temperature !== undefined)\n baseAttrs['gen_ai.request.temperature'] = config.temperature\n if (config.topP !== undefined)\n baseAttrs['gen_ai.request.top_p'] = config.topP\n if (config.maxTokens !== undefined)\n baseAttrs['gen_ai.request.max_tokens'] = config.maxTokens\n\n const baseOptions: SpanOptions = {\n kind: SpanKind.CLIENT,\n attributes: baseAttrs,\n }\n const spanOptions =\n safeCall('otel.onBeforeSpanStart', () =>\n onBeforeSpanStart?.(info, baseOptions),\n ) ?? baseOptions\n\n const parentCtx = otelTrace.setSpan(\n otelContext.active(),\n state.rootSpan,\n )\n let iterSpan!: Span\n otelContext.with(parentCtx, () => {\n // Pass the parent context explicitly as the 3rd arg — this is a\n // real-OTel-compatible way to ensure the span is parented to\n // `rootSpan` even when the host app has not registered a context\n // manager (e.g. in tests or minimal setups).\n iterSpan = tracer.startSpan(name, spanOptions, parentCtx)\n })\n\n const enriched = safeCall('otel.attributeEnricher', () =>\n attributeEnricher?.(info),\n )\n if (enriched) iterSpan.setAttributes(enriched)\n\n state.currentIterationSpan = iterSpan\n state.assistantTextBuffer = ''\n state.assistantTextBufferTruncated = false\n\n if (captureContent) {\n const systemPromptContents = config.systemPrompts.map((p) =>\n typeof p === 'string' ? p : p.content,\n )\n // Anthropic prompt-caching users need to know which prompt carried\n // `cache_control`: it's the one attribute that explains cache\n // hit/miss in observability. Serialise per-prompt metadata as a\n // single JSON span attribute so backends that don't understand\n // GenAI events can still surface it. Kept off span events to\n // avoid breaking the one-event-per-message GenAI semconv contract.\n const systemPromptMetadata = config.systemPrompts.map((p) =>\n typeof p === 'string' || p.metadata === undefined\n ? null\n : p.metadata,\n )\n if (systemPromptMetadata.some((m) => m !== null)) {\n iterSpan.setAttribute(\n 'tanstack.ai.system_prompt.metadata',\n JSON.stringify(systemPromptMetadata),\n )\n }\n // Span events follow the original GenAI semconv (one event per\n // message). Backends that read events get content this way.\n for (const sys of systemPromptContents) {\n iterSpan.addEvent('gen_ai.system.message', {\n content: redactContent(sys),\n })\n }\n for (const m of config.messages) {\n const body = serializeContent(m.content)\n if (body.length === 0) continue\n iterSpan.addEvent(messageEventName(m.role), {\n content: redactContent(body),\n })\n }\n\n // Also emit the current GenAI-semconv attribute form\n // (`gen_ai.input.messages`) — backends like PostHog read prompt\n // content from this attribute, not from span events.\n const inputMessages: Array<{ role: string; content: string }> = []\n for (const sys of systemPromptContents) {\n inputMessages.push({\n role: 'system',\n content: redactContent(sys),\n })\n }\n for (const m of config.messages) {\n const body = serializeContent(m.content)\n if (body.length === 0) continue\n inputMessages.push({\n role: m.role,\n content: redactContent(body),\n })\n }\n if (inputMessages.length > 0) {\n const inputJson = JSON.stringify(inputMessages)\n // Current OTel GenAI semconv — Sentry / PostHog / Datadog read\n // prompt content from this attribute.\n iterSpan.setAttribute('gen_ai.input.messages', inputJson)\n // Langfuse-native attribute. Highest priority in Langfuse's OTLP\n // ingestion (checked before events and gen_ai.input.messages) so\n // the Input panel populates reliably. Harmless to other backends —\n // the attribute is namespaced and unrecognised keys are ignored.\n iterSpan.setAttribute('langfuse.observation.input', inputJson)\n\n // Mirror the first iteration's input onto the root span and at\n // trace level so Langfuse fills Input on the trace card and the\n // chat-level observation. Later iterations append tool-call /\n // assistant messages that are useful per-iteration but noise at\n // the chat / trace level.\n if (state.iterationCount === 0) {\n state.rootSpan.setAttribute(\n 'langfuse.observation.input',\n inputJson,\n )\n state.rootSpan.setAttribute('langfuse.trace.input', inputJson)\n }\n }\n }\n\n state.iterationCount += 1\n })\n return undefined\n },\n\n onChunk(ctx, chunk) {\n safeCall('otel.onChunk', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n\n if (captureContent && chunk.type === 'TEXT_MESSAGE_CONTENT') {\n appendAssistantText(state, chunk.delta)\n }\n\n if (chunk.type !== 'RUN_FINISHED') return\n const span = state.currentIterationSpan\n if (!span) return\n\n if (chunk.finishReason) {\n span.setAttribute('gen_ai.response.finish_reasons', [\n chunk.finishReason,\n ])\n }\n if (chunk.model) span.setAttribute('gen_ai.response.model', chunk.model)\n\n // Set usage attributes on the iteration span directly from the chunk\n // so they're available before `onUsage` fires. Histogram recording is\n // deliberately NOT done here — the chat runner always invokes\n // `runOnUsage` when `chunk.usage` is present, and `onUsage` is the\n // canonical place for the metric. Recording in both would double-count.\n if (chunk.usage) {\n span.setAttributes({\n 'gen_ai.usage.input_tokens': chunk.usage.promptTokens,\n 'gen_ai.usage.output_tokens': chunk.usage.completionTokens,\n })\n }\n\n if (captureContent && state.assistantTextBuffer.length > 0) {\n const completion = redactContent(state.assistantTextBuffer)\n const outputJson = JSON.stringify([\n { role: 'assistant', content: completion },\n ])\n // Event form (older semconv) — kept for backends that consume it.\n span.addEvent('gen_ai.choice', { content: completion })\n // Attribute form (current semconv) — required by backends like\n // PostHog that read completion content from `gen_ai.output.messages`.\n span.setAttribute('gen_ai.output.messages', outputJson)\n // Langfuse-native attribute (highest priority in Langfuse mapping).\n span.setAttribute('langfuse.observation.output', outputJson)\n // Mirror to the root span and trace card. Each iteration overwrites,\n // so the final iteration's completion lands on the root — which is\n // the final answer the user saw, not an intermediate tool-call turn.\n state.rootSpan.setAttribute('langfuse.observation.output', outputJson)\n state.rootSpan.setAttribute('langfuse.trace.output', outputJson)\n state.assistantTextBuffer = ''\n state.assistantTextBufferTruncated = false\n }\n\n // Intentionally leave the iteration span open: tool spans started\n // after `RUN_FINISHED` (tool_calls finishReason) must nest under it,\n // and `onUsage` may still fire. The span is closed in `onConfig` when\n // the next iteration starts, or in `onFinish` / `onError` / `onAbort`.\n })\n return undefined\n },\n\n onUsage(ctx, usage) {\n safeCall('otel.onUsage', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n\n // Always record the token histogram — metrics don't depend on having\n // an iteration span, and skipping here would drop metric data if an\n // adapter emits `onUsage` outside the iteration window.\n if (tokenHistogram) {\n const metricAttrs = {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': ctx.model,\n }\n tokenHistogram.record(usage.promptTokens, {\n ...metricAttrs,\n 'gen_ai.token.type': 'input',\n })\n tokenHistogram.record(usage.completionTokens, {\n ...metricAttrs,\n 'gen_ai.token.type': 'output',\n })\n }\n\n const span = state.currentIterationSpan ?? state.rootSpan\n span.setAttributes({\n 'gen_ai.usage.input_tokens': usage.promptTokens,\n 'gen_ai.usage.output_tokens': usage.completionTokens,\n })\n })\n },\n\n onBeforeToolCall(ctx, hookCtx) {\n safeCall('otel.onBeforeToolCall', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n const parent = state.currentIterationSpan ?? state.rootSpan\n\n const info: OtelSpanInfo<'tool'> = {\n kind: 'tool',\n ctx,\n toolName: hookCtx.toolName,\n toolCallId: hookCtx.toolCallId,\n iteration: state.iterationCount - 1,\n }\n const name =\n safeCall('otel.spanNameFormatter', () => spanNameFormatter?.(info)) ??\n `execute_tool ${hookCtx.toolName}`\n\n const baseAttrs: Record<string, AttributeValue> = {\n 'gen_ai.tool.name': hookCtx.toolName,\n 'gen_ai.tool.call.id': hookCtx.toolCallId,\n 'gen_ai.tool.type': 'function',\n }\n const baseOptions: SpanOptions = {\n kind: SpanKind.INTERNAL,\n attributes: baseAttrs,\n }\n const spanOptions =\n safeCall('otel.onBeforeSpanStart', () =>\n onBeforeSpanStart?.(info, baseOptions),\n ) ?? baseOptions\n\n const parentCtx = otelTrace.setSpan(otelContext.active(), parent)\n let toolSpan!: Span\n otelContext.with(parentCtx, () => {\n toolSpan = tracer.startSpan(name, spanOptions, parentCtx)\n })\n\n const enriched = safeCall('otel.attributeEnricher', () =>\n attributeEnricher?.(info),\n )\n if (enriched) toolSpan.setAttributes(enriched)\n\n // Stamp the tool args onto the tool span so backends that render an\n // input panel per span (e.g. PostHog) have something to show.\n if (captureContent) {\n const argsBody =\n typeof hookCtx.args === 'string'\n ? hookCtx.args\n : (safeCall('otel.serializeToolArgs', () =>\n JSON.stringify(hookCtx.args ?? null),\n ) ?? '[unserializable_tool_args]')\n const redactedArgs = redactContent(argsBody)\n const toolInputJson = JSON.stringify([\n { role: 'tool', content: redactedArgs },\n ])\n toolSpan.setAttribute('gen_ai.input.messages', toolInputJson)\n // Langfuse-native (highest priority in Langfuse mapping).\n toolSpan.setAttribute('langfuse.observation.input', toolInputJson)\n }\n\n state.toolSpans.set(hookCtx.toolCallId, {\n span: toolSpan,\n toolName: hookCtx.toolName,\n })\n })\n return undefined\n },\n\n onAfterToolCall(ctx, info) {\n safeCall('otel.onAfterToolCall', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n const entry = state.toolSpans.get(info.toolCallId)\n if (!entry) return\n const { span: toolSpan } = entry\n\n const outcome = info.ok ? 'success' : 'error'\n toolSpan.setAttribute('tanstack.ai.tool.outcome', outcome)\n\n if (!info.ok && info.error !== undefined) {\n toolSpan.recordException(info.error as Exception)\n const msg = errorMessage(info.error)\n toolSpan.setStatus({\n code: SpanStatusCode.ERROR,\n ...(msg !== undefined && { message: msg }),\n })\n }\n\n if (captureContent) {\n // Serialization can throw on circular refs or `BigInt` values. If it\n // does, fall back to a sentinel so the rest of this handler (span\n // end, onSpanEnd, toolSpans cleanup) still runs — otherwise the tool\n // span would dangle until the onFinish/onError sweep.\n const body =\n typeof info.result === 'string'\n ? info.result\n : (safeCall('otel.serializeToolResult', () =>\n JSON.stringify(info.result ?? null),\n ) ?? '[unserializable_tool_result]')\n const redactedBody = redactContent(body)\n if (state.currentIterationSpan) {\n state.currentIterationSpan.addEvent('gen_ai.tool.message', {\n content: redactedBody,\n tool_call_id: info.toolCallId,\n })\n }\n // Output panel of the tool span itself — `gen_ai.output.messages` is\n // what current GenAI semconv consumers (e.g. PostHog) read.\n const toolOutputJson = JSON.stringify([\n { role: 'tool', content: redactedBody },\n ])\n toolSpan.setAttribute('gen_ai.output.messages', toolOutputJson)\n // Langfuse-native (highest priority in Langfuse mapping).\n toolSpan.setAttribute('langfuse.observation.output', toolOutputJson)\n }\n\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'tool',\n ctx,\n toolName: info.toolName,\n toolCallId: info.toolCallId,\n iteration: state.iterationCount - 1,\n },\n toolSpan,\n ),\n )\n toolSpan.end()\n state.toolSpans.delete(info.toolCallId)\n })\n },\n\n onError(ctx, info) {\n safeCall('otel.onError', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n\n const errType = errorTypeName(info.error)\n const message = errorMessage(info.error)\n const statusMessage =\n message !== undefined ? { message } : ({} as const)\n const exception = info.error as Exception\n\n const iterationSpan = state.currentIterationSpan\n if (iterationSpan) {\n iterationSpan.recordException(exception)\n iterationSpan.setStatus({\n code: SpanStatusCode.ERROR,\n ...statusMessage,\n })\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'iteration',\n ctx,\n iteration: state.iterationCount - 1,\n },\n iterationSpan,\n ),\n )\n iterationSpan.end()\n state.currentIterationSpan = null\n }\n\n for (const [id, entry] of state.toolSpans) {\n const { span, toolName } = entry\n span.recordException(exception)\n span.setStatus({ code: SpanStatusCode.ERROR, ...statusMessage })\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'tool',\n ctx,\n toolCallId: id,\n toolName,\n iteration: state.iterationCount - 1,\n },\n span,\n ),\n )\n span.end()\n state.toolSpans.delete(id)\n }\n\n state.rootSpan.recordException(exception)\n state.rootSpan.setStatus({\n code: SpanStatusCode.ERROR,\n ...statusMessage,\n })\n\n if (durationHistogram) {\n durationHistogram.record(info.duration / 1000, {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': ctx.model,\n 'error.type': errType,\n })\n }\n\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'chat', ctx }, state.rootSpan),\n )\n state.rootSpan.end()\n stateByCtx.delete(ctx)\n })\n },\n\n onAbort(ctx, info) {\n safeCall('otel.onAbort', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n\n const closeCancelled = (span: Span): void => {\n // `gen_ai.completion.reason` is not part of the GenAI semconv; use a\n // TanStack-namespaced attribute so downstream exporters don't treat\n // it as standard. The span status still carries the error code.\n span.setAttribute('tanstack.ai.completion.reason', 'cancelled')\n span.setStatus({ code: SpanStatusCode.ERROR, message: 'cancelled' })\n }\n\n const iterationSpan = state.currentIterationSpan\n if (iterationSpan) {\n closeCancelled(iterationSpan)\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'iteration',\n ctx,\n iteration: state.iterationCount - 1,\n },\n iterationSpan,\n ),\n )\n iterationSpan.end()\n state.currentIterationSpan = null\n }\n for (const [id, entry] of state.toolSpans) {\n const { span, toolName } = entry\n closeCancelled(span)\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'tool',\n ctx,\n toolCallId: id,\n toolName,\n iteration: state.iterationCount - 1,\n },\n span,\n ),\n )\n span.end()\n state.toolSpans.delete(id)\n }\n closeCancelled(state.rootSpan)\n\n if (durationHistogram) {\n durationHistogram.record(info.duration / 1000, {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': ctx.model,\n 'error.type': 'cancelled',\n })\n }\n\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'chat', ctx }, state.rootSpan),\n )\n state.rootSpan.end()\n stateByCtx.delete(ctx)\n })\n },\n\n onFinish(ctx, info) {\n safeCall('otel.onFinish', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n\n // Close any tool spans that never received `onAfterToolCall` (adapter\n // quirk). Done before the iteration span so the hierarchy is closed\n // in depth-first order.\n for (const [id, entry] of state.toolSpans) {\n const { span, toolName } = entry\n span.setAttribute('tanstack.ai.tool.outcome', 'unknown')\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'tool',\n ctx,\n toolCallId: id,\n toolName,\n iteration: state.iterationCount - 1,\n },\n span,\n ),\n )\n span.end()\n state.toolSpans.delete(id)\n }\n\n // The final iteration's span is still open because we keep it open\n // through tool execution and `onUsage`. Close it now.\n closeIterationSpan(state, ctx)\n\n if (durationHistogram) {\n durationHistogram.record(info.duration / 1000, {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': ctx.model,\n })\n }\n\n if (info.usage) {\n state.rootSpan.setAttributes({\n 'gen_ai.usage.input_tokens': info.usage.promptTokens,\n 'gen_ai.usage.output_tokens': info.usage.completionTokens,\n })\n }\n if (info.finishReason) {\n state.rootSpan.setAttribute('gen_ai.response.finish_reasons', [\n info.finishReason,\n ])\n }\n state.rootSpan.setAttribute(\n 'tanstack.ai.iterations',\n state.iterationCount,\n )\n\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'chat', ctx }, state.rootSpan),\n )\n state.rootSpan.end()\n stateByCtx.delete(ctx)\n })\n },\n }\n}\n"],"names":["otelTrace","otelContext"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"otel.js","sources":["../../../src/middlewares/otel.ts"],"sourcesContent":["import {\n SpanKind,\n SpanStatusCode,\n context as otelContext,\n trace as otelTrace,\n} from '@opentelemetry/api'\nimport {\n MAX_TOKENS_KEYS,\n NESTED_MAX_TOKENS_KEY,\n} from '../utilities/sampling-keys'\nimport type {\n AttributeValue,\n Exception,\n Meter,\n Span,\n SpanOptions,\n Tracer,\n} from '@opentelemetry/api'\nimport type {\n ChatMiddleware,\n ChatMiddlewareContext,\n} from '../activities/chat/middleware/types'\n\n/**\n * Scope (role) of an OTel span emitted by this middleware.\n *\n * - `chat` — the root span for a single `chat()` call\n * - `iteration` — one per agent-loop iteration (one model call)\n * - `tool` — one per tool execution inside an iteration\n */\nexport type OtelSpanScope = 'chat' | 'iteration' | 'tool'\n\n/**\n * Alias retained for backwards compatibility. Prefer {@link OtelSpanScope}.\n *\n * @deprecated Use `OtelSpanScope` instead — the name shadows OTel's built-in\n * `SpanKind` which is also imported by integrations of this middleware.\n */\nexport type OtelSpanKind = OtelSpanScope\n\n/**\n * Span metadata passed to `spanNameFormatter`, `attributeEnricher`,\n * `onBeforeSpanStart`, and `onSpanEnd`. Discriminated by `kind` so that\n * tool-only fields narrow automatically inside callback bodies.\n */\nexport type OtelSpanInfo<TScope extends OtelSpanScope = OtelSpanScope> =\n TScope extends 'chat'\n ? { kind: 'chat'; ctx: ChatMiddlewareContext }\n : TScope extends 'iteration'\n ? { kind: 'iteration'; ctx: ChatMiddlewareContext; iteration: number }\n : TScope extends 'tool'\n ? {\n kind: 'tool'\n ctx: ChatMiddlewareContext\n iteration: number\n toolName: string\n toolCallId: string\n }\n : never\n\nexport interface OtelMiddlewareOptions {\n /** OTel `Tracer` used to start root, iteration, and tool spans. */\n tracer: Tracer\n /**\n * Optional OTel `Meter`. When provided, the middleware records\n * `gen_ai.client.operation.duration` and `gen_ai.client.token.usage`\n * histograms. Omit to disable metrics without disabling tracing.\n */\n meter?: Meter\n /**\n * When `true`, prompt and completion content is attached to iteration spans\n * as `gen_ai.*.message` / `gen_ai.choice` events. Defaults to `false` so\n * that PII never lands on a span by accident.\n */\n captureContent?: boolean\n /**\n * Invoked on every captured content string before it lands on a span.\n * Return a redacted version. If this function throws, the middleware emits\n * the literal sentinel `\"[redaction_failed]\"` instead of the original text\n * — it never falls back to raw content.\n */\n redact?: (text: string) => string\n /**\n * Maximum characters kept in the per-iteration assistant text buffer used\n * to emit `gen_ai.choice` events. Extra characters are truncated with a\n * trailing `\"…\"` marker. Defaults to 100 000. Set to `0` to disable the\n * cap. Exporters typically truncate long attribute values anyway.\n */\n maxContentLength?: number\n /** Override the default span name for each `kind`. */\n spanNameFormatter?: (info: OtelSpanInfo) => string\n /** Add extra attributes to each span. */\n attributeEnricher?: (info: OtelSpanInfo) => Record<string, AttributeValue>\n /** Mutate `SpanOptions` immediately before `tracer.startSpan(...)`. */\n onBeforeSpanStart?: (info: OtelSpanInfo, options: SpanOptions) => SpanOptions\n /** Fires just before every `span.end()`. */\n onSpanEnd?: (info: OtelSpanInfo, span: Span) => void\n}\n\ninterface RequestState {\n rootSpan: Span\n currentIterationSpan: Span | null\n toolSpans: Map<string, { span: Span; toolName: string }>\n iterationCount: number\n assistantTextBuffer: string\n assistantTextBufferTruncated: boolean\n startTime: number\n}\n\nconst stateByCtx = new WeakMap<ChatMiddlewareContext, RequestState>()\n\nconst DEFAULT_MAX_CONTENT_LENGTH = 100_000\nconst REDACTION_FAILED_SENTINEL = '[redaction_failed]'\n\nfunction serializeContent(content: unknown): string {\n if (typeof content === 'string') return content\n if (!Array.isArray(content)) return ''\n const parts: Array<string> = []\n for (const part of content) {\n if (!part || typeof part !== 'object') continue\n const type = (part as { type?: string }).type\n switch (type) {\n case 'text':\n parts.push(\n (\n (part as { text?: string }).text ??\n (part as { content?: string }).content ??\n ''\n ).toString(),\n )\n break\n case 'image':\n parts.push('[image]')\n break\n case 'audio':\n parts.push('[audio]')\n break\n case 'video':\n parts.push('[video]')\n break\n case 'document':\n parts.push('[document]')\n break\n case undefined:\n parts.push('[unknown]')\n break\n default:\n parts.push(`[${type}]`)\n }\n }\n return parts.join(' ')\n}\n\nfunction messageEventName(role: string): string {\n switch (role) {\n case 'user':\n return 'gen_ai.user.message'\n case 'assistant':\n return 'gen_ai.assistant.message'\n case 'tool':\n return 'gen_ai.tool.message'\n case 'system':\n return 'gen_ai.system.message'\n default:\n return `gen_ai.${role}.message`\n }\n}\n\n/**\n * Return the first candidate that is a finite `number`, or `undefined`. Used to\n * pick a sampling attribute from among the several provider-native spellings.\n */\nfunction firstNumber(...candidates: Array<unknown>): number | undefined {\n for (const candidate of candidates) {\n if (typeof candidate === 'number' && Number.isFinite(candidate)) {\n return candidate\n }\n }\n return undefined\n}\n\nfunction errorMessage(err: unknown): string | undefined {\n if (err instanceof Error) return err.message\n if (typeof err === 'string') return err\n if (err && typeof err === 'object' && 'message' in err) {\n const m = (err as { message?: unknown }).message\n if (typeof m === 'string') return m\n }\n return undefined\n}\n\nfunction errorTypeName(err: unknown): string {\n if (err instanceof Error) return err.name || 'Error'\n if (err && typeof err === 'object' && 'name' in err) {\n const n = (err as { name?: unknown }).name\n if (typeof n === 'string') return n\n }\n return 'Error'\n}\n\nfunction safeCall<T>(label: string, fn: () => T): T | undefined {\n try {\n return fn()\n } catch (err) {\n // Keep middleware non-fatal, but surface callback failures so that broken\n // extension points (attributeEnricher, spanNameFormatter, onSpanEnd, ...)\n // are observable. Matches the guarantee documented in docs/advanced/otel.md.\n console.warn(`[otelMiddleware] ${label} failed`, err)\n return undefined\n }\n}\n\nexport function otelMiddleware(options: OtelMiddlewareOptions): ChatMiddleware {\n const {\n tracer,\n meter,\n captureContent = false,\n redact = (s) => s,\n maxContentLength = DEFAULT_MAX_CONTENT_LENGTH,\n spanNameFormatter,\n attributeEnricher,\n onBeforeSpanStart,\n onSpanEnd,\n } = options\n\n const durationHistogram = meter?.createHistogram(\n 'gen_ai.client.operation.duration',\n {\n description: 'GenAI client operation duration',\n unit: 's',\n },\n )\n const tokenHistogram = meter?.createHistogram('gen_ai.client.token.usage', {\n description: 'GenAI client token usage',\n unit: '{token}',\n })\n\n // Redact user content, failing closed to a sentinel string instead of ever\n // letting raw text through. Callers that pass `captureContent: true` with a\n // third-party PII redactor depend on this invariant.\n const redactContent = (text: string): string => {\n try {\n return redact(text)\n } catch (err) {\n console.warn('[otelMiddleware] otel.redact failed', err)\n return REDACTION_FAILED_SENTINEL\n }\n }\n\n const appendAssistantText = (state: RequestState, delta: string): void => {\n if (maxContentLength > 0) {\n if (state.assistantTextBufferTruncated) return\n const remaining = maxContentLength - state.assistantTextBuffer.length\n if (remaining <= 0) {\n state.assistantTextBufferTruncated = true\n state.assistantTextBuffer += '…'\n return\n }\n if (delta.length > remaining) {\n state.assistantTextBuffer += delta.slice(0, remaining) + '…'\n state.assistantTextBufferTruncated = true\n return\n }\n }\n state.assistantTextBuffer += delta\n }\n\n const closeIterationSpan = (\n state: RequestState,\n ctx: ChatMiddlewareContext,\n ): void => {\n if (!state.currentIterationSpan) return\n const span = state.currentIterationSpan\n const iteration = state.iterationCount - 1\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'iteration', ctx, iteration }, span),\n )\n span.end()\n state.currentIterationSpan = null\n }\n\n return {\n name: 'otel',\n\n onStart(ctx) {\n safeCall('otel.onStart', () => {\n const info: OtelSpanInfo<'chat'> = { kind: 'chat', ctx }\n const name =\n safeCall('otel.spanNameFormatter', () => spanNameFormatter?.(info)) ??\n `chat ${ctx.model}`\n const baseOptions: SpanOptions = {\n kind: SpanKind.INTERNAL,\n attributes: {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.request.model': ctx.model,\n // NOTE: `gen_ai.operation.name` is deliberately NOT set on the\n // root span. The root represents a `chat()` invocation that may\n // span multiple model calls; only iteration spans correspond to\n // a single chat operation. Backends that map `operation.name=chat`\n // to a \"generation\" event (e.g. PostHog LLM Analytics) would\n // otherwise emit a duplicate generation for the wrapper span.\n },\n }\n const spanOptions =\n safeCall('otel.onBeforeSpanStart', () =>\n onBeforeSpanStart?.(info, baseOptions),\n ) ?? baseOptions\n const rootSpan = tracer.startSpan(name, spanOptions)\n\n const enriched = safeCall('otel.attributeEnricher', () =>\n attributeEnricher?.(info),\n )\n if (enriched) rootSpan.setAttributes(enriched)\n\n stateByCtx.set(ctx, {\n rootSpan,\n currentIterationSpan: null,\n toolSpans: new Map(),\n iterationCount: 0,\n assistantTextBuffer: '',\n assistantTextBufferTruncated: false,\n startTime: Date.now(),\n })\n })\n },\n\n onConfig(ctx, config) {\n if (ctx.phase !== 'beforeModel') return\n safeCall('otel.onConfig', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n\n // The previous iteration's span stays open through tool execution and\n // onUsage so that tool spans nest under it and token attributes land\n // on it. Close it here, just before opening the next iteration.\n closeIterationSpan(state, ctx)\n\n const info: OtelSpanInfo<'iteration'> = {\n kind: 'iteration',\n ctx,\n iteration: ctx.iteration,\n }\n const name =\n safeCall('otel.spanNameFormatter', () => spanNameFormatter?.(info)) ??\n `chat ${ctx.model} #${ctx.iteration}`\n\n const baseAttrs: Record<string, AttributeValue> = {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': ctx.model,\n 'tanstack.ai.iteration': ctx.iteration,\n }\n // Sampling options now live in provider-native `modelOptions`, and\n // providers spell them differently (e.g. `max_output_tokens`,\n // `max_completion_tokens`, `maxOutputTokens`, `num_predict`). Read the\n // first numeric value among the known spellings — including Ollama's\n // nested `options` — so gen_ai attributes populate across providers.\n const sampling = config.modelOptions ?? {}\n const nestedOptions =\n sampling['options'] && typeof sampling['options'] === 'object'\n ? (sampling['options'] as Record<string, unknown>)\n : undefined\n const samplingTemperature = firstNumber(\n sampling['temperature'],\n nestedOptions?.['temperature'],\n )\n const samplingTopP = firstNumber(\n sampling['top_p'],\n sampling['topP'],\n nestedOptions?.['top_p'],\n )\n // Spellings come from the shared `MAX_TOKENS_KEYS` table so this stays\n // in lockstep with the summarize wrapper's caller-limit detection.\n const samplingMaxTokens = firstNumber(\n ...MAX_TOKENS_KEYS.map((k) => sampling[k]),\n nestedOptions?.[NESTED_MAX_TOKENS_KEY],\n )\n if (samplingTemperature !== undefined)\n baseAttrs['gen_ai.request.temperature'] = samplingTemperature\n if (samplingTopP !== undefined)\n baseAttrs['gen_ai.request.top_p'] = samplingTopP\n if (samplingMaxTokens !== undefined)\n baseAttrs['gen_ai.request.max_tokens'] = samplingMaxTokens\n\n const baseOptions: SpanOptions = {\n kind: SpanKind.CLIENT,\n attributes: baseAttrs,\n }\n const spanOptions =\n safeCall('otel.onBeforeSpanStart', () =>\n onBeforeSpanStart?.(info, baseOptions),\n ) ?? baseOptions\n\n const parentCtx = otelTrace.setSpan(\n otelContext.active(),\n state.rootSpan,\n )\n let iterSpan!: Span\n otelContext.with(parentCtx, () => {\n // Pass the parent context explicitly as the 3rd arg — this is a\n // real-OTel-compatible way to ensure the span is parented to\n // `rootSpan` even when the host app has not registered a context\n // manager (e.g. in tests or minimal setups).\n iterSpan = tracer.startSpan(name, spanOptions, parentCtx)\n })\n\n const enriched = safeCall('otel.attributeEnricher', () =>\n attributeEnricher?.(info),\n )\n if (enriched) iterSpan.setAttributes(enriched)\n\n state.currentIterationSpan = iterSpan\n state.assistantTextBuffer = ''\n state.assistantTextBufferTruncated = false\n\n if (captureContent) {\n const systemPromptContents = config.systemPrompts.map((p) =>\n typeof p === 'string' ? p : p.content,\n )\n // Anthropic prompt-caching users need to know which prompt carried\n // `cache_control`: it's the one attribute that explains cache\n // hit/miss in observability. Serialise per-prompt metadata as a\n // single JSON span attribute so backends that don't understand\n // GenAI events can still surface it. Kept off span events to\n // avoid breaking the one-event-per-message GenAI semconv contract.\n const systemPromptMetadata = config.systemPrompts.map((p) =>\n typeof p === 'string' || p.metadata === undefined\n ? null\n : p.metadata,\n )\n if (systemPromptMetadata.some((m) => m !== null)) {\n iterSpan.setAttribute(\n 'tanstack.ai.system_prompt.metadata',\n JSON.stringify(systemPromptMetadata),\n )\n }\n // Span events follow the original GenAI semconv (one event per\n // message). Backends that read events get content this way.\n for (const sys of systemPromptContents) {\n iterSpan.addEvent('gen_ai.system.message', {\n content: redactContent(sys),\n })\n }\n for (const m of config.messages) {\n const body = serializeContent(m.content)\n if (body.length === 0) continue\n iterSpan.addEvent(messageEventName(m.role), {\n content: redactContent(body),\n })\n }\n\n // Also emit the current GenAI-semconv attribute form\n // (`gen_ai.input.messages`) — backends like PostHog read prompt\n // content from this attribute, not from span events.\n const inputMessages: Array<{ role: string; content: string }> = []\n for (const sys of systemPromptContents) {\n inputMessages.push({\n role: 'system',\n content: redactContent(sys),\n })\n }\n for (const m of config.messages) {\n const body = serializeContent(m.content)\n if (body.length === 0) continue\n inputMessages.push({\n role: m.role,\n content: redactContent(body),\n })\n }\n if (inputMessages.length > 0) {\n const inputJson = JSON.stringify(inputMessages)\n // Current OTel GenAI semconv — Sentry / PostHog / Datadog read\n // prompt content from this attribute.\n iterSpan.setAttribute('gen_ai.input.messages', inputJson)\n // Langfuse-native attribute. Highest priority in Langfuse's OTLP\n // ingestion (checked before events and gen_ai.input.messages) so\n // the Input panel populates reliably. Harmless to other backends —\n // the attribute is namespaced and unrecognised keys are ignored.\n iterSpan.setAttribute('langfuse.observation.input', inputJson)\n\n // Mirror the first iteration's input onto the root span and at\n // trace level so Langfuse fills Input on the trace card and the\n // chat-level observation. Later iterations append tool-call /\n // assistant messages that are useful per-iteration but noise at\n // the chat / trace level.\n if (state.iterationCount === 0) {\n state.rootSpan.setAttribute(\n 'langfuse.observation.input',\n inputJson,\n )\n state.rootSpan.setAttribute('langfuse.trace.input', inputJson)\n }\n }\n }\n\n state.iterationCount += 1\n })\n return undefined\n },\n\n onChunk(ctx, chunk) {\n safeCall('otel.onChunk', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n\n if (captureContent && chunk.type === 'TEXT_MESSAGE_CONTENT') {\n appendAssistantText(state, chunk.delta)\n }\n\n if (chunk.type !== 'RUN_FINISHED') return\n const span = state.currentIterationSpan\n if (!span) return\n\n if (chunk.finishReason) {\n span.setAttribute('gen_ai.response.finish_reasons', [\n chunk.finishReason,\n ])\n }\n if (chunk.model) span.setAttribute('gen_ai.response.model', chunk.model)\n\n // Set usage attributes on the iteration span directly from the chunk\n // so they're available before `onUsage` fires. Histogram recording is\n // deliberately NOT done here — the chat runner always invokes\n // `runOnUsage` when `chunk.usage` is present, and `onUsage` is the\n // canonical place for the metric. Recording in both would double-count.\n if (chunk.usage) {\n span.setAttributes({\n 'gen_ai.usage.input_tokens': chunk.usage.promptTokens,\n 'gen_ai.usage.output_tokens': chunk.usage.completionTokens,\n })\n }\n\n if (captureContent && state.assistantTextBuffer.length > 0) {\n const completion = redactContent(state.assistantTextBuffer)\n const outputJson = JSON.stringify([\n { role: 'assistant', content: completion },\n ])\n // Event form (older semconv) — kept for backends that consume it.\n span.addEvent('gen_ai.choice', { content: completion })\n // Attribute form (current semconv) — required by backends like\n // PostHog that read completion content from `gen_ai.output.messages`.\n span.setAttribute('gen_ai.output.messages', outputJson)\n // Langfuse-native attribute (highest priority in Langfuse mapping).\n span.setAttribute('langfuse.observation.output', outputJson)\n // Mirror to the root span and trace card. Each iteration overwrites,\n // so the final iteration's completion lands on the root — which is\n // the final answer the user saw, not an intermediate tool-call turn.\n state.rootSpan.setAttribute('langfuse.observation.output', outputJson)\n state.rootSpan.setAttribute('langfuse.trace.output', outputJson)\n state.assistantTextBuffer = ''\n state.assistantTextBufferTruncated = false\n }\n\n // Intentionally leave the iteration span open: tool spans started\n // after `RUN_FINISHED` (tool_calls finishReason) must nest under it,\n // and `onUsage` may still fire. The span is closed in `onConfig` when\n // the next iteration starts, or in `onFinish` / `onError` / `onAbort`.\n })\n return undefined\n },\n\n onUsage(ctx, usage) {\n safeCall('otel.onUsage', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n\n // Always record the token histogram — metrics don't depend on having\n // an iteration span, and skipping here would drop metric data if an\n // adapter emits `onUsage` outside the iteration window.\n if (tokenHistogram) {\n const metricAttrs = {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': ctx.model,\n }\n tokenHistogram.record(usage.promptTokens, {\n ...metricAttrs,\n 'gen_ai.token.type': 'input',\n })\n tokenHistogram.record(usage.completionTokens, {\n ...metricAttrs,\n 'gen_ai.token.type': 'output',\n })\n }\n\n const span = state.currentIterationSpan ?? state.rootSpan\n span.setAttributes({\n 'gen_ai.usage.input_tokens': usage.promptTokens,\n 'gen_ai.usage.output_tokens': usage.completionTokens,\n })\n })\n },\n\n onBeforeToolCall(ctx, hookCtx) {\n safeCall('otel.onBeforeToolCall', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n const parent = state.currentIterationSpan ?? state.rootSpan\n\n const info: OtelSpanInfo<'tool'> = {\n kind: 'tool',\n ctx,\n toolName: hookCtx.toolName,\n toolCallId: hookCtx.toolCallId,\n iteration: state.iterationCount - 1,\n }\n const name =\n safeCall('otel.spanNameFormatter', () => spanNameFormatter?.(info)) ??\n `execute_tool ${hookCtx.toolName}`\n\n const baseAttrs: Record<string, AttributeValue> = {\n 'gen_ai.tool.name': hookCtx.toolName,\n 'gen_ai.tool.call.id': hookCtx.toolCallId,\n 'gen_ai.tool.type': 'function',\n }\n const baseOptions: SpanOptions = {\n kind: SpanKind.INTERNAL,\n attributes: baseAttrs,\n }\n const spanOptions =\n safeCall('otel.onBeforeSpanStart', () =>\n onBeforeSpanStart?.(info, baseOptions),\n ) ?? baseOptions\n\n const parentCtx = otelTrace.setSpan(otelContext.active(), parent)\n let toolSpan!: Span\n otelContext.with(parentCtx, () => {\n toolSpan = tracer.startSpan(name, spanOptions, parentCtx)\n })\n\n const enriched = safeCall('otel.attributeEnricher', () =>\n attributeEnricher?.(info),\n )\n if (enriched) toolSpan.setAttributes(enriched)\n\n // Stamp the tool args onto the tool span so backends that render an\n // input panel per span (e.g. PostHog) have something to show.\n if (captureContent) {\n const argsBody =\n typeof hookCtx.args === 'string'\n ? hookCtx.args\n : (safeCall('otel.serializeToolArgs', () =>\n JSON.stringify(hookCtx.args ?? null),\n ) ?? '[unserializable_tool_args]')\n const redactedArgs = redactContent(argsBody)\n const toolInputJson = JSON.stringify([\n { role: 'tool', content: redactedArgs },\n ])\n toolSpan.setAttribute('gen_ai.input.messages', toolInputJson)\n // Langfuse-native (highest priority in Langfuse mapping).\n toolSpan.setAttribute('langfuse.observation.input', toolInputJson)\n }\n\n state.toolSpans.set(hookCtx.toolCallId, {\n span: toolSpan,\n toolName: hookCtx.toolName,\n })\n })\n return undefined\n },\n\n onAfterToolCall(ctx, info) {\n safeCall('otel.onAfterToolCall', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n const entry = state.toolSpans.get(info.toolCallId)\n if (!entry) return\n const { span: toolSpan } = entry\n\n const outcome = info.ok ? 'success' : 'error'\n toolSpan.setAttribute('tanstack.ai.tool.outcome', outcome)\n\n if (!info.ok && info.error !== undefined) {\n toolSpan.recordException(info.error as Exception)\n const msg = errorMessage(info.error)\n toolSpan.setStatus({\n code: SpanStatusCode.ERROR,\n ...(msg !== undefined && { message: msg }),\n })\n }\n\n if (captureContent) {\n // Serialization can throw on circular refs or `BigInt` values. If it\n // does, fall back to a sentinel so the rest of this handler (span\n // end, onSpanEnd, toolSpans cleanup) still runs — otherwise the tool\n // span would dangle until the onFinish/onError sweep.\n const body =\n typeof info.result === 'string'\n ? info.result\n : (safeCall('otel.serializeToolResult', () =>\n JSON.stringify(info.result ?? null),\n ) ?? '[unserializable_tool_result]')\n const redactedBody = redactContent(body)\n if (state.currentIterationSpan) {\n state.currentIterationSpan.addEvent('gen_ai.tool.message', {\n content: redactedBody,\n tool_call_id: info.toolCallId,\n })\n }\n // Output panel of the tool span itself — `gen_ai.output.messages` is\n // what current GenAI semconv consumers (e.g. PostHog) read.\n const toolOutputJson = JSON.stringify([\n { role: 'tool', content: redactedBody },\n ])\n toolSpan.setAttribute('gen_ai.output.messages', toolOutputJson)\n // Langfuse-native (highest priority in Langfuse mapping).\n toolSpan.setAttribute('langfuse.observation.output', toolOutputJson)\n }\n\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'tool',\n ctx,\n toolName: info.toolName,\n toolCallId: info.toolCallId,\n iteration: state.iterationCount - 1,\n },\n toolSpan,\n ),\n )\n toolSpan.end()\n state.toolSpans.delete(info.toolCallId)\n })\n },\n\n onError(ctx, info) {\n safeCall('otel.onError', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n\n const errType = errorTypeName(info.error)\n const message = errorMessage(info.error)\n const statusMessage =\n message !== undefined ? { message } : ({} as const)\n const exception = info.error as Exception\n\n const iterationSpan = state.currentIterationSpan\n if (iterationSpan) {\n iterationSpan.recordException(exception)\n iterationSpan.setStatus({\n code: SpanStatusCode.ERROR,\n ...statusMessage,\n })\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'iteration',\n ctx,\n iteration: state.iterationCount - 1,\n },\n iterationSpan,\n ),\n )\n iterationSpan.end()\n state.currentIterationSpan = null\n }\n\n for (const [id, entry] of state.toolSpans) {\n const { span, toolName } = entry\n span.recordException(exception)\n span.setStatus({ code: SpanStatusCode.ERROR, ...statusMessage })\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'tool',\n ctx,\n toolCallId: id,\n toolName,\n iteration: state.iterationCount - 1,\n },\n span,\n ),\n )\n span.end()\n state.toolSpans.delete(id)\n }\n\n state.rootSpan.recordException(exception)\n state.rootSpan.setStatus({\n code: SpanStatusCode.ERROR,\n ...statusMessage,\n })\n\n if (durationHistogram) {\n durationHistogram.record(info.duration / 1000, {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': ctx.model,\n 'error.type': errType,\n })\n }\n\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'chat', ctx }, state.rootSpan),\n )\n state.rootSpan.end()\n stateByCtx.delete(ctx)\n })\n },\n\n onAbort(ctx, info) {\n safeCall('otel.onAbort', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n\n const closeCancelled = (span: Span): void => {\n // `gen_ai.completion.reason` is not part of the GenAI semconv; use a\n // TanStack-namespaced attribute so downstream exporters don't treat\n // it as standard. The span status still carries the error code.\n span.setAttribute('tanstack.ai.completion.reason', 'cancelled')\n span.setStatus({ code: SpanStatusCode.ERROR, message: 'cancelled' })\n }\n\n const iterationSpan = state.currentIterationSpan\n if (iterationSpan) {\n closeCancelled(iterationSpan)\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'iteration',\n ctx,\n iteration: state.iterationCount - 1,\n },\n iterationSpan,\n ),\n )\n iterationSpan.end()\n state.currentIterationSpan = null\n }\n for (const [id, entry] of state.toolSpans) {\n const { span, toolName } = entry\n closeCancelled(span)\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'tool',\n ctx,\n toolCallId: id,\n toolName,\n iteration: state.iterationCount - 1,\n },\n span,\n ),\n )\n span.end()\n state.toolSpans.delete(id)\n }\n closeCancelled(state.rootSpan)\n\n if (durationHistogram) {\n durationHistogram.record(info.duration / 1000, {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': ctx.model,\n 'error.type': 'cancelled',\n })\n }\n\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'chat', ctx }, state.rootSpan),\n )\n state.rootSpan.end()\n stateByCtx.delete(ctx)\n })\n },\n\n onFinish(ctx, info) {\n safeCall('otel.onFinish', () => {\n const state = stateByCtx.get(ctx)\n if (!state) return\n\n // Close any tool spans that never received `onAfterToolCall` (adapter\n // quirk). Done before the iteration span so the hierarchy is closed\n // in depth-first order.\n for (const [id, entry] of state.toolSpans) {\n const { span, toolName } = entry\n span.setAttribute('tanstack.ai.tool.outcome', 'unknown')\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.(\n {\n kind: 'tool',\n ctx,\n toolCallId: id,\n toolName,\n iteration: state.iterationCount - 1,\n },\n span,\n ),\n )\n span.end()\n state.toolSpans.delete(id)\n }\n\n // The final iteration's span is still open because we keep it open\n // through tool execution and `onUsage`. Close it now.\n closeIterationSpan(state, ctx)\n\n if (durationHistogram) {\n durationHistogram.record(info.duration / 1000, {\n 'gen_ai.system': ctx.provider,\n 'gen_ai.operation.name': 'chat',\n 'gen_ai.request.model': ctx.model,\n })\n }\n\n if (info.usage) {\n state.rootSpan.setAttributes({\n 'gen_ai.usage.input_tokens': info.usage.promptTokens,\n 'gen_ai.usage.output_tokens': info.usage.completionTokens,\n })\n }\n if (info.finishReason) {\n state.rootSpan.setAttribute('gen_ai.response.finish_reasons', [\n info.finishReason,\n ])\n }\n state.rootSpan.setAttribute(\n 'tanstack.ai.iterations',\n state.iterationCount,\n )\n\n safeCall('otel.onSpanEnd', () =>\n onSpanEnd?.({ kind: 'chat', ctx }, state.rootSpan),\n )\n state.rootSpan.end()\n stateByCtx.delete(ctx)\n })\n },\n }\n}\n"],"names":["otelTrace","otelContext"],"mappings":";;AA6GA,MAAM,iCAAiB,QAAA;AAEvB,MAAM,6BAA6B;AACnC,MAAM,4BAA4B;AAElC,SAAS,iBAAiB,SAA0B;AAClD,MAAI,OAAO,YAAY,SAAU,QAAO;AACxC,MAAI,CAAC,MAAM,QAAQ,OAAO,EAAG,QAAO;AACpC,QAAM,QAAuB,CAAA;AAC7B,aAAW,QAAQ,SAAS;AAC1B,QAAI,CAAC,QAAQ,OAAO,SAAS,SAAU;AACvC,UAAM,OAAQ,KAA2B;AACzC,YAAQ,MAAA;AAAA,MACN,KAAK;AACH,cAAM;AAAA,WAED,KAA2B,QAC3B,KAA8B,WAC/B,IACA,SAAA;AAAA,QAAS;AAEb;AAAA,MACF,KAAK;AACH,cAAM,KAAK,SAAS;AACpB;AAAA,MACF,KAAK;AACH,cAAM,KAAK,SAAS;AACpB;AAAA,MACF,KAAK;AACH,cAAM,KAAK,SAAS;AACpB;AAAA,MACF,KAAK;AACH,cAAM,KAAK,YAAY;AACvB;AAAA,MACF,KAAK;AACH,cAAM,KAAK,WAAW;AACtB;AAAA,MACF;AACE,cAAM,KAAK,IAAI,IAAI,GAAG;AAAA,IAAA;AAAA,EAE5B;AACA,SAAO,MAAM,KAAK,GAAG;AACvB;AAEA,SAAS,iBAAiB,MAAsB;AAC9C,UAAQ,MAAA;AAAA,IACN,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT,KAAK;AACH,aAAO;AAAA,IACT;AACE,aAAO,UAAU,IAAI;AAAA,EAAA;AAE3B;AAMA,SAAS,eAAe,YAAgD;AACtE,aAAW,aAAa,YAAY;AAClC,QAAI,OAAO,cAAc,YAAY,OAAO,SAAS,SAAS,GAAG;AAC/D,aAAO;AAAA,IACT;AAAA,EACF;AACA,SAAO;AACT;AAEA,SAAS,aAAa,KAAkC;AACtD,MAAI,eAAe,MAAO,QAAO,IAAI;AACrC,MAAI,OAAO,QAAQ,SAAU,QAAO;AACpC,MAAI,OAAO,OAAO,QAAQ,YAAY,aAAa,KAAK;AACtD,UAAM,IAAK,IAA8B;AACzC,QAAI,OAAO,MAAM,SAAU,QAAO;AAAA,EACpC;AACA,SAAO;AACT;AAEA,SAAS,cAAc,KAAsB;AAC3C,MAAI,eAAe,MAAO,QAAO,IAAI,QAAQ;AAC7C,MAAI,OAAO,OAAO,QAAQ,YAAY,UAAU,KAAK;AACnD,UAAM,IAAK,IAA2B;AACtC,QAAI,OAAO,MAAM,SAAU,QAAO;AAAA,EACpC;AACA,SAAO;AACT;AAEA,SAAS,SAAY,OAAe,IAA4B;AAC9D,MAAI;AACF,WAAO,GAAA;AAAA,EACT,SAAS,KAAK;AAIZ,YAAQ,KAAK,oBAAoB,KAAK,WAAW,GAAG;AACpD,WAAO;AAAA,EACT;AACF;AAEO,SAAS,eAAe,SAAgD;AAC7E,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA,iBAAiB;AAAA,IACjB,SAAS,CAAC,MAAM;AAAA,IAChB,mBAAmB;AAAA,IACnB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA,IACE;AAEJ,QAAM,oBAAoB,OAAO;AAAA,IAC/B;AAAA,IACA;AAAA,MACE,aAAa;AAAA,MACb,MAAM;AAAA,IAAA;AAAA,EACR;AAEF,QAAM,iBAAiB,OAAO,gBAAgB,6BAA6B;AAAA,IACzE,aAAa;AAAA,IACb,MAAM;AAAA,EAAA,CACP;AAKD,QAAM,gBAAgB,CAAC,SAAyB;AAC9C,QAAI;AACF,aAAO,OAAO,IAAI;AAAA,IACpB,SAAS,KAAK;AACZ,cAAQ,KAAK,uCAAuC,GAAG;AACvD,aAAO;AAAA,IACT;AAAA,EACF;AAEA,QAAM,sBAAsB,CAAC,OAAqB,UAAwB;AACxE,QAAI,mBAAmB,GAAG;AACxB,UAAI,MAAM,6BAA8B;AACxC,YAAM,YAAY,mBAAmB,MAAM,oBAAoB;AAC/D,UAAI,aAAa,GAAG;AAClB,cAAM,+BAA+B;AACrC,cAAM,uBAAuB;AAC7B;AAAA,MACF;AACA,UAAI,MAAM,SAAS,WAAW;AAC5B,cAAM,uBAAuB,MAAM,MAAM,GAAG,SAAS,IAAI;AACzD,cAAM,+BAA+B;AACrC;AAAA,MACF;AAAA,IACF;AACA,UAAM,uBAAuB;AAAA,EAC/B;AAEA,QAAM,qBAAqB,CACzB,OACA,QACS;AACT,QAAI,CAAC,MAAM,qBAAsB;AACjC,UAAM,OAAO,MAAM;AACnB,UAAM,YAAY,MAAM,iBAAiB;AACzC;AAAA,MAAS;AAAA,MAAkB,MACzB,YAAY,EAAE,MAAM,aAAa,KAAK,UAAA,GAAa,IAAI;AAAA,IAAA;AAEzD,SAAK,IAAA;AACL,UAAM,uBAAuB;AAAA,EAC/B;AAEA,SAAO;AAAA,IACL,MAAM;AAAA,IAEN,QAAQ,KAAK;AACX,eAAS,gBAAgB,MAAM;AAC7B,cAAM,OAA6B,EAAE,MAAM,QAAQ,IAAA;AACnD,cAAM,OACJ,SAAS,0BAA0B,MAAM,oBAAoB,IAAI,CAAC,KAClE,QAAQ,IAAI,KAAK;AACnB,cAAM,cAA2B;AAAA,UAC/B,MAAM,SAAS;AAAA,UACf,YAAY;AAAA,YACV,iBAAiB,IAAI;AAAA,YACrB,wBAAwB,IAAI;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,UAAA;AAAA,QAO9B;AAEF,cAAM,cACJ;AAAA,UAAS;AAAA,UAA0B,MACjC,oBAAoB,MAAM,WAAW;AAAA,QAAA,KAClC;AACP,cAAM,WAAW,OAAO,UAAU,MAAM,WAAW;AAEnD,cAAM,WAAW;AAAA,UAAS;AAAA,UAA0B,MAClD,oBAAoB,IAAI;AAAA,QAAA;AAE1B,YAAI,SAAU,UAAS,cAAc,QAAQ;AAE7C,mBAAW,IAAI,KAAK;AAAA,UAClB;AAAA,UACA,sBAAsB;AAAA,UACtB,+BAAe,IAAA;AAAA,UACf,gBAAgB;AAAA,UAChB,qBAAqB;AAAA,UACrB,8BAA8B;AAAA,UAC9B,WAAW,KAAK,IAAA;AAAA,QAAI,CACrB;AAAA,MACH,CAAC;AAAA,IACH;AAAA,IAEA,SAAS,KAAK,QAAQ;AACpB,UAAI,IAAI,UAAU,cAAe;AACjC,eAAS,iBAAiB,MAAM;AAC9B,cAAM,QAAQ,WAAW,IAAI,GAAG;AAChC,YAAI,CAAC,MAAO;AAKZ,2BAAmB,OAAO,GAAG;AAE7B,cAAM,OAAkC;AAAA,UACtC,MAAM;AAAA,UACN;AAAA,UACA,WAAW,IAAI;AAAA,QAAA;AAEjB,cAAM,OACJ,SAAS,0BAA0B,MAAM,oBAAoB,IAAI,CAAC,KAClE,QAAQ,IAAI,KAAK,KAAK,IAAI,SAAS;AAErC,cAAM,YAA4C;AAAA,UAChD,iBAAiB,IAAI;AAAA,UACrB,yBAAyB;AAAA,UACzB,wBAAwB,IAAI;AAAA,UAC5B,yBAAyB,IAAI;AAAA,QAAA;AAO/B,cAAM,WAAW,OAAO,gBAAgB,CAAA;AACxC,cAAM,gBACJ,SAAS,SAAS,KAAK,OAAO,SAAS,SAAS,MAAM,WACjD,SAAS,SAAS,IACnB;AACN,cAAM,sBAAsB;AAAA,UAC1B,SAAS,aAAa;AAAA,UACtB,gBAAgB,aAAa;AAAA,QAAA;AAE/B,cAAM,eAAe;AAAA,UACnB,SAAS,OAAO;AAAA,UAChB,SAAS,MAAM;AAAA,UACf,gBAAgB,OAAO;AAAA,QAAA;AAIzB,cAAM,oBAAoB;AAAA,UACxB,GAAG,gBAAgB,IAAI,CAAC,MAAM,SAAS,CAAC,CAAC;AAAA,UACzC,gBAAgB,qBAAqB;AAAA,QAAA;AAEvC,YAAI,wBAAwB;AAC1B,oBAAU,4BAA4B,IAAI;AAC5C,YAAI,iBAAiB;AACnB,oBAAU,sBAAsB,IAAI;AACtC,YAAI,sBAAsB;AACxB,oBAAU,2BAA2B,IAAI;AAE3C,cAAM,cAA2B;AAAA,UAC/B,MAAM,SAAS;AAAA,UACf,YAAY;AAAA,QAAA;AAEd,cAAM,cACJ;AAAA,UAAS;AAAA,UAA0B,MACjC,oBAAoB,MAAM,WAAW;AAAA,QAAA,KAClC;AAEP,cAAM,YAAYA,MAAU;AAAA,UAC1BC,QAAY,OAAA;AAAA,UACZ,MAAM;AAAA,QAAA;AAER,YAAI;AACJA,gBAAY,KAAK,WAAW,MAAM;AAKhC,qBAAW,OAAO,UAAU,MAAM,aAAa,SAAS;AAAA,QAC1D,CAAC;AAED,cAAM,WAAW;AAAA,UAAS;AAAA,UAA0B,MAClD,oBAAoB,IAAI;AAAA,QAAA;AAE1B,YAAI,SAAU,UAAS,cAAc,QAAQ;AAE7C,cAAM,uBAAuB;AAC7B,cAAM,sBAAsB;AAC5B,cAAM,+BAA+B;AAErC,YAAI,gBAAgB;AAClB,gBAAM,uBAAuB,OAAO,cAAc;AAAA,YAAI,CAAC,MACrD,OAAO,MAAM,WAAW,IAAI,EAAE;AAAA,UAAA;AAQhC,gBAAM,uBAAuB,OAAO,cAAc;AAAA,YAAI,CAAC,MACrD,OAAO,MAAM,YAAY,EAAE,aAAa,SACpC,OACA,EAAE;AAAA,UAAA;AAER,cAAI,qBAAqB,KAAK,CAAC,MAAM,MAAM,IAAI,GAAG;AAChD,qBAAS;AAAA,cACP;AAAA,cACA,KAAK,UAAU,oBAAoB;AAAA,YAAA;AAAA,UAEvC;AAGA,qBAAW,OAAO,sBAAsB;AACtC,qBAAS,SAAS,yBAAyB;AAAA,cACzC,SAAS,cAAc,GAAG;AAAA,YAAA,CAC3B;AAAA,UACH;AACA,qBAAW,KAAK,OAAO,UAAU;AAC/B,kBAAM,OAAO,iBAAiB,EAAE,OAAO;AACvC,gBAAI,KAAK,WAAW,EAAG;AACvB,qBAAS,SAAS,iBAAiB,EAAE,IAAI,GAAG;AAAA,cAC1C,SAAS,cAAc,IAAI;AAAA,YAAA,CAC5B;AAAA,UACH;AAKA,gBAAM,gBAA0D,CAAA;AAChE,qBAAW,OAAO,sBAAsB;AACtC,0BAAc,KAAK;AAAA,cACjB,MAAM;AAAA,cACN,SAAS,cAAc,GAAG;AAAA,YAAA,CAC3B;AAAA,UACH;AACA,qBAAW,KAAK,OAAO,UAAU;AAC/B,kBAAM,OAAO,iBAAiB,EAAE,OAAO;AACvC,gBAAI,KAAK,WAAW,EAAG;AACvB,0BAAc,KAAK;AAAA,cACjB,MAAM,EAAE;AAAA,cACR,SAAS,cAAc,IAAI;AAAA,YAAA,CAC5B;AAAA,UACH;AACA,cAAI,cAAc,SAAS,GAAG;AAC5B,kBAAM,YAAY,KAAK,UAAU,aAAa;AAG9C,qBAAS,aAAa,yBAAyB,SAAS;AAKxD,qBAAS,aAAa,8BAA8B,SAAS;AAO7D,gBAAI,MAAM,mBAAmB,GAAG;AAC9B,oBAAM,SAAS;AAAA,gBACb;AAAA,gBACA;AAAA,cAAA;AAEF,oBAAM,SAAS,aAAa,wBAAwB,SAAS;AAAA,YAC/D;AAAA,UACF;AAAA,QACF;AAEA,cAAM,kBAAkB;AAAA,MAC1B,CAAC;AACD,aAAO;AAAA,IACT;AAAA,IAEA,QAAQ,KAAK,OAAO;AAClB,eAAS,gBAAgB,MAAM;AAC7B,cAAM,QAAQ,WAAW,IAAI,GAAG;AAChC,YAAI,CAAC,MAAO;AAEZ,YAAI,kBAAkB,MAAM,SAAS,wBAAwB;AAC3D,8BAAoB,OAAO,MAAM,KAAK;AAAA,QACxC;AAEA,YAAI,MAAM,SAAS,eAAgB;AACnC,cAAM,OAAO,MAAM;AACnB,YAAI,CAAC,KAAM;AAEX,YAAI,MAAM,cAAc;AACtB,eAAK,aAAa,kCAAkC;AAAA,YAClD,MAAM;AAAA,UAAA,CACP;AAAA,QACH;AACA,YAAI,MAAM,MAAO,MAAK,aAAa,yBAAyB,MAAM,KAAK;AAOvE,YAAI,MAAM,OAAO;AACf,eAAK,cAAc;AAAA,YACjB,6BAA6B,MAAM,MAAM;AAAA,YACzC,8BAA8B,MAAM,MAAM;AAAA,UAAA,CAC3C;AAAA,QACH;AAEA,YAAI,kBAAkB,MAAM,oBAAoB,SAAS,GAAG;AAC1D,gBAAM,aAAa,cAAc,MAAM,mBAAmB;AAC1D,gBAAM,aAAa,KAAK,UAAU;AAAA,YAChC,EAAE,MAAM,aAAa,SAAS,WAAA;AAAA,UAAW,CAC1C;AAED,eAAK,SAAS,iBAAiB,EAAE,SAAS,YAAY;AAGtD,eAAK,aAAa,0BAA0B,UAAU;AAEtD,eAAK,aAAa,+BAA+B,UAAU;AAI3D,gBAAM,SAAS,aAAa,+BAA+B,UAAU;AACrE,gBAAM,SAAS,aAAa,yBAAyB,UAAU;AAC/D,gBAAM,sBAAsB;AAC5B,gBAAM,+BAA+B;AAAA,QACvC;AAAA,MAMF,CAAC;AACD,aAAO;AAAA,IACT;AAAA,IAEA,QAAQ,KAAK,OAAO;AAClB,eAAS,gBAAgB,MAAM;AAC7B,cAAM,QAAQ,WAAW,IAAI,GAAG;AAChC,YAAI,CAAC,MAAO;AAKZ,YAAI,gBAAgB;AAClB,gBAAM,cAAc;AAAA,YAClB,iBAAiB,IAAI;AAAA,YACrB,yBAAyB;AAAA,YACzB,wBAAwB,IAAI;AAAA,UAAA;AAE9B,yBAAe,OAAO,MAAM,cAAc;AAAA,YACxC,GAAG;AAAA,YACH,qBAAqB;AAAA,UAAA,CACtB;AACD,yBAAe,OAAO,MAAM,kBAAkB;AAAA,YAC5C,GAAG;AAAA,YACH,qBAAqB;AAAA,UAAA,CACtB;AAAA,QACH;AAEA,cAAM,OAAO,MAAM,wBAAwB,MAAM;AACjD,aAAK,cAAc;AAAA,UACjB,6BAA6B,MAAM;AAAA,UACnC,8BAA8B,MAAM;AAAA,QAAA,CACrC;AAAA,MACH,CAAC;AAAA,IACH;AAAA,IAEA,iBAAiB,KAAK,SAAS;AAC7B,eAAS,yBAAyB,MAAM;AACtC,cAAM,QAAQ,WAAW,IAAI,GAAG;AAChC,YAAI,CAAC,MAAO;AACZ,cAAM,SAAS,MAAM,wBAAwB,MAAM;AAEnD,cAAM,OAA6B;AAAA,UACjC,MAAM;AAAA,UACN;AAAA,UACA,UAAU,QAAQ;AAAA,UAClB,YAAY,QAAQ;AAAA,UACpB,WAAW,MAAM,iBAAiB;AAAA,QAAA;AAEpC,cAAM,OACJ,SAAS,0BAA0B,MAAM,oBAAoB,IAAI,CAAC,KAClE,gBAAgB,QAAQ,QAAQ;AAElC,cAAM,YAA4C;AAAA,UAChD,oBAAoB,QAAQ;AAAA,UAC5B,uBAAuB,QAAQ;AAAA,UAC/B,oBAAoB;AAAA,QAAA;AAEtB,cAAM,cAA2B;AAAA,UAC/B,MAAM,SAAS;AAAA,UACf,YAAY;AAAA,QAAA;AAEd,cAAM,cACJ;AAAA,UAAS;AAAA,UAA0B,MACjC,oBAAoB,MAAM,WAAW;AAAA,QAAA,KAClC;AAEP,cAAM,YAAYD,MAAU,QAAQC,QAAY,OAAA,GAAU,MAAM;AAChE,YAAI;AACJA,gBAAY,KAAK,WAAW,MAAM;AAChC,qBAAW,OAAO,UAAU,MAAM,aAAa,SAAS;AAAA,QAC1D,CAAC;AAED,cAAM,WAAW;AAAA,UAAS;AAAA,UAA0B,MAClD,oBAAoB,IAAI;AAAA,QAAA;AAE1B,YAAI,SAAU,UAAS,cAAc,QAAQ;AAI7C,YAAI,gBAAgB;AAClB,gBAAM,WACJ,OAAO,QAAQ,SAAS,WACpB,QAAQ,OACP;AAAA,YAAS;AAAA,YAA0B,MAClC,KAAK,UAAU,QAAQ,QAAQ,IAAI;AAAA,UAAA,KAChC;AACX,gBAAM,eAAe,cAAc,QAAQ;AAC3C,gBAAM,gBAAgB,KAAK,UAAU;AAAA,YACnC,EAAE,MAAM,QAAQ,SAAS,aAAA;AAAA,UAAa,CACvC;AACD,mBAAS,aAAa,yBAAyB,aAAa;AAE5D,mBAAS,aAAa,8BAA8B,aAAa;AAAA,QACnE;AAEA,cAAM,UAAU,IAAI,QAAQ,YAAY;AAAA,UACtC,MAAM;AAAA,UACN,UAAU,QAAQ;AAAA,QAAA,CACnB;AAAA,MACH,CAAC;AACD,aAAO;AAAA,IACT;AAAA,IAEA,gBAAgB,KAAK,MAAM;AACzB,eAAS,wBAAwB,MAAM;AACrC,cAAM,QAAQ,WAAW,IAAI,GAAG;AAChC,YAAI,CAAC,MAAO;AACZ,cAAM,QAAQ,MAAM,UAAU,IAAI,KAAK,UAAU;AACjD,YAAI,CAAC,MAAO;AACZ,cAAM,EAAE,MAAM,SAAA,IAAa;AAE3B,cAAM,UAAU,KAAK,KAAK,YAAY;AACtC,iBAAS,aAAa,4BAA4B,OAAO;AAEzD,YAAI,CAAC,KAAK,MAAM,KAAK,UAAU,QAAW;AACxC,mBAAS,gBAAgB,KAAK,KAAkB;AAChD,gBAAM,MAAM,aAAa,KAAK,KAAK;AACnC,mBAAS,UAAU;AAAA,YACjB,MAAM,eAAe;AAAA,YACrB,GAAI,QAAQ,UAAa,EAAE,SAAS,IAAA;AAAA,UAAI,CACzC;AAAA,QACH;AAEA,YAAI,gBAAgB;AAKlB,gBAAM,OACJ,OAAO,KAAK,WAAW,WACnB,KAAK,SACJ;AAAA,YAAS;AAAA,YAA4B,MACpC,KAAK,UAAU,KAAK,UAAU,IAAI;AAAA,UAAA,KAC/B;AACX,gBAAM,eAAe,cAAc,IAAI;AACvC,cAAI,MAAM,sBAAsB;AAC9B,kBAAM,qBAAqB,SAAS,uBAAuB;AAAA,cACzD,SAAS;AAAA,cACT,cAAc,KAAK;AAAA,YAAA,CACpB;AAAA,UACH;AAGA,gBAAM,iBAAiB,KAAK,UAAU;AAAA,YACpC,EAAE,MAAM,QAAQ,SAAS,aAAA;AAAA,UAAa,CACvC;AACD,mBAAS,aAAa,0BAA0B,cAAc;AAE9D,mBAAS,aAAa,+BAA+B,cAAc;AAAA,QACrE;AAEA;AAAA,UAAS;AAAA,UAAkB,MACzB;AAAA,YACE;AAAA,cACE,MAAM;AAAA,cACN;AAAA,cACA,UAAU,KAAK;AAAA,cACf,YAAY,KAAK;AAAA,cACjB,WAAW,MAAM,iBAAiB;AAAA,YAAA;AAAA,YAEpC;AAAA,UAAA;AAAA,QACF;AAEF,iBAAS,IAAA;AACT,cAAM,UAAU,OAAO,KAAK,UAAU;AAAA,MACxC,CAAC;AAAA,IACH;AAAA,IAEA,QAAQ,KAAK,MAAM;AACjB,eAAS,gBAAgB,MAAM;AAC7B,cAAM,QAAQ,WAAW,IAAI,GAAG;AAChC,YAAI,CAAC,MAAO;AAEZ,cAAM,UAAU,cAAc,KAAK,KAAK;AACxC,cAAM,UAAU,aAAa,KAAK,KAAK;AACvC,cAAM,gBACJ,YAAY,SAAY,EAAE,QAAA,IAAa,CAAA;AACzC,cAAM,YAAY,KAAK;AAEvB,cAAM,gBAAgB,MAAM;AAC5B,YAAI,eAAe;AACjB,wBAAc,gBAAgB,SAAS;AACvC,wBAAc,UAAU;AAAA,YACtB,MAAM,eAAe;AAAA,YACrB,GAAG;AAAA,UAAA,CACJ;AACD;AAAA,YAAS;AAAA,YAAkB,MACzB;AAAA,cACE;AAAA,gBACE,MAAM;AAAA,gBACN;AAAA,gBACA,WAAW,MAAM,iBAAiB;AAAA,cAAA;AAAA,cAEpC;AAAA,YAAA;AAAA,UACF;AAEF,wBAAc,IAAA;AACd,gBAAM,uBAAuB;AAAA,QAC/B;AAEA,mBAAW,CAAC,IAAI,KAAK,KAAK,MAAM,WAAW;AACzC,gBAAM,EAAE,MAAM,SAAA,IAAa;AAC3B,eAAK,gBAAgB,SAAS;AAC9B,eAAK,UAAU,EAAE,MAAM,eAAe,OAAO,GAAG,eAAe;AAC/D;AAAA,YAAS;AAAA,YAAkB,MACzB;AAAA,cACE;AAAA,gBACE,MAAM;AAAA,gBACN;AAAA,gBACA,YAAY;AAAA,gBACZ;AAAA,gBACA,WAAW,MAAM,iBAAiB;AAAA,cAAA;AAAA,cAEpC;AAAA,YAAA;AAAA,UACF;AAEF,eAAK,IAAA;AACL,gBAAM,UAAU,OAAO,EAAE;AAAA,QAC3B;AAEA,cAAM,SAAS,gBAAgB,SAAS;AACxC,cAAM,SAAS,UAAU;AAAA,UACvB,MAAM,eAAe;AAAA,UACrB,GAAG;AAAA,QAAA,CACJ;AAED,YAAI,mBAAmB;AACrB,4BAAkB,OAAO,KAAK,WAAW,KAAM;AAAA,YAC7C,iBAAiB,IAAI;AAAA,YACrB,yBAAyB;AAAA,YACzB,wBAAwB,IAAI;AAAA,YAC5B,cAAc;AAAA,UAAA,CACf;AAAA,QACH;AAEA;AAAA,UAAS;AAAA,UAAkB,MACzB,YAAY,EAAE,MAAM,QAAQ,IAAA,GAAO,MAAM,QAAQ;AAAA,QAAA;AAEnD,cAAM,SAAS,IAAA;AACf,mBAAW,OAAO,GAAG;AAAA,MACvB,CAAC;AAAA,IACH;AAAA,IAEA,QAAQ,KAAK,MAAM;AACjB,eAAS,gBAAgB,MAAM;AAC7B,cAAM,QAAQ,WAAW,IAAI,GAAG;AAChC,YAAI,CAAC,MAAO;AAEZ,cAAM,iBAAiB,CAAC,SAAqB;AAI3C,eAAK,aAAa,iCAAiC,WAAW;AAC9D,eAAK,UAAU,EAAE,MAAM,eAAe,OAAO,SAAS,aAAa;AAAA,QACrE;AAEA,cAAM,gBAAgB,MAAM;AAC5B,YAAI,eAAe;AACjB,yBAAe,aAAa;AAC5B;AAAA,YAAS;AAAA,YAAkB,MACzB;AAAA,cACE;AAAA,gBACE,MAAM;AAAA,gBACN;AAAA,gBACA,WAAW,MAAM,iBAAiB;AAAA,cAAA;AAAA,cAEpC;AAAA,YAAA;AAAA,UACF;AAEF,wBAAc,IAAA;AACd,gBAAM,uBAAuB;AAAA,QAC/B;AACA,mBAAW,CAAC,IAAI,KAAK,KAAK,MAAM,WAAW;AACzC,gBAAM,EAAE,MAAM,SAAA,IAAa;AAC3B,yBAAe,IAAI;AACnB;AAAA,YAAS;AAAA,YAAkB,MACzB;AAAA,cACE;AAAA,gBACE,MAAM;AAAA,gBACN;AAAA,gBACA,YAAY;AAAA,gBACZ;AAAA,gBACA,WAAW,MAAM,iBAAiB;AAAA,cAAA;AAAA,cAEpC;AAAA,YAAA;AAAA,UACF;AAEF,eAAK,IAAA;AACL,gBAAM,UAAU,OAAO,EAAE;AAAA,QAC3B;AACA,uBAAe,MAAM,QAAQ;AAE7B,YAAI,mBAAmB;AACrB,4BAAkB,OAAO,KAAK,WAAW,KAAM;AAAA,YAC7C,iBAAiB,IAAI;AAAA,YACrB,yBAAyB;AAAA,YACzB,wBAAwB,IAAI;AAAA,YAC5B,cAAc;AAAA,UAAA,CACf;AAAA,QACH;AAEA;AAAA,UAAS;AAAA,UAAkB,MACzB,YAAY,EAAE,MAAM,QAAQ,IAAA,GAAO,MAAM,QAAQ;AAAA,QAAA;AAEnD,cAAM,SAAS,IAAA;AACf,mBAAW,OAAO,GAAG;AAAA,MACvB,CAAC;AAAA,IACH;AAAA,IAEA,SAAS,KAAK,MAAM;AAClB,eAAS,iBAAiB,MAAM;AAC9B,cAAM,QAAQ,WAAW,IAAI,GAAG;AAChC,YAAI,CAAC,MAAO;AAKZ,mBAAW,CAAC,IAAI,KAAK,KAAK,MAAM,WAAW;AACzC,gBAAM,EAAE,MAAM,SAAA,IAAa;AAC3B,eAAK,aAAa,4BAA4B,SAAS;AACvD;AAAA,YAAS;AAAA,YAAkB,MACzB;AAAA,cACE;AAAA,gBACE,MAAM;AAAA,gBACN;AAAA,gBACA,YAAY;AAAA,gBACZ;AAAA,gBACA,WAAW,MAAM,iBAAiB;AAAA,cAAA;AAAA,cAEpC;AAAA,YAAA;AAAA,UACF;AAEF,eAAK,IAAA;AACL,gBAAM,UAAU,OAAO,EAAE;AAAA,QAC3B;AAIA,2BAAmB,OAAO,GAAG;AAE7B,YAAI,mBAAmB;AACrB,4BAAkB,OAAO,KAAK,WAAW,KAAM;AAAA,YAC7C,iBAAiB,IAAI;AAAA,YACrB,yBAAyB;AAAA,YACzB,wBAAwB,IAAI;AAAA,UAAA,CAC7B;AAAA,QACH;AAEA,YAAI,KAAK,OAAO;AACd,gBAAM,SAAS,cAAc;AAAA,YAC3B,6BAA6B,KAAK,MAAM;AAAA,YACxC,8BAA8B,KAAK,MAAM;AAAA,UAAA,CAC1C;AAAA,QACH;AACA,YAAI,KAAK,cAAc;AACrB,gBAAM,SAAS,aAAa,kCAAkC;AAAA,YAC5D,KAAK;AAAA,UAAA,CACN;AAAA,QACH;AACA,cAAM,SAAS;AAAA,UACb;AAAA,UACA,MAAM;AAAA,QAAA;AAGR;AAAA,UAAS;AAAA,UAAkB,MACzB,YAAY,EAAE,MAAM,QAAQ,IAAA,GAAO,MAAM,QAAQ;AAAA,QAAA;AAEnD,cAAM,SAAS,IAAA;AACf,mBAAW,OAAO,GAAG;AAAA,MACvB,CAAC;AAAA,IACH;AAAA,EAAA;AAEJ;"}
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -629,41 +629,6 @@ export interface TextOptions<TProviderOptionsSuperset extends Record<string, any
|
|
|
629
629
|
*/
|
|
630
630
|
systemPrompts?: Array<SystemPrompt>;
|
|
631
631
|
agentLoopStrategy?: AgentLoopStrategy;
|
|
632
|
-
/**
|
|
633
|
-
* Controls the randomness of the output.
|
|
634
|
-
* Higher values (e.g., 0.8) make output more random, lower values (e.g., 0.2) make it more focused and deterministic.
|
|
635
|
-
* Range: [0.0, 2.0]
|
|
636
|
-
*
|
|
637
|
-
* Note: Generally recommended to use either temperature or topP, but not both.
|
|
638
|
-
*
|
|
639
|
-
* Provider usage:
|
|
640
|
-
* - OpenAI: `temperature` (number) - in text.top_p field
|
|
641
|
-
* - Anthropic: `temperature` (number) - ranges from 0.0 to 1.0, default 1.0
|
|
642
|
-
* - Gemini: `generationConfig.temperature` (number) - ranges from 0.0 to 2.0
|
|
643
|
-
*/
|
|
644
|
-
temperature?: number;
|
|
645
|
-
/**
|
|
646
|
-
* Nucleus sampling parameter. An alternative to temperature sampling.
|
|
647
|
-
* The model considers the results of tokens with topP probability mass.
|
|
648
|
-
* For example, 0.1 means only tokens comprising the top 10% probability mass are considered.
|
|
649
|
-
*
|
|
650
|
-
* Note: Generally recommended to use either temperature or topP, but not both.
|
|
651
|
-
*
|
|
652
|
-
* Provider usage:
|
|
653
|
-
* - OpenAI: `text.top_p` (number)
|
|
654
|
-
* - Anthropic: `top_p` (number | null)
|
|
655
|
-
* - Gemini: `generationConfig.topP` (number)
|
|
656
|
-
*/
|
|
657
|
-
topP?: number;
|
|
658
|
-
/**
|
|
659
|
-
* The maximum number of tokens to generate in the response.
|
|
660
|
-
*
|
|
661
|
-
* Provider usage:
|
|
662
|
-
* - OpenAI: `max_output_tokens` (number) - includes visible output and reasoning tokens
|
|
663
|
-
* - Anthropic: `max_tokens` (number, required) - range x >= 1
|
|
664
|
-
* - Gemini: `generationConfig.maxOutputTokens` (number)
|
|
665
|
-
*/
|
|
666
|
-
maxTokens?: number;
|
|
667
632
|
/**
|
|
668
633
|
* Additional metadata to attach to the request.
|
|
669
634
|
* Can be used for tracking, debugging, or passing custom information.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for the provider-native key spellings that cap output
|
|
3
|
+
* tokens. Sampling options live in opaque, provider-native `modelOptions`, and
|
|
4
|
+
* every provider spells the token cap differently. Two call sites must agree on
|
|
5
|
+
* this set or they silently drift:
|
|
6
|
+
*
|
|
7
|
+
* - `activities/summarize/chat-stream-summarize.ts` — detects a caller-supplied
|
|
8
|
+
* token limit so the summarize default never overrides it.
|
|
9
|
+
* - `middlewares/otel.ts` — picks the first numeric spelling to populate the
|
|
10
|
+
* `gen_ai.request.max_tokens` attribute across providers.
|
|
11
|
+
*
|
|
12
|
+
* Keep this list in lockstep with `MAX_TOKENS_KEY_BY_ADAPTER` (the adapter →
|
|
13
|
+
* native-key map) in the summarize wrapper.
|
|
14
|
+
*/
|
|
15
|
+
export declare const MAX_TOKENS_KEYS: readonly ["max_output_tokens", "max_tokens", "max_completion_tokens", "maxOutputTokens", "maxCompletionTokens", "maxTokens"];
|
|
16
|
+
/**
|
|
17
|
+
* Ollama nests sampling under `options`; its token cap is `options.num_predict`
|
|
18
|
+
* rather than a flat key.
|
|
19
|
+
*/
|
|
20
|
+
export declare const NESTED_MAX_TOKENS_KEY: "num_predict";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
const MAX_TOKENS_KEYS = [
|
|
2
|
+
"max_output_tokens",
|
|
3
|
+
// OpenAI (Responses)
|
|
4
|
+
"max_tokens",
|
|
5
|
+
// Anthropic / Grok
|
|
6
|
+
"max_completion_tokens",
|
|
7
|
+
// Groq
|
|
8
|
+
"maxOutputTokens",
|
|
9
|
+
// Gemini
|
|
10
|
+
"maxCompletionTokens",
|
|
11
|
+
// OpenRouter
|
|
12
|
+
"maxTokens"
|
|
13
|
+
// generic / migration leftover (no adapter reads it)
|
|
14
|
+
];
|
|
15
|
+
const NESTED_MAX_TOKENS_KEY = "num_predict";
|
|
16
|
+
export {
|
|
17
|
+
MAX_TOKENS_KEYS,
|
|
18
|
+
NESTED_MAX_TOKENS_KEY
|
|
19
|
+
};
|
|
20
|
+
//# sourceMappingURL=sampling-keys.js.map
|