diff --git a/.gitignore b/.gitignore index ab23f78..c638c91 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,5 @@ __pycache__/ mirror-failure.txt mirror-result.txt publish-failure.txt +!plugins/zcode-plugin-langfuse/payload/dist/ +!plugins/zcode-plugin-langfuse/payload/dist/** diff --git a/marketplace.json b/marketplace.json index 4e87656..a757acd 100644 --- a/marketplace.json +++ b/marketplace.json @@ -532,6 +532,24 @@ "cli", "oauth2" ] + }, + { + "name": "zcode-plugin-langfuse", + "source": "./plugins/zcode-plugin-langfuse", + "description": "Fail-open Langfuse tracing for ZCode sessions, prompts, tool calls, and assistant responses.", + "description_i18n": { + "en": "Fail-open Langfuse tracing for ZCode sessions, prompts, tool calls, and assistant responses.", + "zh-CN": "为 ZCode 会话、提示词、工具调用和助手回复提供 fail-open 的 Langfuse 追踪。" + }, + "version": "0.2.3", + "category": "developer-tools", + "tags": [ + "langfuse", + "tracing", + "telemetry", + "hooks" + ], + "strict": true } ] } diff --git a/plugins/zcode-plugin-langfuse/.claude-plugin/plugin.json b/plugins/zcode-plugin-langfuse/.claude-plugin/plugin.json new file mode 100644 index 0000000..8326021 --- /dev/null +++ b/plugins/zcode-plugin-langfuse/.claude-plugin/plugin.json @@ -0,0 +1,83 @@ +{ + "name": "zcode-plugin-langfuse", + "description": "Fail-open Langfuse tracing for ZCode sessions, prompts, tool calls, and assistant responses.", + "description_i18n": { + "en": "Fail-open Langfuse tracing for ZCode sessions, prompts, tool calls, and assistant responses.", + "zh-CN": "为 ZCode 会话、提示词、工具调用和助手回复提供 fail-open 的 Langfuse 追踪。" + }, + "version": "0.2.3", + "author": { + "name": "Community contributor" + }, + "license": "MIT", + "keywords": [ + "langfuse", + "observability", + "tracing", + "telemetry", + "hooks" + ], + "userConfig": { + "langfuse_public_key": { + "title": "Langfuse public key", + "description": "Langfuse project public key. You can also provide LANGFUSE_PUBLIC_KEY in the ZCode process environment.", + "type": "string", + "required": false + }, + "langfuse_secret_key": { + "title": "Langfuse secret key", + "description": "Langfuse project secret key. It is used only by the local hook process and is never written to hook output.", + "type": "string", + "required": false, + "sensitive": true + }, + "langfuse_base_url": { + "title": "Langfuse base URL", + "description": "HTTPS Langfuse host, for example https://cloud.langfuse.com or a self-hosted HTTPS URL. Plain HTTP is rejected.", + "type": "string", + "default": "https://cloud.langfuse.com" + }, + "langfuse_user_id": { + "title": "Langfuse user ID", + "description": "Optional stable user identifier attached to traces. Leave empty to omit it.", + "type": "string", + "required": false + }, + "langfuse_environment": { + "title": "Langfuse environment", + "description": "Environment label attached to traces.", + "type": "string", + "default": "development" + }, + "capture_prompts": { + "title": "Capture prompts", + "description": "Include user prompts and assistant responses in Langfuse. Disable for metadata-only tracing.", + "type": "boolean", + "default": true + }, + "capture_tool_inputs": { + "title": "Capture tool inputs", + "description": "Include tool names and inputs in Langfuse spans.", + "type": "boolean", + "default": true + }, + "capture_tool_outputs": { + "title": "Capture tool outputs", + "description": "Include tool results and errors in Langfuse spans.", + "type": "boolean", + "default": true + }, + "max_capture_chars": { + "title": "Maximum captured characters", + "description": "Maximum characters per captured prompt, tool payload, or response. Larger values are truncated.", + "type": "number", + "default": 20000 + }, + "debug": { + "title": "Debug diagnostics", + "description": "Write non-sensitive lifecycle diagnostics to stderr. Disabled by default.", + "type": "boolean", + "default": false + } + } +} diff --git a/plugins/zcode-plugin-langfuse/.zcode-plugin/plugin.json b/plugins/zcode-plugin-langfuse/.zcode-plugin/plugin.json new file mode 100644 index 0000000..8326021 --- /dev/null +++ b/plugins/zcode-plugin-langfuse/.zcode-plugin/plugin.json @@ -0,0 +1,83 @@ +{ + "name": "zcode-plugin-langfuse", + "description": "Fail-open Langfuse tracing for ZCode sessions, prompts, tool calls, and assistant responses.", + "description_i18n": { + "en": "Fail-open Langfuse tracing for ZCode sessions, prompts, tool calls, and assistant responses.", + "zh-CN": "为 ZCode 会话、提示词、工具调用和助手回复提供 fail-open 的 Langfuse 追踪。" + }, + "version": "0.2.3", + "author": { + "name": "Community contributor" + }, + "license": "MIT", + "keywords": [ + "langfuse", + "observability", + "tracing", + "telemetry", + "hooks" + ], + "userConfig": { + "langfuse_public_key": { + "title": "Langfuse public key", + "description": "Langfuse project public key. You can also provide LANGFUSE_PUBLIC_KEY in the ZCode process environment.", + "type": "string", + "required": false + }, + "langfuse_secret_key": { + "title": "Langfuse secret key", + "description": "Langfuse project secret key. It is used only by the local hook process and is never written to hook output.", + "type": "string", + "required": false, + "sensitive": true + }, + "langfuse_base_url": { + "title": "Langfuse base URL", + "description": "HTTPS Langfuse host, for example https://cloud.langfuse.com or a self-hosted HTTPS URL. Plain HTTP is rejected.", + "type": "string", + "default": "https://cloud.langfuse.com" + }, + "langfuse_user_id": { + "title": "Langfuse user ID", + "description": "Optional stable user identifier attached to traces. Leave empty to omit it.", + "type": "string", + "required": false + }, + "langfuse_environment": { + "title": "Langfuse environment", + "description": "Environment label attached to traces.", + "type": "string", + "default": "development" + }, + "capture_prompts": { + "title": "Capture prompts", + "description": "Include user prompts and assistant responses in Langfuse. Disable for metadata-only tracing.", + "type": "boolean", + "default": true + }, + "capture_tool_inputs": { + "title": "Capture tool inputs", + "description": "Include tool names and inputs in Langfuse spans.", + "type": "boolean", + "default": true + }, + "capture_tool_outputs": { + "title": "Capture tool outputs", + "description": "Include tool results and errors in Langfuse spans.", + "type": "boolean", + "default": true + }, + "max_capture_chars": { + "title": "Maximum captured characters", + "description": "Maximum characters per captured prompt, tool payload, or response. Larger values are truncated.", + "type": "number", + "default": 20000 + }, + "debug": { + "title": "Debug diagnostics", + "description": "Write non-sensitive lifecycle diagnostics to stderr. Disabled by default.", + "type": "boolean", + "default": false + } + } +} diff --git a/plugins/zcode-plugin-langfuse/LICENSE b/plugins/zcode-plugin-langfuse/LICENSE new file mode 100644 index 0000000..b2f7956 --- /dev/null +++ b/plugins/zcode-plugin-langfuse/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Community contributors + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/plugins/zcode-plugin-langfuse/README.md b/plugins/zcode-plugin-langfuse/README.md new file mode 100644 index 0000000..85b3bb6 --- /dev/null +++ b/plugins/zcode-plugin-langfuse/README.md @@ -0,0 +1,239 @@ +# Langfuse Observability for ZCode + +[English](README.md) · [简体中文](README.zh-CN.md) + +A community ZCode plugin that sends one Langfuse trace per completed ZCode turn. +It is designed for review and possible inclusion in the ZCode official plugin +marketplace. + +> **Status:** early community contribution (`0.2.3`). The plugin is fail-open: +> a missing credential, malformed hook payload, local state error, or Langfuse +> request error must never block a ZCode session. + +## What it records + +The plugin listens to ZCode's process-hook events: + +- `SessionStart` +- `UserPromptSubmit` +- `PreToolUse` +- `PostToolUse` +- `PostToolUseFailure` +- `Stop` + +At `Stop`, it emits a trace named `ZCode Turn` containing: + +- the ZCode session ID; +- the user prompt and final assistant message, when prompt capture is enabled; +- tool calls as Langfuse spans, including names and optional input/output; +- an assistant response generation; +- release, environment, turn ID, and tool-count metadata. + +It does **not** read the transcript file or collect hidden chain-of-thought. It +only uses fields delivered in the ZCode hook payload. + +## Permissions and side effects + +Each of the six events starts a `node` process with the current user's +permissions. The process reads one JSON Hook event from stdin and writes one +empty JSON object to stdout; it does not spawn a shell or run user commands. + +The hook reads `ZCODE_CONFIG_PATH`, or +`~/.zcode/cli/config.json` when that variable is unset, to find persisted plugin +options. It writes only bounded, hashed JSON session state under +`ZCODE_PLUGIN_DATA`, or the ZCode plugin data directory fallback. At `Stop`, +it sends HTTPS requests to the configured Langfuse ingestion endpoint using the +local credentials. No transcript files or hidden reasoning are read. + +## Architecture + +```text +ZCode hook stdin + │ one JSON object + ▼ + hooks/entry.mjs ──► TurnTracker ──► JsonStateStore + │ │ + │ └─ per-session, atomic, hashed filename + ▼ + LangfuseTraceSink ──► bundled official `langfuse` SDK + │ + ▼ + Langfuse ingestion API +``` + +The source is deliberately split into deep modules: + +- `src/domain/` — hook and trace data types plus payload extraction; +- `src/application/` — configuration and the turn state machine; +- `src/adapters/` — the filesystem state adapter and Langfuse SDK adapter; +- `src/hooks/` — the small process entry point and fail-open policy. + +The distributable `dist/hooks/entry.mjs` bundles the official JavaScript SDK, so +an installed plugin does not need a separate `npm install` at runtime. + +## Configuration + +The plugin reads ZCode `userConfig` values using the standard ZCode environment +mapping. For example, the manifest key `langfuse_public_key` becomes: + +```text +ZCODE_USER_CONFIG_LANGFUSE_PUBLIC_KEY +``` + +The following ordinary environment variables are also accepted, which is useful +for local smoke tests and managed deployments: + +```text +LANGFUSE_PUBLIC_KEY +LANGFUSE_SECRET_KEY +LANGFUSE_BASE_URL +LANGFUSE_USER_ID +LANGFUSE_ENVIRONMENT +LANGFUSE_RELEASE +LANGFUSE_ENABLED +LANGFUSE_CAPTURE_PROMPTS +LANGFUSE_CAPTURE_TOOL_INPUTS +LANGFUSE_CAPTURE_TOOL_OUTPUTS +LANGFUSE_MAX_CAPTURE_CHARS +LANGFUSE_DEBUG +``` + +`LANGFUSE_BASE_URL` defaults to `https://cloud.langfuse.com`. Set it to your +self-hosted HTTPS URL, for example `https://langfuse.example.com`. Plain HTTP +URLs are rejected and replaced with the default HTTPS endpoint. + +For a self-hosted project, configure the public and secret keys in the plugin +configuration. The hook reads its own persisted `plugins.options` entry using +`ZCODE_PLUGIN_ID`; this is needed because current ZCode runtimes do not inject +all `userConfig` values into process-hook environment variables. Standard +`LANGFUSE_*` environment variables override stored options. Never commit +credentials or put them in `hooks/hooks.json`. + +### Privacy controls + +All capture controls default to `true` for useful traces. Set any of these to +`false` to keep the corresponding content out of both the Langfuse request and +local per-session state: + +```text +LANGFUSE_CAPTURE_PROMPTS=false +LANGFUSE_CAPTURE_TOOL_INPUTS=false +LANGFUSE_CAPTURE_TOOL_OUTPUTS=false +``` + +Every captured field is bounded by `LANGFUSE_MAX_CAPTURE_CHARS` (default +`20000`). Metadata-only mode still reports timing/session/tool-count structure, +but not prompt, response, tool input, tool output, or error text. + +## Development + +Requirements: Node.js 20 or newer. + +```bash +npm ci +npm run check +npm run package:plugin +``` + +`npm run build` builds the complete `dist/` output, structured as a ZCode +marketplace shell whose single entry uses the official plugin layout (same +shape as the `zcode-plugins-official` template): + +```text +dist/marketplace.json +dist/plugins/zcode-plugin-langfuse/ +├── .zcode-plugin/plugin.json +├── .claude-plugin/plugin.json (identical copy for Claude compatibility) +├── hooks/hooks.json (points at hooks/entry.mjs) +├── hooks/entry.mjs (sealed runtime bundle) +├── README.md +├── README_CN.md +├── LICENSE +└── THIRD_PARTY_NOTICES.md +``` + +`dist/plugins/zcode-plugin-langfuse/` is the installable plugin: its +`hooks/entry.mjs` runtime, manifest, hooks, dual-language README, license, and +third-party notices. The release workflow compresses the tree into the +versioned ZIP; the official catalog and local directory installs consume the +plugin directory as-is. Everything under `dist/` is generated and not +committed. + +The hook can be smoke-tested without credentials: + +```bash +printf '%s\n' '{"hook_event_name":"Stop","session_id":"smoke","last_assistant_message":"ok"}' \ + | ZCODE_PLUGIN_DATA="$(mktemp -d)" \ + LANGFUSE_DEBUG=true \ + node dist/plugins/zcode-plugin-langfuse/hooks/entry.mjs +``` + +Expected stdout is one empty hook result object: + +```json +{} +``` + +Diagnostics, when enabled, go to stderr. They never contain keys or full +prompts. + +## Third-party software + +The runtime uses `langfuse` 3.38.20, `langfuse-core` 3.38.20, and `mustache` +4.2.0. Each dependency is MIT-licensed; see +[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md) for versions, provenance, and +source links. Langfuse is an external service selected by the user and is not +bundled with credentials. + +## Local installation for testing + +ZCode treats a selected directory as a **plugin marketplace**, so this repository +includes `marketplace.json` at its root; its entry points at +`./dist/plugins/zcode-plugin-langfuse`. Build first, then use **Settings → +Plugins → Add marketplace → Select directory** and choose this repository. +Install `zcode-plugin-langfuse` from the resulting personal marketplace, enable +it, and configure its `userConfig` values. + +The plugin manifest is at `.zcode-plugin/plugin.json`; the hook declaration is +at `hooks/hooks.json`. + +## Install a release + +Download `zcode-plugin-langfuse-v.zip` from the release page, extract it, and add +the extracted plugin directory as a local marketplace via **Settings → +Plugins → Add marketplace**. Do not add a fresh checkout of this repository as +a marketplace: it has no `dist/` bundle (generated, not committed), so hooks +cannot start. For a development install from a checkout, run `npm install` +first — the `prepare` hook builds `dist/`. + +- Marketplace manifest: +- Plugin manifest: +- Latest release (asset: `zcode-plugin-langfuse-v.zip`): +- Release page: +- Official ZCode plugin documentation: +- Official ZCode plugin marketplace: + +The repository marketplace is named `zcode-plugin-langfuse`, and its +`marketplace.json` resolves the plugin from `source: "."`. Versioned release +assets are generated by the tagged GitHub Actions workflow. + +Before submitting a marketplace change, verify: + +1. `npm run check` passes; +2. `npm run package:plugin` creates and validates the release ZIP; +3. `hooks/hooks.json` contains no unsupported hook events; +4. no credentials are present in the repository, package, or logs; +5. disabling prompt/tool capture behaves as documented. + +## Project policies + +- [Agent instructions](AGENTS.md) +- [Contributing](CONTRIBUTING.md) +- [Code of Conduct](CODE_OF_CONDUCT.md) +- [Security policy](SECURITY.md) +- [Release checklist](docs/releasing.md) +- [Design notes](DESIGN.md) + +## License + +MIT. See [LICENSE](LICENSE). diff --git a/plugins/zcode-plugin-langfuse/README_CN.md b/plugins/zcode-plugin-langfuse/README_CN.md new file mode 100644 index 0000000..2068418 --- /dev/null +++ b/plugins/zcode-plugin-langfuse/README_CN.md @@ -0,0 +1,165 @@ +# ZCode Langfuse Observability + +[English](README.md) · [简体中文](README.zh-CN.md) + +面向 ZCode 的社区 Langfuse 观测插件,按每个完成的 ZCode turn 生成一条 +Langfuse trace,目标是提交给 ZCode 官方插件市场。 + +> 当前版本:`0.2.3`。插件遵循 **fail-open**:缺少凭据、Hook 输入损坏、本地 +> 状态错误或 Langfuse 请求失败,都不能阻塞 ZCode 会话。 + +## 采集范围 + +监听 `SessionStart`、`UserPromptSubmit`、`PreToolUse`、`PostToolUse`、 +`PostToolUseFailure` 和 `Stop`。在 `Stop` 时发送: + +- session ID; +- 用户 prompt 与最后一条 assistant 回复(可关闭); +- 工具调用 Langfuse span(名称、输入、输出可分别关闭); +- assistant generation; +- release、environment、turn ID 和工具数量元数据。 + +插件**不读取 transcript 文件,也不采集隐藏完整思维链**,只使用 ZCode +Hook stdin 传入的字段。 + +## 权限与副作用 + +六个事件都会以当前用户权限启动一个 `node` process Hook。Hook 从 stdin +读取一个 JSON 事件,并向 stdout 写入一个空 JSON 对象;不会启动 shell,也不会 +执行用户命令。 + +Hook 会读取 `ZCODE_CONFIG_PATH` 指定的配置;未设置时读取 +`~/.zcode/cli/config.json` 中保存的插件选项。它只会在 +`ZCODE_PLUGIN_DATA` 或 ZCode 插件数据目录回退路径下写入有界、哈希化的 JSON +会话状态。`Stop` 时使用本地凭据向用户配置的 Langfuse HTTPS 接口发送请求。 +不会读取 transcript 文件或隐藏推理内容。 + +## 配置 + +Manifest 中的 `userConfig` 会映射成以下环境变量,例如: + +```text +ZCODE_USER_CONFIG_LANGFUSE_PUBLIC_KEY +ZCODE_USER_CONFIG_LANGFUSE_SECRET_KEY +ZCODE_USER_CONFIG_LANGFUSE_BASE_URL +``` + +同时支持标准变量: + +```text +LANGFUSE_PUBLIC_KEY +LANGFUSE_SECRET_KEY +LANGFUSE_BASE_URL +LANGFUSE_USER_ID +LANGFUSE_ENVIRONMENT +LANGFUSE_RELEASE +LANGFUSE_ENABLED +LANGFUSE_CAPTURE_PROMPTS +LANGFUSE_CAPTURE_TOOL_INPUTS +LANGFUSE_CAPTURE_TOOL_OUTPUTS +LANGFUSE_MAX_CAPTURE_CHARS +LANGFUSE_DEBUG +``` + +`LANGFUSE_BASE_URL` 默认 `https://cloud.langfuse.com`;自建 Langfuse 请填写 +HTTPS 地址,例如 `https://langfuse.example.com`。明文 HTTP 地址会被拒绝并回退到 +默认 HTTPS 地址。 + +敏感 secret 不要写入仓库或 `hooks/hooks.json`。Hook 会根据运行时提供的 +`ZCODE_PLUGIN_ID` 从 ZCode 的 `plugins.options` 读取本插件配置;这是因为当前 +ZCode 不会把全部 `userConfig` 自动注入 process Hook 环境。标准 +`LANGFUSE_*` 环境变量优先级更高,可覆盖保存的配置。 + +关闭内容采集: + +```text +LANGFUSE_CAPTURE_PROMPTS=false +LANGFUSE_CAPTURE_TOOL_INPUTS=false +LANGFUSE_CAPTURE_TOOL_OUTPUTS=false +``` + +关闭后,内容不会写入 Langfuse,也不会写入本地会话状态;仍保留 session、 +工具数量等结构化元数据。单字段默认最多采集 20000 个字符,可用 +`LANGFUSE_MAX_CAPTURE_CHARS` 调整。 + +## 第三方软件 + +运行时使用 `langfuse` 3.38.20、`langfuse-core` 3.38.20 和 `mustache` 4.2.0, +均为 MIT 许可证。版本、来源和许可证见 +[THIRD_PARTY_NOTICES.md](THIRD_PARTY_NOTICES.md)。Langfuse 是用户选择的外部服务, +仓库不会打包凭据。 + +## 在 ZCode 中本地测试 + +ZCode 选择本地目录时会按“插件市场”读取,因此仓库根目录包含 +`marketplace.json`,其条目指向 `./dist/plugins/zcode-plugin-langfuse`。先运行 +`npm run build`,再在 **设置 → 插件 → 添加插件市场 → +选择目录** 中选择本仓库,从“个人”市场安装并启用 +`zcode-plugin-langfuse`。正式市场使用版本化 ZIP 和 SHA-256。 + +## 在线 ZCode 插件市场 + +从 Release 页下载 `zcode-plugin-langfuse-v<版本>.zip` 并解压,在 **设置 → 插件 → 添加插件市场 → +选择目录** 中选择解压后的插件目录安装。不要把全新检出的源码仓库直接添加为市场: +其中没有 `dist/` bundle(生成目录不入库),Hook 无法启动。开发安装请先在检出目录 +运行 `npm install`(`prepare` 钩子会构建 `dist/`),再选择该目录。 + +- 市场清单: +- 插件清单: +- 最新 Release(资产:`zcode-plugin-langfuse-v<版本>.zip`): +- Release 页面: +- ZCode 官方插件文档: +- ZCode 官方插件市场: + +本仓库市场名为 `zcode-plugin-langfuse`,`marketplace.json` 使用 +`source: "."` 从仓库根目录解析插件。版本化发行文件由 GitHub Actions 的 tag +workflow 生成。 + +## 开发与构建 + +需要 Node.js 20+: + +```bash +npm ci +npm run check +npm run package:plugin +``` + +`npm run build` 会把官方 `langfuse` JavaScript SDK 打包进本地 `dist/`。 +`npm run build` 会构建完整的 `dist/` 输出:外层是插件市场壳,内层插件目录与 +ZCode 官方模板(`zcode-plugins-official`)布局一致: + +```text +dist/marketplace.json +dist/plugins/zcode-plugin-langfuse/ +├── .zcode-plugin/plugin.json +├── .claude-plugin/plugin.json (同内容副本,兼容 Claude) +├── hooks/hooks.json (指向 hooks/entry.mjs) +├── hooks/entry.mjs (密封运行时 bundle) +├── README.md +├── README_CN.md +├── LICENSE +└── THIRD_PARTY_NOTICES.md +``` + +`dist/plugins/zcode-plugin-langfuse/` 即可安装的插件:包含 `hooks/entry.mjs` +运行时、manifest、hooks、双语 README、许可证与第三方声明。Release +工作流负责把该目录压缩为版本化 ZIP;官方目录与本地目录安装直接使用该插件目录。 +整个 `dist/` 均为生成目录,不入库。 + +目录分层: + +- `src/domain/`:Hook 与 trace 数据类型、输入解析; +- `src/application/`:配置与 turn 状态机; +- `src/adapters/`:本地状态和 Langfuse SDK 适配器; +- `src/hooks/`:很薄的进程入口和 fail-open 策略。 + +更多设计约束见 [DESIGN.md](DESIGN.md)。 + +项目规范: + +- [Agent 指令](AGENTS.md) +- [贡献指南](CONTRIBUTING.md) +- [行为准则](CODE_OF_CONDUCT.md) +- [安全策略](SECURITY.md) +- [发布清单](docs/releasing.md) diff --git a/plugins/zcode-plugin-langfuse/THIRD_PARTY_NOTICES.md b/plugins/zcode-plugin-langfuse/THIRD_PARTY_NOTICES.md new file mode 100644 index 0000000..e228fc0 --- /dev/null +++ b/plugins/zcode-plugin-langfuse/THIRD_PARTY_NOTICES.md @@ -0,0 +1,14 @@ +# Third-party notices + +The runtime uses the following npm packages. Versions are pinned by +`package-lock.json` and their licenses remain with the respective projects: + +| Package | Version | License | Source | +| --- | --- | --- | --- | +| `langfuse` | 3.38.20 | MIT | | +| `langfuse-core` | 3.38.20 | MIT | | +| `mustache` | 4.2.0 | MIT | | + +The plugin sends telemetry to a user-configured Langfuse service over HTTPS. +That service is external to this repository and is governed by the service +operator's terms and privacy policy. No credentials are bundled or published. diff --git a/plugins/zcode-plugin-langfuse/hooks/entry.mjs b/plugins/zcode-plugin-langfuse/hooks/entry.mjs new file mode 100644 index 0000000..b069afb --- /dev/null +++ b/plugins/zcode-plugin-langfuse/hooks/entry.mjs @@ -0,0 +1,4754 @@ +// Generated by npm run build. Edit src/, not dist/. + +// src/hooks/entry.ts +import { randomUUID as randomUUID2 } from "node:crypto"; + +// src/application/config.ts +import { readFileSync } from "node:fs"; +import { homedir } from "node:os"; +import { join } from "node:path"; +var DEFAULT_BASE_URL = "https://cloud.langfuse.com"; +var DEFAULT_RELEASE = "0.2.3"; +var DEFAULT_MAX_CAPTURE_CHARS = 2e4; +function asText(value) { + if (value === void 0) return void 0; + return typeof value === "string" ? value : String(value); +} +function firstNonEmpty(...values) { + return values.map(asText).find((value) => value !== void 0 && value.trim().length > 0)?.trim(); +} +function userConfig(env, name) { + const normalized = name.toUpperCase(); + return firstNonEmpty( + env[`ZCODE_USER_CONFIG_${normalized}`], + env[`ZCODE_PLUGIN_CONFIG_${normalized}`] + ); +} +function isStoredOption(value) { + return typeof value === "string" || typeof value === "number" || typeof value === "boolean"; +} +function parseStoredOptions(value) { + if (typeof value !== "object" || value === null || Array.isArray(value)) + return {}; + const options = {}; + for (const [key, option2] of Object.entries(value)) { + if (isStoredOption(option2)) options[key] = option2; + } + return options; +} +function readStoredOptions(env) { + const configPaths = [ + env.ZCODE_CONFIG_PATH, + join(homedir(), ".zcode", "cli", "config.json") + ].filter((path) => Boolean(path)); + const configuredPluginId = env.ZCODE_PLUGIN_ID; + for (const configPath of configPaths) { + try { + const parsed = JSON.parse(readFileSync(configPath, "utf8")); + if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) + continue; + const plugins = parsed.plugins; + if (typeof plugins !== "object" || plugins === null || Array.isArray(plugins)) + continue; + const options = plugins.options; + if (typeof options !== "object" || options === null || Array.isArray(options)) + continue; + const optionEntries = options; + if (configuredPluginId && optionEntries[configuredPluginId]) { + return parseStoredOptions(optionEntries[configuredPluginId]); + } + } catch { + } + } + return {}; +} +function option(env, storedOptions, name, environmentName) { + return firstNonEmpty( + userConfig(env, name), + env[environmentName], + storedOptions[name] + ); +} +function parseBoolean(value, fallback) { + if (!value) return fallback; + if (["1", "true", "yes", "on"].includes(value.toLowerCase())) return true; + if (["0", "false", "no", "off"].includes(value.toLowerCase())) return false; + return fallback; +} +function parsePositiveInteger(value, fallback) { + if (!value) return fallback; + const parsed = Number.parseInt(value, 10); + if (!Number.isFinite(parsed) || parsed <= 0) return fallback; + return Math.min(parsed, 1e6); +} +function normalizeBaseUrl(value) { + const candidate = value ?? DEFAULT_BASE_URL; + try { + const url = new URL(candidate); + if (url.protocol !== "https:") return DEFAULT_BASE_URL; + return candidate.replace(/\/+$/, ""); + } catch { + return DEFAULT_BASE_URL; + } +} +function readConfig(env = process.env, storedOptions = readStoredOptions(env)) { + const enabled = parseBoolean( + option(env, storedOptions, "enabled", "LANGFUSE_ENABLED"), + true + ); + const publicKey = option(env, storedOptions, "langfuse_public_key", "LANGFUSE_PUBLIC_KEY") ?? null; + const secretKey = option(env, storedOptions, "langfuse_secret_key", "LANGFUSE_SECRET_KEY") ?? null; + return { + publicKey, + secretKey, + baseUrl: normalizeBaseUrl( + option(env, storedOptions, "langfuse_base_url", "LANGFUSE_BASE_URL") + ), + userId: option(env, storedOptions, "langfuse_user_id", "LANGFUSE_USER_ID") ?? null, + environment: option( + env, + storedOptions, + "langfuse_environment", + "LANGFUSE_ENVIRONMENT" + ) ?? "development", + release: option(env, storedOptions, "langfuse_release", "LANGFUSE_RELEASE") ?? DEFAULT_RELEASE, + enabled, + capturePrompts: parseBoolean( + option(env, storedOptions, "capture_prompts", "LANGFUSE_CAPTURE_PROMPTS"), + true + ), + captureToolInputs: parseBoolean( + option( + env, + storedOptions, + "capture_tool_inputs", + "LANGFUSE_CAPTURE_TOOL_INPUTS" + ), + true + ), + captureToolOutputs: parseBoolean( + option( + env, + storedOptions, + "capture_tool_outputs", + "LANGFUSE_CAPTURE_TOOL_OUTPUTS" + ), + true + ), + maxCaptureChars: parsePositiveInteger( + option( + env, + storedOptions, + "max_capture_chars", + "LANGFUSE_MAX_CAPTURE_CHARS" + ), + DEFAULT_MAX_CAPTURE_CHARS + ), + debug: parseBoolean( + option(env, storedOptions, "debug", "LANGFUSE_DEBUG"), + false + ) + }; +} +function isConfigured(config) { + return config.enabled && Boolean(config.publicKey && config.secretKey); +} + +// src/domain/extract.ts +function toJsonValue(value) { + if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") { + return value; + } + if (Array.isArray(value)) { + const values = value.map(toJsonValue); + return values.every((item) => item !== void 0) ? values : void 0; + } + if (typeof value === "object") { + const entries = Object.entries(value); + const result = {}; + for (const [key, item] of entries) { + const parsed = toJsonValue(item); + if (parsed === void 0) return void 0; + result[key] = parsed; + } + return result; + } + return void 0; +} +function valueAt(payload, ...keys) { + for (const key of keys) { + const value = toJsonValue(payload[key]); + if (value !== void 0 && value !== null) return value; + } + return void 0; +} +function asString(value) { + if (typeof value === "string") return value; + if (typeof value === "number" || typeof value === "boolean") + return String(value); + return null; +} +function stringifyValue(value) { + if (typeof value === "string") return value; + if (value === void 0) return ""; + const json = JSON.stringify(value); + return json ?? String(value); +} +function eventName(payload) { + return asString( + valueAt(payload, "hook_event_name", "hookEventName", "event", "event_name") + ); +} +function sessionId(payload) { + const value = asString(valueAt(payload, "session_id", "sessionId")); + return value?.trim() || null; +} +function prompt(payload) { + const value = valueAt( + payload, + "prompt", + "user_prompt", + "userPrompt", + "message" + ); + return value === void 0 ? null : stringifyValue(value); +} +function assistantMessage(payload) { + const value = valueAt( + payload, + "last_assistant_message", + "lastAssistantMessage" + ); + return value === void 0 ? null : stringifyValue(value); +} +function toolId(payload) { + const value = asString( + valueAt(payload, "tool_use_id", "toolUseId", "tool_id", "toolId", "id") + ); + return value?.trim() || null; +} +function toolName(payload) { + return asString( + valueAt(payload, "tool_name", "toolName", "name", "tool") + )?.trim() ?? "unknown"; +} +function toolInput(payload) { + return valueAt(payload, "tool_input", "toolInput", "input", "arguments") ?? null; +} +function toolOutput(payload) { + return valueAt( + payload, + "tool_output", + "toolOutput", + "output", + "result", + "tool_response" + ) ?? null; +} +function toolError(payload) { + return stringifyValue( + valueAt(payload, "error", "error_message", "errorMessage", "message") ?? "Tool failed" + ); +} + +// src/application/turn-tracker.ts +function createSession(session, now) { + return { + version: 1, + sessionId: session, + startedAt: now, + updatedAt: now, + currentTurn: null + }; +} +function createTurn(id, now, userPrompt) { + return { + id, + prompt: userPrompt, + startedAt: now, + tools: [] + }; +} +var TRUNCATION_MARKER = "\u2026 [truncated]"; +function truncate(value, maxChars) { + if (value.length <= maxChars) return value; + if (maxChars <= TRUNCATION_MARKER.length) return value.slice(0, maxChars); + return `${value.slice(0, maxChars - TRUNCATION_MARKER.length)}${TRUNCATION_MARKER}`; +} +function boundedValue(value, maxChars) { + const serialized = JSON.stringify(value); + if (serialized.length <= maxChars) return value; + return truncate(serialized, maxChars); +} +function sanitizeTurn(turn, config) { + return { + ...turn, + prompt: config.capturePrompts && turn.prompt !== null ? truncate(turn.prompt, config.maxCaptureChars) : null, + assistantMessage: config.capturePrompts && turn.assistantMessage !== null ? truncate(turn.assistantMessage, config.maxCaptureChars) : null, + tools: turn.tools.map((tool) => ({ + ...tool, + name: truncate(tool.name, 256), + input: config.captureToolInputs ? boundedValue(tool.input, config.maxCaptureChars) : null, + output: config.captureToolOutputs && tool.output !== null ? boundedValue(tool.output, config.maxCaptureChars) : null, + error: config.captureToolOutputs && tool.error !== null ? truncate(tool.error, config.maxCaptureChars) : null + })) + }; +} +function findTool(turn, id, name) { + if (id) { + const byId = turn.tools.find((tool) => tool.id === id); + if (byId) return byId; + } + return [...turn.tools].reverse().find((tool) => tool.endedAt === null && tool.name === name) ?? [...turn.tools].reverse().find((tool) => tool.endedAt === null) ?? null; +} +var TurnTracker = class { + constructor(stateStore, traceSink, config, clock, idGenerator) { + this.stateStore = stateStore; + this.traceSink = traceSink; + this.config = config; + this.clock = clock; + this.idGenerator = idGenerator; + } + stateStore; + traceSink; + config; + clock; + idGenerator; + async handle(payload) { + const name = eventName(payload); + const session = sessionId(payload); + if (!name || !session) return; + let completed = null; + await this.stateStore.withSessionLock(session, async () => { + const now = this.clock.now().toISOString(); + const existing = await this.stateStore.load(session) ?? createSession(session, now); + switch (name) { + case "SessionStart": + existing.updatedAt = now; + await this.stateStore.save(existing); + return; + case "UserPromptSubmit": { + const userPrompt = prompt(payload); + existing.currentTurn = createTurn( + this.idGenerator.next(), + now, + this.config.capturePrompts && userPrompt !== null ? truncate(userPrompt, this.config.maxCaptureChars) : null + ); + existing.updatedAt = now; + await this.stateStore.save(existing); + return; + } + case "PreToolUse": + this.recordToolStart(existing, payload, now); + await this.stateStore.save(existing); + return; + case "PostToolUse": + this.recordToolOutput(existing, payload, now, false); + await this.stateStore.save(existing); + return; + case "PostToolUseFailure": + this.recordToolOutput(existing, payload, now, true); + await this.stateStore.save(existing); + return; + case "Stop": + completed = sanitizeTurn( + { + sessionId: session, + turnId: existing.currentTurn?.id ?? this.idGenerator.next(), + prompt: existing.currentTurn?.prompt ?? null, + assistantMessage: assistantMessage(payload), + startedAt: existing.currentTurn?.startedAt ?? now, + endedAt: now, + tools: existing.currentTurn?.tools ?? [] + }, + this.config + ); + await this.stateStore.clear(session); + return; + default: + return; + } + }); + if (completed) await this.traceSink.publishTurn(completed); + } + recordToolStart(state, payload, now) { + const turn = state.currentTurn ?? createTurn(this.idGenerator.next(), now, null); + state.currentTurn = turn; + turn.tools.push({ + id: toolId(payload) ?? this.idGenerator.next(), + name: toolName(payload), + input: this.config.captureToolInputs ? boundedValue(toolInput(payload), this.config.maxCaptureChars) : null, + output: null, + error: null, + startedAt: now, + endedAt: null + }); + state.updatedAt = now; + } + recordToolOutput(state, payload, now, failed) { + const turn = state.currentTurn ?? createTurn(this.idGenerator.next(), now, null); + state.currentTurn = turn; + const name = toolName(payload); + const tool = findTool(turn, toolId(payload), name); + const output = !failed && this.config.captureToolOutputs ? boundedValue(toolOutput(payload), this.config.maxCaptureChars) : null; + const error = failed && this.config.captureToolOutputs ? truncate(toolError(payload), this.config.maxCaptureChars) : null; + if (tool) { + tool.output = output; + tool.error = error; + tool.endedAt = now; + } else { + turn.tools.push({ + id: toolId(payload) ?? this.idGenerator.next(), + name, + input: null, + output, + error, + startedAt: now, + endedAt: now + }); + } + state.updatedAt = now; + } +}; +var NoopTraceSink = class { + async publishTurn(_turn) { + } +}; + +// src/adapters/json-state-store.ts +import { createHash, randomUUID } from "node:crypto"; +import { + mkdir, + open, + readFile, + rename, + rm, + stat, + writeFile +} from "node:fs/promises"; +import { homedir as homedir2, tmpdir } from "node:os"; +import { join as join2 } from "node:path"; + +// src/domain/identity.ts +var PLUGIN_ID = "zcode-plugin-langfuse"; + +// src/adapters/json-state-store.ts +var LOCK_WAIT_MS = 25; +var LOCK_ATTEMPTS = 160; +var STALE_LOCK_MS = 6e4; +function isRecord(value) { + return typeof value === "object" && value !== null && !Array.isArray(value); +} +function isJsonValue(value) { + if (value === null || typeof value === "string" || typeof value === "number" || typeof value === "boolean") { + return true; + } + if (Array.isArray(value)) return value.every(isJsonValue); + return isRecord(value) && Object.values(value).every(isJsonValue); +} +function parseToolCall(value) { + if (!isRecord(value)) return null; + if (typeof value.id !== "string" || typeof value.name !== "string" || !isJsonValue(value.input) || value.output !== null && !isJsonValue(value.output) || value.error !== null && typeof value.error !== "string" || typeof value.startedAt !== "string" || value.endedAt !== null && typeof value.endedAt !== "string") { + return null; + } + return { + id: value.id, + name: value.name, + input: value.input, + output: value.output, + error: value.error, + startedAt: value.startedAt, + endedAt: value.endedAt + }; +} +function parseTurn(value) { + if (!isRecord(value)) return null; + if (typeof value.id !== "string" || value.prompt !== null && typeof value.prompt !== "string" || typeof value.startedAt !== "string" || !Array.isArray(value.tools)) { + return null; + } + const tools = value.tools.map(parseToolCall); + if (tools.some((tool) => tool === null)) return null; + return { + id: value.id, + prompt: value.prompt, + startedAt: value.startedAt, + tools: tools.filter((tool) => tool !== null) + }; +} +function parseState(value) { + if (!isRecord(value)) return null; + if (value.version !== 1 || typeof value.sessionId !== "string" || typeof value.startedAt !== "string" || typeof value.updatedAt !== "string") { + return null; + } + const currentTurn = value.currentTurn === null ? null : parseTurn(value.currentTurn); + if (value.currentTurn !== null && currentTurn === null) return null; + return { + version: 1, + sessionId: value.sessionId, + startedAt: value.startedAt, + updatedAt: value.updatedAt, + currentTurn + }; +} +function sessionKey(sessionId2) { + return createHash("sha256").update(sessionId2).digest("hex"); +} +function defaultDataDir() { + return join2( + homedir2() || tmpdir(), + ".zcode", + "cli", + "plugins", + "data", + PLUGIN_ID + ); +} +var JsonStateStore = class { + dataDir; + constructor(dataDir = process.env.ZCODE_PLUGIN_DATA || defaultDataDir()) { + this.dataDir = dataDir; + } + async load(sessionId2) { + try { + const contents = await readFile(this.statePath(sessionId2), "utf8"); + return parseState(JSON.parse(contents)); + } catch { + return null; + } + } + async save(state) { + await mkdir(this.dataDir, { recursive: true, mode: 448 }); + const path = this.statePath(state.sessionId); + const temporaryPath = `${path}.${randomUUID()}.tmp`; + await writeFile(temporaryPath, JSON.stringify(state), { + encoding: "utf8", + mode: 384 + }); + await rename(temporaryPath, path); + } + async clear(sessionId2) { + await rm(this.statePath(sessionId2), { force: true }); + } + async withSessionLock(sessionId2, task) { + await mkdir(this.dataDir, { recursive: true, mode: 448 }); + const lockPath = this.lockPath(sessionId2); + let acquired = false; + for (let attempt = 0; attempt < LOCK_ATTEMPTS; attempt += 1) { + try { + const handle = await open(lockPath, "wx", 384); + await handle.close(); + acquired = true; + break; + } catch { + await this.removeStaleLock(lockPath); + await new Promise((resolve) => setTimeout(resolve, LOCK_WAIT_MS)); + } + } + if (!acquired) + throw new Error("Timed out acquiring the Langfuse state lock"); + try { + return await task(); + } finally { + await rm(lockPath, { force: true }); + } + } + statePath(sessionId2) { + return join2(this.dataDir, `${sessionKey(sessionId2)}.json`); + } + lockPath(sessionId2) { + return join2(this.dataDir, `${sessionKey(sessionId2)}.lock`); + } + async removeStaleLock(lockPath) { + try { + const details = await stat(lockPath); + if (Date.now() - details.mtimeMs > STALE_LOCK_MS) + await rm(lockPath, { force: true }); + } catch { + } + } +}; + +// node_modules/mustache/mustache.mjs +var objectToString = Object.prototype.toString; +var isArray = Array.isArray || function isArrayPolyfill(object) { + return objectToString.call(object) === "[object Array]"; +}; +function isFunction(object) { + return typeof object === "function"; +} +function typeStr(obj) { + return isArray(obj) ? "array" : typeof obj; +} +function escapeRegExp(string) { + return string.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, "\\$&"); +} +function hasProperty(obj, propName) { + return obj != null && typeof obj === "object" && propName in obj; +} +function primitiveHasOwnProperty(primitive, propName) { + return primitive != null && typeof primitive !== "object" && primitive.hasOwnProperty && primitive.hasOwnProperty(propName); +} +var regExpTest = RegExp.prototype.test; +function testRegExp(re, string) { + return regExpTest.call(re, string); +} +var nonSpaceRe = /\S/; +function isWhitespace(string) { + return !testRegExp(nonSpaceRe, string); +} +var entityMap = { + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + "/": "/", + "`": "`", + "=": "=" +}; +function escapeHtml(string) { + return String(string).replace(/[&<>"'`=\/]/g, function fromEntityMap(s) { + return entityMap[s]; + }); +} +var whiteRe = /\s*/; +var spaceRe = /\s+/; +var equalsRe = /\s*=/; +var curlyRe = /\s*\}/; +var tagRe = /#|\^|\/|>|\{|&|=|!/; +function parseTemplate(template, tags) { + if (!template) + return []; + var lineHasNonSpace = false; + var sections = []; + var tokens = []; + var spaces = []; + var hasTag = false; + var nonSpace = false; + var indentation = ""; + var tagIndex = 0; + function stripSpace() { + if (hasTag && !nonSpace) { + while (spaces.length) + delete tokens[spaces.pop()]; + } else { + spaces = []; + } + hasTag = false; + nonSpace = false; + } + var openingTagRe, closingTagRe, closingCurlyRe; + function compileTags(tagsToCompile) { + if (typeof tagsToCompile === "string") + tagsToCompile = tagsToCompile.split(spaceRe, 2); + if (!isArray(tagsToCompile) || tagsToCompile.length !== 2) + throw new Error("Invalid tags: " + tagsToCompile); + openingTagRe = new RegExp(escapeRegExp(tagsToCompile[0]) + "\\s*"); + closingTagRe = new RegExp("\\s*" + escapeRegExp(tagsToCompile[1])); + closingCurlyRe = new RegExp("\\s*" + escapeRegExp("}" + tagsToCompile[1])); + } + compileTags(tags || mustache.tags); + var scanner = new Scanner(template); + var start, type, value, chr, token, openSection; + while (!scanner.eos()) { + start = scanner.pos; + value = scanner.scanUntil(openingTagRe); + if (value) { + for (var i = 0, valueLength = value.length; i < valueLength; ++i) { + chr = value.charAt(i); + if (isWhitespace(chr)) { + spaces.push(tokens.length); + indentation += chr; + } else { + nonSpace = true; + lineHasNonSpace = true; + indentation += " "; + } + tokens.push(["text", chr, start, start + 1]); + start += 1; + if (chr === "\n") { + stripSpace(); + indentation = ""; + tagIndex = 0; + lineHasNonSpace = false; + } + } + } + if (!scanner.scan(openingTagRe)) + break; + hasTag = true; + type = scanner.scan(tagRe) || "name"; + scanner.scan(whiteRe); + if (type === "=") { + value = scanner.scanUntil(equalsRe); + scanner.scan(equalsRe); + scanner.scanUntil(closingTagRe); + } else if (type === "{") { + value = scanner.scanUntil(closingCurlyRe); + scanner.scan(curlyRe); + scanner.scanUntil(closingTagRe); + type = "&"; + } else { + value = scanner.scanUntil(closingTagRe); + } + if (!scanner.scan(closingTagRe)) + throw new Error("Unclosed tag at " + scanner.pos); + if (type == ">") { + token = [type, value, start, scanner.pos, indentation, tagIndex, lineHasNonSpace]; + } else { + token = [type, value, start, scanner.pos]; + } + tagIndex++; + tokens.push(token); + if (type === "#" || type === "^") { + sections.push(token); + } else if (type === "/") { + openSection = sections.pop(); + if (!openSection) + throw new Error('Unopened section "' + value + '" at ' + start); + if (openSection[1] !== value) + throw new Error('Unclosed section "' + openSection[1] + '" at ' + start); + } else if (type === "name" || type === "{" || type === "&") { + nonSpace = true; + } else if (type === "=") { + compileTags(value); + } + } + stripSpace(); + openSection = sections.pop(); + if (openSection) + throw new Error('Unclosed section "' + openSection[1] + '" at ' + scanner.pos); + return nestTokens(squashTokens(tokens)); +} +function squashTokens(tokens) { + var squashedTokens = []; + var token, lastToken; + for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { + token = tokens[i]; + if (token) { + if (token[0] === "text" && lastToken && lastToken[0] === "text") { + lastToken[1] += token[1]; + lastToken[3] = token[3]; + } else { + squashedTokens.push(token); + lastToken = token; + } + } + } + return squashedTokens; +} +function nestTokens(tokens) { + var nestedTokens = []; + var collector = nestedTokens; + var sections = []; + var token, section; + for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { + token = tokens[i]; + switch (token[0]) { + case "#": + case "^": + collector.push(token); + sections.push(token); + collector = token[4] = []; + break; + case "/": + section = sections.pop(); + section[5] = token[2]; + collector = sections.length > 0 ? sections[sections.length - 1][4] : nestedTokens; + break; + default: + collector.push(token); + } + } + return nestedTokens; +} +function Scanner(string) { + this.string = string; + this.tail = string; + this.pos = 0; +} +Scanner.prototype.eos = function eos() { + return this.tail === ""; +}; +Scanner.prototype.scan = function scan(re) { + var match = this.tail.match(re); + if (!match || match.index !== 0) + return ""; + var string = match[0]; + this.tail = this.tail.substring(string.length); + this.pos += string.length; + return string; +}; +Scanner.prototype.scanUntil = function scanUntil(re) { + var index = this.tail.search(re), match; + switch (index) { + case -1: + match = this.tail; + this.tail = ""; + break; + case 0: + match = ""; + break; + default: + match = this.tail.substring(0, index); + this.tail = this.tail.substring(index); + } + this.pos += match.length; + return match; +}; +function Context(view, parentContext) { + this.view = view; + this.cache = { ".": this.view }; + this.parent = parentContext; +} +Context.prototype.push = function push(view) { + return new Context(view, this); +}; +Context.prototype.lookup = function lookup(name) { + var cache = this.cache; + var value; + if (cache.hasOwnProperty(name)) { + value = cache[name]; + } else { + var context = this, intermediateValue, names, index, lookupHit = false; + while (context) { + if (name.indexOf(".") > 0) { + intermediateValue = context.view; + names = name.split("."); + index = 0; + while (intermediateValue != null && index < names.length) { + if (index === names.length - 1) + lookupHit = hasProperty(intermediateValue, names[index]) || primitiveHasOwnProperty(intermediateValue, names[index]); + intermediateValue = intermediateValue[names[index++]]; + } + } else { + intermediateValue = context.view[name]; + lookupHit = hasProperty(context.view, name); + } + if (lookupHit) { + value = intermediateValue; + break; + } + context = context.parent; + } + cache[name] = value; + } + if (isFunction(value)) + value = value.call(this.view); + return value; +}; +function Writer() { + this.templateCache = { + _cache: {}, + set: function set(key, value) { + this._cache[key] = value; + }, + get: function get(key) { + return this._cache[key]; + }, + clear: function clear() { + this._cache = {}; + } + }; +} +Writer.prototype.clearCache = function clearCache() { + if (typeof this.templateCache !== "undefined") { + this.templateCache.clear(); + } +}; +Writer.prototype.parse = function parse(template, tags) { + var cache = this.templateCache; + var cacheKey = template + ":" + (tags || mustache.tags).join(":"); + var isCacheEnabled = typeof cache !== "undefined"; + var tokens = isCacheEnabled ? cache.get(cacheKey) : void 0; + if (tokens == void 0) { + tokens = parseTemplate(template, tags); + isCacheEnabled && cache.set(cacheKey, tokens); + } + return tokens; +}; +Writer.prototype.render = function render(template, view, partials, config) { + var tags = this.getConfigTags(config); + var tokens = this.parse(template, tags); + var context = view instanceof Context ? view : new Context(view, void 0); + return this.renderTokens(tokens, context, partials, template, config); +}; +Writer.prototype.renderTokens = function renderTokens(tokens, context, partials, originalTemplate, config) { + var buffer = ""; + var token, symbol, value; + for (var i = 0, numTokens = tokens.length; i < numTokens; ++i) { + value = void 0; + token = tokens[i]; + symbol = token[0]; + if (symbol === "#") value = this.renderSection(token, context, partials, originalTemplate, config); + else if (symbol === "^") value = this.renderInverted(token, context, partials, originalTemplate, config); + else if (symbol === ">") value = this.renderPartial(token, context, partials, config); + else if (symbol === "&") value = this.unescapedValue(token, context); + else if (symbol === "name") value = this.escapedValue(token, context, config); + else if (symbol === "text") value = this.rawValue(token); + if (value !== void 0) + buffer += value; + } + return buffer; +}; +Writer.prototype.renderSection = function renderSection(token, context, partials, originalTemplate, config) { + var self = this; + var buffer = ""; + var value = context.lookup(token[1]); + function subRender(template) { + return self.render(template, context, partials, config); + } + if (!value) return; + if (isArray(value)) { + for (var j = 0, valueLength = value.length; j < valueLength; ++j) { + buffer += this.renderTokens(token[4], context.push(value[j]), partials, originalTemplate, config); + } + } else if (typeof value === "object" || typeof value === "string" || typeof value === "number") { + buffer += this.renderTokens(token[4], context.push(value), partials, originalTemplate, config); + } else if (isFunction(value)) { + if (typeof originalTemplate !== "string") + throw new Error("Cannot use higher-order sections without the original template"); + value = value.call(context.view, originalTemplate.slice(token[3], token[5]), subRender); + if (value != null) + buffer += value; + } else { + buffer += this.renderTokens(token[4], context, partials, originalTemplate, config); + } + return buffer; +}; +Writer.prototype.renderInverted = function renderInverted(token, context, partials, originalTemplate, config) { + var value = context.lookup(token[1]); + if (!value || isArray(value) && value.length === 0) + return this.renderTokens(token[4], context, partials, originalTemplate, config); +}; +Writer.prototype.indentPartial = function indentPartial(partial, indentation, lineHasNonSpace) { + var filteredIndentation = indentation.replace(/[^ \t]/g, ""); + var partialByNl = partial.split("\n"); + for (var i = 0; i < partialByNl.length; i++) { + if (partialByNl[i].length && (i > 0 || !lineHasNonSpace)) { + partialByNl[i] = filteredIndentation + partialByNl[i]; + } + } + return partialByNl.join("\n"); +}; +Writer.prototype.renderPartial = function renderPartial(token, context, partials, config) { + if (!partials) return; + var tags = this.getConfigTags(config); + var value = isFunction(partials) ? partials(token[1]) : partials[token[1]]; + if (value != null) { + var lineHasNonSpace = token[6]; + var tagIndex = token[5]; + var indentation = token[4]; + var indentedValue = value; + if (tagIndex == 0 && indentation) { + indentedValue = this.indentPartial(value, indentation, lineHasNonSpace); + } + var tokens = this.parse(indentedValue, tags); + return this.renderTokens(tokens, context, partials, indentedValue, config); + } +}; +Writer.prototype.unescapedValue = function unescapedValue(token, context) { + var value = context.lookup(token[1]); + if (value != null) + return value; +}; +Writer.prototype.escapedValue = function escapedValue(token, context, config) { + var escape = this.getConfigEscape(config) || mustache.escape; + var value = context.lookup(token[1]); + if (value != null) + return typeof value === "number" && escape === mustache.escape ? String(value) : escape(value); +}; +Writer.prototype.rawValue = function rawValue(token) { + return token[1]; +}; +Writer.prototype.getConfigTags = function getConfigTags(config) { + if (isArray(config)) { + return config; + } else if (config && typeof config === "object") { + return config.tags; + } else { + return void 0; + } +}; +Writer.prototype.getConfigEscape = function getConfigEscape(config) { + if (config && typeof config === "object" && !isArray(config)) { + return config.escape; + } else { + return void 0; + } +}; +var mustache = { + name: "mustache.js", + version: "4.2.0", + tags: ["{{", "}}"], + clearCache: void 0, + escape: void 0, + parse: void 0, + render: void 0, + Scanner: void 0, + Context: void 0, + Writer: void 0, + /** + * Allows a user to override the default caching strategy, by providing an + * object with set, get and clear methods. This can also be used to disable + * the cache by setting it to the literal `undefined`. + */ + set templateCache(cache) { + defaultWriter.templateCache = cache; + }, + /** + * Gets the default or overridden caching object from the default writer. + */ + get templateCache() { + return defaultWriter.templateCache; + } +}; +var defaultWriter = new Writer(); +mustache.clearCache = function clearCache2() { + return defaultWriter.clearCache(); +}; +mustache.parse = function parse2(template, tags) { + return defaultWriter.parse(template, tags); +}; +mustache.render = function render2(template, view, partials, config) { + if (typeof template !== "string") { + throw new TypeError('Invalid template! Template should be a "string" but "' + typeStr(template) + '" was given as the first argument for mustache#render(template, view, partials)'); + } + return defaultWriter.render(template, view, partials, config); +}; +mustache.escape = escapeHtml; +mustache.Scanner = Scanner; +mustache.Context = Context; +mustache.Writer = Writer; +var mustache_default = mustache; + +// node_modules/langfuse-core/lib/index.mjs +var SimpleEventEmitter = class { + constructor() { + this.events = {}; + this.events = {}; + } + on(event, listener) { + if (!this.events[event]) { + this.events[event] = []; + } + this.events[event].push(listener); + return () => { + this.events[event] = this.events[event].filter((x) => x !== listener); + }; + } + emit(event, payload) { + for (const listener of this.events[event] || []) { + listener(payload); + } + for (const listener of this.events["*"] || []) { + listener(event, payload); + } + } +}; +var DEFAULT_PROMPT_CACHE_TTL_SECONDS = 60; +var LangfusePromptCacheItem = class { + constructor(value, ttlSeconds) { + this.value = value; + this._expiry = Date.now() + ttlSeconds * 1e3; + } + get isExpired() { + return Date.now() > this._expiry; + } +}; +var LangfusePromptCache = class { + constructor() { + this._cache = /* @__PURE__ */ new Map(); + this._defaultTtlSeconds = DEFAULT_PROMPT_CACHE_TTL_SECONDS; + this._refreshingKeys = /* @__PURE__ */ new Map(); + } + getIncludingExpired(key) { + return this._cache.get(key) ?? null; + } + set(key, value, ttlSeconds) { + const effectiveTtlSeconds = ttlSeconds ?? this._defaultTtlSeconds; + this._cache.set(key, new LangfusePromptCacheItem(value, effectiveTtlSeconds)); + } + addRefreshingPromise(key, promise) { + this._refreshingKeys.set(key, promise); + promise.then(() => { + this._refreshingKeys.delete(key); + }).catch(() => { + this._refreshingKeys.delete(key); + }); + } + isRefreshing(key) { + return this._refreshingKeys.has(key); + } + invalidate(promptName) { + console.log("invalidating", promptName, this._cache.keys()); + for (const key of this._cache.keys()) { + if (key.startsWith(promptName)) { + this._cache.delete(key); + } + } + } +}; +var LangfusePersistedProperty; +(function(LangfusePersistedProperty2) { + LangfusePersistedProperty2["Props"] = "props"; + LangfusePersistedProperty2["Queue"] = "queue"; + LangfusePersistedProperty2["OptedOut"] = "opted_out"; +})(LangfusePersistedProperty || (LangfusePersistedProperty = {})); +var ChatMessageType; +(function(ChatMessageType2) { + ChatMessageType2["ChatMessage"] = "chatmessage"; + ChatMessageType2["Placeholder"] = "placeholder"; +})(ChatMessageType || (ChatMessageType = {})); +mustache_default.escape = function(text) { + return text; +}; +var BasePromptClient = class { + constructor(prompt2, isFallback = false, type) { + this.name = prompt2.name; + this.version = prompt2.version; + this.config = prompt2.config; + this.labels = prompt2.labels; + this.tags = prompt2.tags; + this.isFallback = isFallback; + this.type = type; + this.commitMessage = prompt2.commitMessage; + } + _transformToLangchainVariables(content) { + const jsonEscapedContent = this.escapeJsonForLangchain(content); + return jsonEscapedContent.replace(/\{\{(\w+)\}\}/g, "{$1}"); + } + /** + * Escapes every curly brace that is part of a JSON object by doubling it. + * + * A curly brace is considered “JSON-related” when, after skipping any immediate + * whitespace, the next non-whitespace character is a single (') or double (") quote. + * + * Braces that are already doubled (e.g. `{{variable}}` placeholders) are left untouched. + * + * @param text - Input string that may contain JSON snippets. + * @returns The string with JSON-related braces doubled. + */ + escapeJsonForLangchain(text) { + const out = []; + const stack = []; + let i = 0; + const n = text.length; + while (i < n) { + const ch = text[i]; + if (ch === "{") { + if (i + 1 < n && text[i + 1] === "{") { + out.push("{{"); + i += 2; + continue; + } + let j = i + 1; + while (j < n && /\s/.test(text[j])) { + j++; + } + const isJson = j < n && (text[j] === "'" || text[j] === '"'); + out.push(isJson ? "{{" : "{"); + stack.push(isJson); + i += 1; + continue; + } + if (ch === "}") { + if (i + 1 < n && text[i + 1] === "}") { + out.push("}}"); + i += 2; + continue; + } + const isJson = stack.pop() ?? false; + out.push(isJson ? "}}" : "}"); + i += 1; + continue; + } + out.push(ch); + i += 1; + } + return out.join(""); + } +}; +var TextPromptClient = class extends BasePromptClient { + constructor(prompt2, isFallback = false) { + super(prompt2, isFallback, "text"); + this.promptResponse = prompt2; + this.prompt = prompt2.prompt; + } + compile(variables, _placeholders) { + return mustache_default.render(this.promptResponse.prompt, variables ?? {}); + } + getLangchainPrompt(_options) { + return this._transformToLangchainVariables(this.prompt); + } + toJSON() { + return JSON.stringify({ + name: this.name, + prompt: this.prompt, + version: this.version, + isFallback: this.isFallback, + tags: this.tags, + labels: this.labels, + type: this.type, + config: this.config + }); + } +}; +var ChatPromptClient = class _ChatPromptClient extends BasePromptClient { + constructor(prompt2, isFallback = false) { + const normalizedPrompt = _ChatPromptClient.normalizePrompt(prompt2.prompt); + const typedPrompt = { + ...prompt2, + prompt: normalizedPrompt + }; + super(typedPrompt, isFallback, "chat"); + this.promptResponse = typedPrompt; + this.prompt = normalizedPrompt; + } + static normalizePrompt(prompt2) { + return prompt2.map((item) => { + if ("type" in item) { + return item; + } else { + return { + type: ChatMessageType.ChatMessage, + ...item + }; + } + }); + } + compile(variables, placeholders) { + const messagesWithPlaceholdersReplaced = []; + const placeholderValues = placeholders ?? {}; + for (const item of this.prompt) { + if ("type" in item && item.type === ChatMessageType.Placeholder) { + const placeholderValue = placeholderValues[item.name]; + if (Array.isArray(placeholderValue) && placeholderValue.length > 0 && placeholderValue.every((msg) => typeof msg === "object" && "role" in msg && "content" in msg)) { + messagesWithPlaceholdersReplaced.push(...placeholderValue); + } else if (Array.isArray(placeholderValue) && placeholderValue.length === 0) ; + else if (placeholderValue !== void 0) { + messagesWithPlaceholdersReplaced.push(JSON.stringify(placeholderValue)); + } else { + messagesWithPlaceholdersReplaced.push(item); + } + } else if ("role" in item && "content" in item && item.type === ChatMessageType.ChatMessage) { + messagesWithPlaceholdersReplaced.push({ + role: item.role, + content: item.content + }); + } + } + return messagesWithPlaceholdersReplaced.map((item) => { + if (typeof item === "object" && item !== null && "role" in item && "content" in item) { + return { + ...item, + content: mustache_default.render(item.content, variables ?? {}) + }; + } else { + return item; + } + }); + } + getLangchainPrompt(options) { + const messagesWithPlaceholdersReplaced = []; + const placeholderValues = options?.placeholders ?? {}; + for (const item of this.prompt) { + if ("type" in item && item.type === ChatMessageType.Placeholder) { + const placeholderValue = placeholderValues[item.name]; + if (Array.isArray(placeholderValue) && placeholderValue.length > 0 && placeholderValue.every((msg) => typeof msg === "object" && "role" in msg && "content" in msg)) { + messagesWithPlaceholdersReplaced.push(...placeholderValue.map((msg) => { + return { + role: msg.role, + content: this._transformToLangchainVariables(msg.content) + }; + })); + } else if (Array.isArray(placeholderValue) && placeholderValue.length === 0) ; + else if (placeholderValue !== void 0) { + messagesWithPlaceholdersReplaced.push(JSON.stringify(placeholderValue)); + } else { + messagesWithPlaceholdersReplaced.push({ + variableName: item.name, + optional: false + }); + } + } else if ("role" in item && "content" in item && item.type === ChatMessageType.ChatMessage) { + messagesWithPlaceholdersReplaced.push({ + role: item.role, + content: this._transformToLangchainVariables(item.content) + }); + } + } + return messagesWithPlaceholdersReplaced; + } + toJSON() { + return JSON.stringify({ + name: this.name, + prompt: this.promptResponse.prompt.map((item) => { + if ("type" in item && item.type === ChatMessageType.ChatMessage) { + const { + type: _, + ...messageWithoutType + } = item; + return messageWithoutType; + } + return item; + }), + version: this.version, + isFallback: this.isFallback, + tags: this.tags, + labels: this.labels, + type: this.type, + config: this.config + }); + } +}; +function assert(truthyValue, message) { + if (!truthyValue) { + throw new Error(message); + } +} +function removeTrailingSlash(url) { + return url?.replace(/\/+$/, ""); +} +async function retriable(fn, props = {}, log) { + const { + retryCount = 3, + retryDelay = 5e3, + retryCheck = () => true + } = props; + let lastError = null; + for (let i = 0; i < retryCount + 1; i++) { + if (i > 0) { + await new Promise((resolve) => setTimeout(resolve, retryDelay)); + log(`Retrying ${i + 1} of ${retryCount + 1}`); + } + try { + const res = await fn(); + return res; + } catch (e) { + lastError = e; + if (!retryCheck(e)) { + throw e; + } + log(`Retriable error: ${JSON.stringify(e)}`); + } + } + throw lastError; +} +function generateUUID(globalThis2) { + let d = (/* @__PURE__ */ new Date()).getTime(); + let d2 = globalThis2 && globalThis2.performance && globalThis2.performance.now && globalThis2.performance.now() * 1e3 || 0; + return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) { + let r = Math.random() * 16; + if (d > 0) { + r = (d + r) % 16 | 0; + d = Math.floor(d / 16); + } else { + r = (d2 + r) % 16 | 0; + d2 = Math.floor(d2 / 16); + } + return (c === "x" ? r : r & 3 | 8).toString(16); + }); +} +function currentTimestamp() { + return (/* @__PURE__ */ new Date()).getTime(); +} +function currentISOTime() { + return (/* @__PURE__ */ new Date()).toISOString(); +} +function safeSetTimeout(fn, timeout) { + const t = setTimeout(fn, timeout); + t?.unref && t?.unref(); + return t; +} +function getEnv(key) { + if (typeof process !== "undefined" && process.env[key]) { + return process.env[key]; + } else if (typeof globalThis !== "undefined") { + return globalThis[key]; + } + return; +} +function configLangfuseSDK(params, secretRequired = true) { + const { + publicKey, + secretKey, + ...coreOptions + } = params ?? {}; + const finalPublicKey = publicKey ?? getEnv("LANGFUSE_PUBLIC_KEY"); + const finalSecretKey = secretRequired ? secretKey ?? getEnv("LANGFUSE_SECRET_KEY") : void 0; + const finalBaseUrl = coreOptions.baseUrl ?? getEnv("LANGFUSE_BASEURL"); + const finalCoreOptions = { + ...coreOptions, + baseUrl: finalBaseUrl + }; + return { + publicKey: finalPublicKey, + ...secretRequired ? { + secretKey: finalSecretKey + } : void 0, + ...finalCoreOptions + }; +} +var encodeQueryParams = (params) => { + const queryParams = new URLSearchParams(); + Object.entries(params ?? {}).forEach(([key, value]) => { + if (value !== void 0 && value !== null) { + if (value instanceof Date) { + queryParams.append(key, value.toISOString()); + } else { + queryParams.append(key, value.toString()); + } + } + }); + return queryParams.toString(); +}; +var utils = /* @__PURE__ */ Object.freeze({ + __proto__: null, + assert, + configLangfuseSDK, + currentISOTime, + currentTimestamp, + encodeQueryParams, + generateUUID, + getEnv, + removeTrailingSlash, + retriable, + safeSetTimeout +}); +var common_release_envs = [ + // Vercel + "VERCEL_GIT_COMMIT_SHA", + "NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA", + // Netlify + "COMMIT_REF", + // Render + "RENDER_GIT_COMMIT", + // GitLab CI + "CI_COMMIT_SHA", + // CicleCI + "CIRCLE_SHA1", + // Cloudflare pages + "CF_PAGES_COMMIT_SHA", + // AWS Amplify + "REACT_APP_GIT_SHA", + // Heroku + "SOURCE_VERSION", + // Trigger.dev + "TRIGGER_DEPLOYMENT_ID" +]; +function getCommonReleaseEnvs() { + for (const key of common_release_envs) { + const value = getEnv(key); + if (value) { + return value; + } + } + return void 0; +} +var fs = null; +var cryptoModule = null; +var dynamicImport = (module) => { + return import( + /* webpackIgnore: true */ + module + ); +}; +if (typeof globalThis.Deno !== "undefined") { + Promise.all([dynamicImport("node:fs"), dynamicImport("node:crypto")]).then(([importedFs, importedCrypto]) => { + fs = importedFs; + cryptoModule = importedCrypto; + }).catch(); +} else if (typeof process !== "undefined" && process.versions?.node) { + Promise.all([dynamicImport("fs"), dynamicImport("crypto")]).then(([importedFs, importedCrypto]) => { + fs = importedFs; + cryptoModule = importedCrypto; + }).catch(); +} else if (typeof crypto !== "undefined") { + cryptoModule = crypto; +} +var LangfuseMedia = class _LangfuseMedia { + constructor(params) { + const { + obj, + base64DataUri, + contentType, + contentBytes, + filePath + } = params; + this.obj = obj; + this._mediaId = void 0; + if (base64DataUri) { + const [contentBytesParsed, contentTypeParsed] = this.parseBase64DataUri(base64DataUri); + this._contentBytes = contentBytesParsed; + this._contentType = contentTypeParsed; + this._source = "base64_data_uri"; + } else if (contentBytes && contentType) { + this._contentBytes = contentBytes; + this._contentType = contentType; + this._source = "bytes"; + } else if (filePath && contentType) { + if (!fs) { + throw new Error("File system support is not available in this environment"); + } + if (!fs.existsSync(filePath)) { + throw new Error(`File at path ${filePath} does not exist`); + } + this._contentBytes = this.readFile(filePath); + this._contentType = this._contentBytes ? contentType : void 0; + this._source = this._contentBytes ? "file" : void 0; + } else { + console.error("base64DataUri, or contentBytes and contentType, or filePath must be provided to LangfuseMedia"); + } + } + readFile(filePath) { + try { + if (!fs) { + throw new Error("File system support is not available in this environment"); + } + return fs.readFileSync(filePath); + } catch (error) { + console.error(`Error reading file at path ${filePath}`, error); + return void 0; + } + } + parseBase64DataUri(data) { + try { + if (!data || typeof data !== "string") { + throw new Error("Data URI is not a string"); + } + if (!data.startsWith("data:")) { + throw new Error("Data URI does not start with 'data:'"); + } + const [header, actualData] = data.slice(5).split(",", 2); + if (!header || !actualData) { + throw new Error("Invalid URI"); + } + const headerParts = header.split(";"); + if (!headerParts.includes("base64")) { + throw new Error("Data is not base64 encoded"); + } + const contentType = headerParts[0]; + if (!contentType) { + throw new Error("Content type is empty"); + } + return [Buffer.from(actualData, "base64"), contentType]; + } catch (error) { + console.error("Error parsing base64 data URI", error); + return [void 0, void 0]; + } + } + get contentLength() { + return this._contentBytes?.length; + } + get contentSha256Hash() { + if (!this._contentBytes) { + return void 0; + } + if (!cryptoModule) { + console.error("Crypto support is not available in this environment"); + return void 0; + } + const sha256Hash = cryptoModule.createHash("sha256").update(this._contentBytes).digest("base64"); + return sha256Hash; + } + toJSON() { + if (!this._contentType || !this._source || !this._mediaId) { + return ``; + } + return `@@@langfuseMedia:type=${this._contentType}|id=${this._mediaId}|source=${this._source}@@@`; + } + /** + * Parses a media reference string into a ParsedMediaReference. + * + * Example reference string: + * "@@@langfuseMedia:type=image/jpeg|id=some-uuid|source=base64DataUri@@@" + * + * @param referenceString - The reference string to parse. + * @returns An object with the mediaId, source, and contentType. + * + * @throws Error if the reference string is invalid or missing required fields. + */ + static parseReferenceString(referenceString) { + const prefix = "@@@langfuseMedia:"; + const suffix = "@@@"; + if (!referenceString.startsWith(prefix)) { + throw new Error("Reference string does not start with '@@@langfuseMedia:type='"); + } + if (!referenceString.endsWith(suffix)) { + throw new Error("Reference string does not end with '@@@'"); + } + const content = referenceString.slice(prefix.length, -suffix.length); + const pairs = content.split("|"); + const parsedData = {}; + for (const pair of pairs) { + const [key, value] = pair.split("=", 2); + parsedData[key] = value; + } + if (!("type" in parsedData && "id" in parsedData && "source" in parsedData)) { + throw new Error("Missing required fields in reference string"); + } + return { + mediaId: parsedData["id"], + source: parsedData["source"], + contentType: parsedData["type"] + }; + } + /** + * Replaces the media reference strings in an object with base64 data URIs for the media content. + * + * This method recursively traverses an object (up to a maximum depth of 10) looking for media reference strings + * in the format "@@@langfuseMedia:...@@@". When found, it fetches the actual media content using the provided + * Langfuse client and replaces the reference string with a base64 data URI. + * + * If fetching media content fails for a reference string, a warning is logged and the reference string is left unchanged. + * + * @param params - Configuration object + * @param params.obj - The object to process. Can be a primitive value, array, or nested object + * @param params.langfuseClient - Langfuse client instance used to fetch media content + * @param params.resolveWith - The representation of the media content to replace the media reference string with. Currently only "base64DataUri" is supported. + * @param params.maxDepth - Optional. Default is 10. The maximum depth to traverse the object. + * + * @returns A deep copy of the input object with all media references replaced with base64 data URIs where possible + * + * @example + * ```typescript + * const obj = { + * image: "@@@langfuseMedia:type=image/jpeg|id=123|source=bytes@@@", + * nested: { + * pdf: "@@@langfuseMedia:type=application/pdf|id=456|source=bytes@@@" + * } + * }; + * + * const result = await LangfuseMedia.resolveMediaReferences({ + * obj, + * langfuseClient + * }); + * + * // Result: + * // { + * // image: "data:image/jpeg;base64,/9j/4AAQSkZJRg...", + * // nested: { + * // pdf: "data:application/pdf;base64,JVBERi0xLjcK..." + * // } + * // } + * ``` + */ + static async resolveMediaReferences(params) { + const { + obj, + langfuseClient, + maxDepth = 10 + } = params; + async function traverse(obj2, depth) { + if (depth > maxDepth) { + return obj2; + } + if (typeof obj2 === "string") { + const regex = /@@@langfuseMedia:.+?@@@/g; + const referenceStringMatches = obj2.match(regex); + if (!referenceStringMatches) { + return obj2; + } + let result = obj2; + const referenceStringToMediaContentMap = /* @__PURE__ */ new Map(); + await Promise.all(referenceStringMatches.map(async (referenceString) => { + try { + const parsedMediaReference = _LangfuseMedia.parseReferenceString(referenceString); + const mediaData = await langfuseClient.fetchMedia(parsedMediaReference.mediaId); + const mediaContent = await langfuseClient.fetch(mediaData.url, { + method: "GET", + headers: {} + }); + if (mediaContent.status !== 200) { + throw new Error("Failed to fetch media content"); + } + const base64MediaContent = Buffer.from(await mediaContent.arrayBuffer()).toString("base64"); + const base64DataUri = `data:${mediaData.contentType};base64,${base64MediaContent}`; + referenceStringToMediaContentMap.set(referenceString, base64DataUri); + } catch (error) { + console.warn("Error fetching media content for reference string", referenceString, error); + } + })); + for (const [referenceString, base64MediaContent] of referenceStringToMediaContentMap.entries()) { + result = result.replaceAll(referenceString, base64MediaContent); + } + return result; + } + if (Array.isArray(obj2)) { + return Promise.all(obj2.map(async (item) => await traverse(item, depth + 1))); + } + if (typeof obj2 === "object" && obj2 !== null) { + return Object.fromEntries(await Promise.all(Object.entries(obj2).map(async ([key, value]) => [key, await traverse(value, depth + 1)]))); + } + return obj2; + } + return traverse(obj, 0); + } +}; +function isInSample(input, sampleRate) { + if (sampleRate === void 0) { + return true; + } else if (sampleRate === 0) { + return false; + } + if (sampleRate < 0 || sampleRate > 1 || isNaN(sampleRate)) { + console.warn("Sample rate must be between 0 and 1. Ignoring setting."); + return true; + } + return simpleHash(input) < sampleRate; +} +function simpleHash(str) { + let hash = 0; + const prime = 31; + for (let i = 0; i < str.length; i++) { + hash = hash * prime + str.charCodeAt(i) >>> 0; + } + hash = (hash >>> 16 ^ hash) * 73244475; + hash = (hash >>> 16 ^ hash) * 73244475; + hash = hash >>> 16 ^ hash; + return Math.abs(hash) / 2147483647; +} +var MAX_EVENT_SIZE_BYTES = getEnv("LANGFUSE_MAX_EVENT_SIZE_BYTES") ? Number(getEnv("LANGFUSE_MAX_EVENT_SIZE_BYTES")) : 1e6; +var MAX_BATCH_SIZE_BYTES = getEnv("LANGFUSE_MAX_BATCH_SIZE_BYTES") ? Number(getEnv("LANGFUSE_MAX_BATCH_SIZE_BYTES")) : 25e5; +var ENVIRONMENT_PATTERN = /^(?!langfuse)[a-z0-9_-]+$/; +var WHITELISTED_LANGFUSE_INTERNAL_ENVIRONMENTS = ["langfuse-prompt-experiment"]; +var LangfuseFetchHttpError = class extends Error { + constructor(response, body) { + super("HTTP error while fetching Langfuse: " + response.status + " and body: " + body); + this.response = response; + this.name = "LangfuseFetchHttpError"; + } +}; +var LangfuseFetchNetworkError = class extends Error { + constructor(error) { + super("Network error while fetching Langfuse", error instanceof Error ? { + cause: error + } : {}); + this.error = error; + this.name = "LangfuseFetchNetworkError"; + } +}; +function isLangfuseFetchHttpError(error) { + return typeof error === "object" && error.name === "LangfuseFetchHttpError"; +} +function isLangfuseFetchNetworkError(error) { + return typeof error === "object" && error.name === "LangfuseFetchNetworkError"; +} +function isLangfuseFetchError(err) { + return isLangfuseFetchHttpError(err) || isLangfuseFetchNetworkError(err); +} +var SUPPORT_URL = "https://langfuse.com/support"; +var API_DOCS_URL = "https://api.reference.langfuse.com"; +var RBAC_DOCS_URL = "https://langfuse.com/docs/rbac"; +var INSTALLATION_DOCS_URL = "https://langfuse.com/docs/sdk/typescript/guide"; +var RATE_LIMITS_URL = "https://langfuse.com/faq/all/api-limits"; +var NPM_PACKAGE_URL = "https://www.npmjs.com/package/langfuse"; +var updatePromptResponse = `Make sure to keep your SDK updated, refer to ${NPM_PACKAGE_URL} for details.`; +var defaultServerErrorPrompt = `This is an unusual occurrence and we are monitoring it closely. For help, please contact support: ${SUPPORT_URL}.`; +var defaultErrorResponse = `Unexpected error occurred. Please check your request and contact support: ${SUPPORT_URL}.`; +var errorResponseByCode = /* @__PURE__ */ new Map([ + // Internal error category: 5xx errors, 404 error + [500, `Internal server error occurred. For help, please contact support: ${SUPPORT_URL}`], + [501, `Not implemented. Please check your request and contact support for help: ${SUPPORT_URL}.`], + [502, `Bad gateway. ${defaultServerErrorPrompt}`], + [503, `Service unavailable. ${defaultServerErrorPrompt}`], + [504, `Gateway timeout. ${defaultServerErrorPrompt}`], + [404, `Internal error occurred. ${defaultServerErrorPrompt}`], + // Client error category: 4xx errors, excluding 404 + [400, `Bad request. Please check your request for any missing or incorrect parameters. Refer to our API docs: ${API_DOCS_URL} for details.`], + [401, `Unauthorized. Please check your public/private host settings. Refer to our installation and setup guide: ${INSTALLATION_DOCS_URL} for details on SDK configuration.`], + [403, `Forbidden. Please check your access control settings. Refer to our RBAC docs: ${RBAC_DOCS_URL} for details.`], + [429, `Rate limit exceeded. For more information on rate limits please see: ${RATE_LIMITS_URL}`] +]); +function getErrorResponseByCode(code) { + if (!code) { + return `${defaultErrorResponse} ${updatePromptResponse}`; + } + const errorResponse = errorResponseByCode.get(code) || defaultErrorResponse; + return `${code}: ${errorResponse} ${updatePromptResponse}`; +} +function logIngestionError(error) { + if (isLangfuseFetchHttpError(error)) { + const code = error.response.status; + const errorResponse = getErrorResponseByCode(code); + console.error("[Langfuse SDK]", errorResponse, `Error details: ${error}`); + } else if (isLangfuseFetchNetworkError(error)) { + console.error("[Langfuse SDK] Network error: ", error); + } else { + console.error("[Langfuse SDK] Unknown error:", error); + } +} +var LangfuseCoreStateless = class { + constructor(params) { + this.additionalHeaders = {}; + this.debugMode = false; + this.pendingEventProcessingPromises = {}; + this.pendingIngestionPromises = {}; + this.localEventExportMap = /* @__PURE__ */ new Map(); + this._events = new SimpleEventEmitter(); + const { + publicKey, + secretKey, + enabled, + _projectId, + _isLocalEventExportEnabled, + ...options + } = params; + this._events.on("error", (payload) => { + console.error(`[Langfuse SDK] ${typeof payload === "string" ? payload : JSON.stringify(payload)}`); + }); + this.enabled = enabled === false ? false : true; + this.publicKey = publicKey ?? ""; + this.secretKey = secretKey; + this.baseUrl = removeTrailingSlash(options?.baseUrl || "https://cloud.langfuse.com"); + this.additionalHeaders = options?.additionalHeaders || {}; + this.flushAt = options?.flushAt ? Math.max(options?.flushAt, 1) : 15; + this.flushInterval = options?.flushInterval ?? 1e4; + this.release = options?.release ?? getEnv("LANGFUSE_RELEASE") ?? getCommonReleaseEnvs() ?? void 0; + this.mask = options?.mask; + this.sampleRate = options?.sampleRate ?? (getEnv("LANGFUSE_SAMPLE_RATE") ? Number(getEnv("LANGFUSE_SAMPLE_RATE")) : void 0); + if (this.sampleRate) { + this._events.emit("debug", `Langfuse trace sampling enabled with sampleRate ${this.sampleRate}.`); + } + this.environment = options?.environment ?? getEnv("LANGFUSE_TRACING_ENVIRONMENT"); + if (this.environment && !(ENVIRONMENT_PATTERN.test(this.environment) || WHITELISTED_LANGFUSE_INTERNAL_ENVIRONMENTS.includes(this.environment)) && !_isLocalEventExportEnabled) { + this._events.emit("error", `Invalid tracing environment set: ${this.environment} . Environment must match regex ${ENVIRONMENT_PATTERN}. Events will be rejected by Langfuse server.`); + } + this._retryOptions = { + retryCount: options?.fetchRetryCount ?? 3, + retryDelay: options?.fetchRetryDelay ?? 3e3, + retryCheck: isLangfuseFetchError + }; + this.requestTimeout = options?.requestTimeout ?? 5e3; + this.sdkIntegration = options?.sdkIntegration ?? "DEFAULT"; + this.isLocalEventExportEnabled = _isLocalEventExportEnabled ?? false; + if (this.isLocalEventExportEnabled && !_projectId) { + this._events.emit("error", "Local event export is enabled, but no project ID was provided. Disabling local export."); + this.isLocalEventExportEnabled = false; + return; + } else if (!this.isLocalEventExportEnabled && _projectId) { + this._events.emit("error", "Local event export is disabled, but a project ID was provided. Disabling local export."); + this.isLocalEventExportEnabled = false; + return; + } else { + this.projectId = _projectId; + } + } + getSdkIntegration() { + return this.sdkIntegration; + } + getCommonEventProperties() { + return { + $lib: this.getLibraryId(), + $lib_version: this.getLibraryVersion() + }; + } + on(event, cb) { + return this._events.on(event, cb); + } + debug(enabled = true) { + this.removeDebugCallback?.(); + this.debugMode = enabled; + if (enabled) { + this.removeDebugCallback = this.on("*", (event, payload) => { + if (event === "error") { + return; + } + console.log("[Langfuse Debug]", event, JSON.stringify(payload)); + }); + } + } + /*** + *** Handlers for each object type + ***/ + traceStateless(body) { + const { + id: bodyId, + timestamp: bodyTimestamp, + release: bodyRelease, + ...rest + } = body; + const id = bodyId ?? generateUUID(); + const release = bodyRelease ?? this.release; + const parsedBody = { + id, + release, + timestamp: bodyTimestamp ?? /* @__PURE__ */ new Date(), + environment: this.environment, + ...rest + }; + this.enqueue("trace-create", parsedBody); + return id; + } + eventStateless(body) { + const { + id: bodyId, + startTime: bodyStartTime, + ...rest + } = body; + const id = bodyId ?? generateUUID(); + const parsedBody = { + id, + startTime: bodyStartTime ?? /* @__PURE__ */ new Date(), + environment: this.environment, + ...rest + }; + this.enqueue("event-create", parsedBody); + return id; + } + spanStateless(body) { + const { + id: bodyId, + startTime: bodyStartTime, + ...rest + } = body; + const id = bodyId || generateUUID(); + const parsedBody = { + id, + startTime: bodyStartTime ?? /* @__PURE__ */ new Date(), + environment: this.environment, + ...rest + }; + this.enqueue("span-create", parsedBody); + return id; + } + generationStateless(body) { + const { + id: bodyId, + startTime: bodyStartTime, + prompt: prompt2, + ...rest + } = body; + const promptDetails = prompt2 && !prompt2.isFallback ? { + promptName: prompt2.name, + promptVersion: prompt2.version + } : {}; + const id = bodyId || generateUUID(); + const parsedBody = { + id, + startTime: bodyStartTime ?? /* @__PURE__ */ new Date(), + environment: this.environment, + ...promptDetails, + ...rest + }; + this.enqueue("generation-create", parsedBody); + return id; + } + scoreStateless(body) { + const { + id: bodyId, + ...rest + } = body; + const id = bodyId || generateUUID(); + const parsedBody = { + id, + environment: this.environment, + ...rest + }; + this.enqueue("score-create", parsedBody); + return id; + } + updateSpanStateless(body) { + this.enqueue("span-update", body); + return body.id; + } + updateGenerationStateless(body) { + const { + prompt: prompt2, + ...rest + } = body; + const promptDetails = prompt2 && !prompt2.isFallback ? { + promptName: prompt2.name, + promptVersion: prompt2.version + } : {}; + const parsedBody = { + ...promptDetails, + ...rest + }; + this.enqueue("generation-update", parsedBody); + return body.id; + } + async _getDataset(name) { + const encodedName = encodeURIComponent(name); + return this.fetchAndLogErrors(`${this.baseUrl}/api/public/v2/datasets/${encodedName}`, this._getFetchOptions({ + method: "GET" + })); + } + async _getDatasetItems(query) { + const params = new URLSearchParams(); + Object.entries(query ?? {}).forEach(([key, value]) => { + if (value !== void 0 && value !== null) { + params.append(key, value.toString()); + } + }); + return this.fetchAndLogErrors(`${this.baseUrl}/api/public/dataset-items?${params}`, this._getFetchOptions({ + method: "GET" + })); + } + async _fetchMedia(id) { + return this.fetchAndLogErrors(`${this.baseUrl}/api/public/media/${id}`, this._getFetchOptions({ + method: "GET" + })); + } + async fetchTraces(query) { + const { + data, + meta + } = await this.fetchAndLogErrors(`${this.baseUrl}/api/public/traces?${encodeQueryParams(query)}`, this._getFetchOptions({ + method: "GET" + })); + return { + data, + meta + }; + } + async fetchTrace(traceId) { + const res = await this.fetchAndLogErrors(`${this.baseUrl}/api/public/traces/${traceId}`, this._getFetchOptions({ + method: "GET" + })); + return { + data: res + }; + } + async fetchObservations(query) { + const { + data, + meta + } = await this.fetchAndLogErrors(`${this.baseUrl}/api/public/observations?${encodeQueryParams(query)}`, this._getFetchOptions({ + method: "GET" + })); + return { + data, + meta + }; + } + async fetchObservation(observationId) { + const res = await this.fetchAndLogErrors(`${this.baseUrl}/api/public/observations/${observationId}`, this._getFetchOptions({ + method: "GET" + })); + return { + data: res + }; + } + async fetchSessions(query) { + const { + data, + meta + } = await this.fetchAndLogErrors(`${this.baseUrl}/api/public/sessions?${encodeQueryParams(query)}`, this._getFetchOptions({ + method: "GET" + })); + return { + data, + meta + }; + } + async getDatasetRun(params) { + const encodedDatasetName = encodeURIComponent(params.datasetName); + const encodedRunName = encodeURIComponent(params.runName); + return this.fetchAndLogErrors(`${this.baseUrl}/api/public/datasets/${encodedDatasetName}/runs/${encodedRunName}`, this._getFetchOptions({ + method: "GET" + })); + } + async getDatasetRuns(datasetName, query) { + return this.fetchAndLogErrors(`${this.baseUrl}/api/public/datasets/${encodeURIComponent(datasetName)}/runs?${encodeQueryParams(query)}`, this._getFetchOptions({ + method: "GET" + })); + } + async createDatasetRunItem(body) { + return this.fetchAndLogErrors(`${this.baseUrl}/api/public/dataset-run-items`, this._getFetchOptions({ + method: "POST", + body: JSON.stringify(body) + })); + } + /** + * Creates a dataset. Upserts the dataset if it already exists. + * + * @param dataset Can be either a string (name) or an object with name, description and metadata + * @returns A promise that resolves to the response of the create operation. + */ + async createDataset(dataset) { + const body = typeof dataset === "string" ? { + name: dataset + } : dataset; + return this.fetchAndLogErrors(`${this.baseUrl}/api/public/datasets`, this._getFetchOptions({ + method: "POST", + body: JSON.stringify(body) + })); + } + /** + * Creates a dataset item. Upserts the item if it already exists. + * @param body The body of the dataset item to be created. + * @returns A promise that resolves to the response of the create operation. + */ + async createDatasetItem(body) { + return this.fetchAndLogErrors(`${this.baseUrl}/api/public/dataset-items`, this._getFetchOptions({ + method: "POST", + body: JSON.stringify(body) + })); + } + async getDatasetItem(id) { + return this.fetchAndLogErrors(`${this.baseUrl}/api/public/dataset-items/${id}`, this._getFetchOptions({ + method: "GET" + })); + } + _parsePayload(response) { + try { + return JSON.parse(response); + } catch { + return response; + } + } + async createPromptStateless(body) { + return this.fetchAndLogErrors(`${this.baseUrl}/api/public/v2/prompts`, this._getFetchOptions({ + method: "POST", + body: JSON.stringify(body) + })); + } + async updatePromptStateless(body) { + return this.fetchAndLogErrors(`${this.baseUrl}/api/public/v2/prompts/${encodeURIComponent(body.name)}/versions/${encodeURIComponent(body.version)}`, this._getFetchOptions({ + method: "PATCH", + body: JSON.stringify(body) + })); + } + async getPromptStateless(name, version2, label, maxRetries, requestTimeout) { + const encodedName = encodeURIComponent(name); + const params = new URLSearchParams(); + if (version2 && label) { + throw new Error("Provide either version or label, not both."); + } + if (version2) { + params.append("version", version2.toString()); + } + if (label) { + params.append("label", label); + } + const url = `${this.baseUrl}/api/public/v2/prompts/${encodedName}${params.size ? "?" + params : ""}`; + const boundedMaxRetries = this._getBoundedMaxRetries({ + maxRetries, + defaultMaxRetries: 2, + maxRetriesUpperBound: 4 + }); + const retryOptions = { + ...this._retryOptions, + retryCount: boundedMaxRetries, + retryDelay: 500 + }; + const retryLogger = (string) => this._events.emit("retry", string + ", " + url + ", " + JSON.stringify(retryOptions)); + return retriable(async () => { + const res = await this.fetch(url, this._getFetchOptions({ + method: "GET", + fetchTimeout: requestTimeout ?? this.requestTimeout + })).catch((e) => { + if (e.name === "AbortError") { + throw new LangfuseFetchNetworkError("Fetch request timed out"); + } + throw new LangfuseFetchNetworkError(e); + }); + if (res.status >= 500) { + throw new LangfuseFetchHttpError(res, await res.text()); + } + const data = await res.json(); + return { + fetchResult: res.status === 200 ? "success" : "failure", + data + }; + }, retryOptions, retryLogger); + } + _getBoundedMaxRetries(params) { + const defaultMaxRetries = Math.max(params.defaultMaxRetries ?? 2, 0); + const maxRetriesUpperBound = Math.max(params.maxRetriesUpperBound ?? 4, 0); + if (params.maxRetries === void 0) { + return defaultMaxRetries; + } + return Math.min(Math.max(params.maxRetries, 0), maxRetriesUpperBound); + } + /*** + *** QUEUEING AND FLUSHING + ***/ + enqueue(type, body) { + if (!this.enabled) { + return; + } + const traceId = this.parseTraceId(type, body); + if (!traceId) { + this._events.emit("warning", "Failed to parse traceID for sampling. Please open a Github issue in https://github.com/langfuse/langfuse/issues/new/choose"); + } else if (!isInSample(traceId, this.sampleRate)) { + this._events.emit("debug", `Event with trace ID ${traceId} is out of sample. Skipping.`); + return; + } + const promise = this.processEnqueueEvent(type, body); + const promiseId = generateUUID(); + this.pendingEventProcessingPromises[promiseId] = promise; + promise.catch((e) => { + this._events.emit("error", e); + }).finally(() => { + delete this.pendingEventProcessingPromises[promiseId]; + }); + } + async processEnqueueEvent(type, body) { + this.maskEventBodyInPlace(body); + await this.processMediaInEvent(type, body); + const finalEventBody = this.truncateEventBody(body, MAX_EVENT_SIZE_BYTES); + try { + JSON.stringify(finalEventBody); + } catch (e) { + this._events.emit("error", `Event Body for ${type} is not JSON-serializable: ${e}`); + return; + } + const queue = this.getPersistedProperty(LangfusePersistedProperty.Queue) || []; + queue.push({ + id: generateUUID(), + type, + timestamp: currentISOTime(), + body: finalEventBody, + // TODO: fix typecast. EventBody is not correctly narrowed to the correct type dictated by the 'type' property. This should be part of a larger type cleanup. + metadata: void 0 + }); + this.setPersistedProperty(LangfusePersistedProperty.Queue, queue); + this._events.emit(type, finalEventBody); + if (queue.length >= this.flushAt) { + this.flush(); + } + if (this.flushInterval && !this._flushTimer) { + this._flushTimer = safeSetTimeout(() => this.flush(), this.flushInterval); + } + } + maskEventBodyInPlace(body) { + if (!this.mask) { + return; + } + const maskableKeys = ["input", "output"]; + for (const key of maskableKeys) { + if (key in body) { + try { + body[key] = this.mask({ + data: body[key] + }); + } catch (e) { + this._events.emit("error", `Error masking ${key}: ${e}`); + body[key] = ""; + } + } + } + } + /** + * Truncates the event body if its byte size exceeds the specified maximum byte size. + * Emits a warning event if truncation occurs. + * The fields that may be truncated are: "input", "output", and "metadata". + * The fields are truncated in the order of their size, from largest to smallest until the total byte size is within the limit. + */ + truncateEventBody(body, maxByteSize) { + const bodySize = this.getByteSize(body); + if (bodySize <= maxByteSize) { + return body; + } + this._events.emit("warning", `Event Body is too large (${bodySize} bytes) and will be truncated`); + const keysToCheck = ["input", "output", "metadata"]; + const keySizes = keysToCheck.map((key) => ({ + key, + size: key in body ? this.getByteSize(body[key]) : 0 + })).sort((a, b) => b.size - a.size); + let result = { + ...body + }; + let currentSize = bodySize; + for (const { + key, + size + } of keySizes) { + if (currentSize > maxByteSize && Object.prototype.hasOwnProperty.call(result, key)) { + result = { + ...result, + [key]: "" + }; + this._events.emit("warning", `Truncated ${key} due to total size exceeding limit`); + currentSize -= size; + } + } + return result; + } + getByteSize(obj) { + const serialized = JSON.stringify(obj); + if (typeof TextEncoder !== "undefined") { + return new TextEncoder().encode(serialized).length; + } else { + return encodeURIComponent(serialized).replace(/%[A-F\d]{2}/g, "U").length; + } + } + async processMediaInEvent(type, body) { + if (!body) { + return; + } + const traceId = this.parseTraceId(type, body); + if (!traceId) { + this._events.emit("warning", "traceId is required for media upload"); + return; + } + const observationId = (type.includes("generation") || type.includes("span")) && body.id ? body.id : void 0; + await Promise.all(["input", "output", "metadata"].map(async (field) => { + if (body[field]) { + body[field] = await this.findAndProcessMedia({ + data: body[field], + traceId, + observationId, + field + }).catch((e) => { + this._events.emit("error", `Error processing multimodal event: ${e}`); + }) ?? body[field]; + } + })); + } + parseTraceId(type, body) { + return "traceId" in body ? body.traceId : type.includes("trace") ? body.id : void 0; + } + async findAndProcessMedia({ + data, + traceId, + observationId, + field + }) { + const seenObjects = /* @__PURE__ */ new WeakMap(); + const maxLevels = 10; + const processRecursively = async (data2, level) => { + if (typeof data2 === "string" && data2.startsWith("data:")) { + const media = new LangfuseMedia({ + base64DataUri: data2 + }); + await this.processMediaItem({ + media, + traceId, + observationId, + field + }); + return media; + } + if (typeof data2 !== "object" || data2 === null) { + return data2; + } + if (seenObjects.has(data2) || level > maxLevels) { + return data2; + } + seenObjects.set(data2, true); + if (data2 instanceof LangfuseMedia || Object.prototype.toString.call(data2) === "[object LangfuseMedia]") { + await this.processMediaItem({ + media: data2, + traceId, + observationId, + field + }); + return data2; + } + if (Array.isArray(data2)) { + return await Promise.all(data2.map((item) => processRecursively(item, level + 1))); + } + if (typeof data2 === "object" && data2 !== null) { + if ("input_audio" in data2 && typeof data2["input_audio"] === "object" && "data" in data2.input_audio) { + const media = new LangfuseMedia({ + base64DataUri: `data:audio/${data2.input_audio["format"] || "wav"};base64,${data2.input_audio.data}` + }); + await this.processMediaItem({ + media, + traceId, + observationId, + field + }); + return { + ...data2, + input_audio: { + ...data2.input_audio, + data: media + } + }; + } + if ("audio" in data2 && typeof data2["audio"] === "object" && "data" in data2.audio) { + const media = new LangfuseMedia({ + base64DataUri: `data:audio/${data2.audio["format"] || "wav"};base64,${data2.audio.data}` + }); + await this.processMediaItem({ + media, + traceId, + observationId, + field + }); + return { + ...data2, + audio: { + ...data2.audio, + data: media + } + }; + } + return Object.fromEntries(await Promise.all(Object.entries(data2).map(async ([key, value]) => [key, await processRecursively(value, level + 1)]))); + } + return data2; + }; + return await processRecursively(data, 1); + } + async processMediaItem({ + media, + traceId, + observationId, + field + }) { + try { + if (!media.contentLength || !media._contentType || !media.contentSha256Hash || !media._contentBytes) { + return; + } + const getUploadUrlBody = { + contentLength: media.contentLength, + traceId, + observationId, + field, + contentType: media._contentType, + sha256Hash: media.contentSha256Hash + }; + const fetchResponse = await this.fetch(`${this.baseUrl}/api/public/media`, this._getFetchOptions({ + method: "POST", + body: JSON.stringify(getUploadUrlBody) + })); + const uploadUrlResponse = await fetchResponse.json(); + const { + uploadUrl, + mediaId + } = uploadUrlResponse; + media._mediaId = mediaId; + if (uploadUrl) { + this._events.emit("debug", `Uploading media ${mediaId}`); + const startTime = Date.now(); + const uploadResponse = await this.uploadMediaWithBackoff({ + uploadUrl, + contentBytes: media._contentBytes, + contentType: media._contentType, + contentSha256Hash: media.contentSha256Hash, + maxRetries: 3, + baseDelay: 1e3 + }); + if (!uploadResponse) { + throw Error("Media upload process failed"); + } + const patchMediaBody = { + uploadedAt: (/* @__PURE__ */ new Date()).toISOString(), + uploadHttpStatus: uploadResponse.status, + uploadHttpError: await uploadResponse.text(), + uploadTimeMs: Date.now() - startTime + }; + await this.fetch(`${this.baseUrl}/api/public/media/${mediaId}`, this._getFetchOptions({ + method: "PATCH", + body: JSON.stringify(patchMediaBody) + })); + this._events.emit("debug", `Media upload status reported for ${mediaId}`); + } else { + this._events.emit("debug", `Media ${mediaId} already uploaded`); + } + } catch (err) { + this._events.emit("error", `Error processing media item: ${err}`); + } + } + async uploadMediaWithBackoff(params) { + const { + uploadUrl, + contentType, + contentSha256Hash, + contentBytes, + maxRetries, + baseDelay + } = params; + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + const uploadResponse = await this.fetch(uploadUrl, { + method: "PUT", + body: contentBytes, + headers: { + "Content-Type": contentType, + "x-amz-checksum-sha256": contentSha256Hash, + "x-ms-blob-type": "BlockBlob" + } + }); + if (attempt < maxRetries && uploadResponse.status !== 200 && uploadResponse.status !== 201) { + throw new Error(`Upload failed with status ${uploadResponse.status}`); + } + return uploadResponse; + } catch (e) { + if (attempt === maxRetries) { + throw e; + } + const delay = baseDelay * Math.pow(2, attempt); + const jitter = Math.random() * 1e3; + await new Promise((resolve) => setTimeout(resolve, delay + jitter)); + } + } + } + /** + * Asynchronously flushes all events that are not yet sent to the server. + * This function always resolves, even if there were errors when flushing. + * Errors are emitted as "error" events and the promise resolves. + * + * @returns {Promise} A promise that resolves when the flushing is completed. + */ + async flushAsync() { + await Promise.all(Object.values(this.pendingEventProcessingPromises)).catch((e) => { + logIngestionError(e); + }); + return new Promise((resolve, _reject) => { + try { + this.flush((err, data) => { + if (err) { + logIngestionError(err); + resolve(); + } else { + resolve(data); + } + }); + } catch (e) { + console.error("[Langfuse SDK] Error while flushing Langfuse", e); + } + }); + } + // Flushes all events that are not yet sent to the server + flush(callback) { + if (this._flushTimer) { + clearTimeout(this._flushTimer); + this._flushTimer = null; + } + const queue = this.getPersistedProperty(LangfusePersistedProperty.Queue) || []; + if (!queue.length) { + return callback?.(); + } + const items = queue.splice(0, this.flushAt); + const { + processedItems, + remainingItems + } = this.processQueueItems(items, MAX_EVENT_SIZE_BYTES, MAX_BATCH_SIZE_BYTES); + this.setPersistedProperty(LangfusePersistedProperty.Queue, [...remainingItems, ...queue]); + const promiseUUID = generateUUID(); + const done = (err) => { + if (err) { + this._events.emit("warning", err); + } + callback?.(err, items); + this._events.emit("flush", items); + }; + if (this.isLocalEventExportEnabled && this.projectId) { + if (!this.localEventExportMap.has(this.projectId)) { + this.localEventExportMap.set(this.projectId, [...items]); + } else { + this.localEventExportMap.get(this.projectId)?.push(...items); + } + done(); + return; + } + const payload = JSON.stringify({ + batch: processedItems, + metadata: { + batch_size: processedItems.length, + sdk_integration: this.sdkIntegration, + sdk_version: this.getLibraryVersion(), + sdk_variant: this.getLibraryId(), + public_key: this.publicKey, + sdk_name: "langfuse-js" + } + }); + const url = `${this.baseUrl}/api/public/ingestion`; + const fetchOptions = this._getFetchOptions({ + method: "POST", + body: payload + }); + const requestPromise = this.fetchWithRetry(url, fetchOptions).then(() => done()).catch((err) => { + done(err); + }); + this.pendingIngestionPromises[promiseUUID] = requestPromise; + requestPromise.finally(() => { + delete this.pendingIngestionPromises[promiseUUID]; + }); + } + processQueueItems(queue, MAX_MSG_SIZE, BATCH_SIZE_LIMIT) { + let totalSize = 0; + const processedItems = []; + const remainingItems = []; + for (let i = 0; i < queue.length; i++) { + try { + const itemSize = new Blob([JSON.stringify(queue[i])]).size; + if (itemSize > MAX_MSG_SIZE) { + console.warn(`Item exceeds size limit (size: ${itemSize}), dropping item.`); + continue; + } + if (totalSize + itemSize >= BATCH_SIZE_LIMIT) { + console.debug(`hit batch size limit (size: ${totalSize + itemSize})`); + remainingItems.push(...queue.slice(i)); + console.log(`Remaining items: ${remainingItems.length}`); + console.log(`processes items: ${processedItems.length}`); + break; + } + totalSize += itemSize; + processedItems.push(queue[i]); + } catch (error) { + this._events.emit("error", error); + remainingItems.push(...queue.slice(i)); + break; + } + } + return { + processedItems, + remainingItems + }; + } + _getFetchOptions(p) { + const fetchOptions = { + method: p.method, + headers: { + "Content-Type": "application/json", + "X-Langfuse-Sdk-Name": "langfuse-js", + "X-Langfuse-Sdk-Version": this.getLibraryVersion(), + "X-Langfuse-Sdk-Variant": this.getLibraryId(), + "X-Langfuse-Sdk-Integration": this.sdkIntegration, + "X-Langfuse-Public-Key": this.publicKey, + ...this.additionalHeaders, + ...this.constructAuthorizationHeader(this.publicKey, this.secretKey) + }, + body: p.body, + ...p.fetchTimeout !== void 0 ? { + signal: AbortSignal.timeout(p.fetchTimeout) + } : {} + }; + return fetchOptions; + } + constructAuthorizationHeader(publicKey, secretKey) { + if (secretKey === void 0) { + return { + Authorization: "Bearer " + publicKey + }; + } else { + const encodedCredentials = typeof btoa === "function" ? ( + // btoa() is available, the code is running in a browser or edge environment + btoa(publicKey + ":" + secretKey) + ) : ( + // btoa() is not available, the code is running in Node.js + Buffer.from(publicKey + ":" + secretKey).toString("base64") + ); + return { + Authorization: "Basic " + encodedCredentials + }; + } + } + async fetchWithRetry(url, options, retryOptions) { + AbortSignal.timeout ??= function timeout(ms) { + const ctrl = new AbortController(); + setTimeout(() => ctrl.abort(), ms); + return ctrl.signal; + }; + return await retriable(async () => { + let res = null; + try { + res = await this.fetch(url, { + signal: AbortSignal.timeout(this.requestTimeout), + ...options + }); + } catch (e) { + throw new LangfuseFetchNetworkError(e); + } + if (res.status < 200 || res.status >= 400) { + const body = await res.json(); + throw new LangfuseFetchHttpError(res, JSON.stringify(body)); + } + const returnBody = await res.json(); + if (res.status === 207 && returnBody.errors.length > 0) { + throw new LangfuseFetchHttpError(res, JSON.stringify(returnBody.errors)); + } + return res; + }, { + ...this._retryOptions, + ...retryOptions + }, (string) => this._events.emit("retry", string + ", " + url + ", " + JSON.stringify(options))); + } + async fetchAndLogErrors(url, options) { + const res = await this.fetch(url, options); + const data = res.status === 429 ? await res.text() : await res.json(); + if (res.status < 200 || res.status >= 400) { + logIngestionError(new LangfuseFetchHttpError(res, JSON.stringify(data))); + } + return data; + } + async shutdownAsync() { + clearTimeout(this._flushTimer); + try { + await this.flushAsync(); + await Promise.all(Object.values(this.pendingIngestionPromises).map((x) => x.catch(() => { + }))); + await this.flushAsync(); + } catch (e) { + console.error("[Langfuse SDK] Error while shutting down Langfuse", e); + } + } + async _exportLocalEvents(projectId) { + if (this.isLocalEventExportEnabled) { + clearTimeout(this._flushTimer); + await this.flushAsync(); + const events = this.localEventExportMap.get(projectId) ?? []; + this.localEventExportMap.delete(projectId); + return events; + } else { + this._events.emit("error", "Local event exports are disabled, but _exportLocalEvents() was called."); + return []; + } + } + shutdown() { + console.warn("shutdown() is deprecated. It does not wait for all events to be processed. Please use shutdownAsync() instead."); + void this.shutdownAsync(); + } + async awaitAllQueuedAndPendingRequests() { + clearTimeout(this._flushTimer); + await this.flushAsync(); + await Promise.all(Object.values(this.pendingIngestionPromises)); + } +}; +var LangfuseCore = class extends LangfuseCoreStateless { + constructor(params) { + const { + publicKey, + secretKey, + enabled, + _isLocalEventExportEnabled + } = params; + let isObservabilityEnabled = enabled === false ? false : true; + if (_isLocalEventExportEnabled) { + isObservabilityEnabled = true; + } else if (!secretKey) { + isObservabilityEnabled = false; + if (enabled !== false) { + console.warn("Langfuse secret key was not passed to constructor or not set as 'LANGFUSE_SECRET_KEY' environment variable. No observability data will be sent to Langfuse."); + } + } else if (!publicKey) { + isObservabilityEnabled = false; + if (enabled !== false) { + console.warn("Langfuse public key was not passed to constructor or not set as 'LANGFUSE_PUBLIC_KEY' environment variable. No observability data will be sent to Langfuse."); + } + } + super({ + ...params, + enabled: isObservabilityEnabled + }); + this._promptCache = new LangfusePromptCache(); + } + trace(body) { + const id = this.traceStateless(body ?? {}); + const t = new LangfuseTraceClient(this, id); + if (getEnv("DEFER") && body) { + try { + const deferRuntime = getEnv("__deferRuntime"); + if (deferRuntime) { + deferRuntime.langfuseTraces([{ + id, + name: body.name || "", + url: t.getTraceUrl() + }]); + } + } catch { + } + } + return t; + } + span(body) { + const traceId = body.traceId || this.traceStateless({ + name: body.name + }); + const id = this.spanStateless({ + ...body, + traceId + }); + return new LangfuseSpanClient(this, id, traceId); + } + generation(body) { + const traceId = body.traceId || this.traceStateless({ + name: body.name + }); + const id = this.generationStateless({ + ...body, + traceId + }); + return new LangfuseGenerationClient(this, id, traceId); + } + event(body) { + const traceId = body.traceId || this.traceStateless({ + name: body.name + }); + const id = this.eventStateless({ + ...body, + traceId + }); + return new LangfuseEventClient(this, id, traceId); + } + score(body) { + this.scoreStateless(body); + return this; + } + async getDataset(name, options) { + const dataset = await this._getDataset(name); + const items = []; + let page = 1; + while (true) { + const itemsResponse = await this._getDatasetItems({ + datasetName: name, + limit: options?.fetchItemsPageSize ?? 50, + page + }); + items.push(...itemsResponse.data); + if (itemsResponse.meta.totalPages <= page) { + break; + } + page++; + } + const returnDataset = { + ...dataset, + description: dataset.description ?? void 0, + metadata: dataset.metadata ?? void 0, + items: items.map((item) => ({ + ...item, + link: async (obj, runName, runArgs) => { + await this.awaitAllQueuedAndPendingRequests(); + const data = await this.createDatasetRunItem({ + runName, + datasetItemId: item.id, + observationId: obj.observationId, + traceId: obj.traceId, + runDescription: runArgs?.description, + metadata: runArgs?.metadata + }); + return data; + } + })) + }; + return returnDataset; + } + async createPrompt(body) { + const labels = body.labels ?? []; + const promptResponse = body.type === "chat" ? await this.createPromptStateless({ + ...body, + prompt: body.prompt.map((item) => { + if ("type" in item && item.type === ChatMessageType.Placeholder) { + return { + type: ChatMessageType.Placeholder, + name: item.name + }; + } else { + return { + type: ChatMessageType.ChatMessage, + ...item + }; + } + }), + labels: body.isActive ? [.../* @__PURE__ */ new Set([...labels, "production"])] : labels + // backward compatibility for isActive + }) : await this.createPromptStateless({ + ...body, + type: body.type ?? "text", + labels: body.isActive ? [.../* @__PURE__ */ new Set([...labels, "production"])] : labels + // backward compatibility for isActive + }); + if (promptResponse.type === "chat") { + return new ChatPromptClient(promptResponse); + } + return new TextPromptClient(promptResponse); + } + async updatePrompt(body) { + const newPrompt = await this.updatePromptStateless(body); + this._promptCache.invalidate(body.name); + return newPrompt; + } + async getPrompt(name, version2, options) { + const cacheKey = this._getPromptCacheKey({ + name, + version: version2, + label: options?.label + }); + const cachedPrompt = this._promptCache.getIncludingExpired(cacheKey); + if (!cachedPrompt || options?.cacheTtlSeconds === 0) { + try { + return await this._fetchPromptAndUpdateCache({ + name, + version: version2, + label: options?.label, + cacheTtlSeconds: options?.cacheTtlSeconds, + maxRetries: options?.maxRetries, + fetchTimeout: options?.fetchTimeoutMs + }); + } catch (err) { + if (options?.fallback) { + const sharedFallbackParams = { + name, + version: version2 ?? 0, + labels: options.label ? [options.label] : [], + cacheTtlSeconds: options?.cacheTtlSeconds, + config: {}, + tags: [] + }; + if (options.type === "chat") { + return new ChatPromptClient({ + ...sharedFallbackParams, + type: "chat", + prompt: options.fallback.map((msg) => ({ + type: ChatMessageType.ChatMessage, + ...msg + })) + }, true); + } else { + return new TextPromptClient({ + ...sharedFallbackParams, + type: "text", + prompt: options.fallback + }, true); + } + } + throw err; + } + } + if (cachedPrompt.isExpired) { + if (!this._promptCache.isRefreshing(cacheKey)) { + const refreshPromptPromise = this._fetchPromptAndUpdateCache({ + name, + version: version2, + label: options?.label, + cacheTtlSeconds: options?.cacheTtlSeconds, + maxRetries: options?.maxRetries, + fetchTimeout: options?.fetchTimeoutMs + }).catch(() => { + console.warn(`Failed to refresh prompt cache '${cacheKey}', stale cache will be used until next refresh succeeds.`); + }); + this._promptCache.addRefreshingPromise(cacheKey, refreshPromptPromise); + } + return cachedPrompt.value; + } + return cachedPrompt.value; + } + _getPromptCacheKey(params) { + const { + name, + version: version2, + label + } = params; + const parts = [name]; + if (version2 !== void 0) { + parts.push("version:" + version2.toString()); + } else if (label !== void 0) { + parts.push("label:" + label); + } else { + parts.push("label:production"); + } + return parts.join("-"); + } + async _fetchPromptAndUpdateCache(params) { + const cacheKey = this._getPromptCacheKey(params); + try { + const { + name, + version: version2, + cacheTtlSeconds, + label, + maxRetries, + fetchTimeout + } = params; + const { + data, + fetchResult + } = await this.getPromptStateless(name, version2, label, maxRetries, fetchTimeout); + if (fetchResult === "failure") { + throw Error(data.message ?? "Internal error while fetching prompt"); + } + let prompt2; + if (data.type === "chat") { + prompt2 = new ChatPromptClient(data); + } else { + prompt2 = new TextPromptClient(data); + } + this._promptCache.set(cacheKey, prompt2, cacheTtlSeconds); + return prompt2; + } catch (error) { + console.error(`[Langfuse SDK] Error while fetching prompt '${cacheKey}':`, error); + throw error; + } + } + async fetchMedia(id) { + return await this._fetchMedia(id); + } + /** + * Replaces the media reference strings in an object with base64 data URIs for the media content. + * + * This method recursively traverses an object (up to a maximum depth of 10) looking for media reference strings + * in the format "@@@langfuseMedia:...@@@". When found, it fetches the actual media content using the provided + * Langfuse client and replaces the reference string with a base64 data URI. + * + * If fetching media content fails for a reference string, a warning is logged and the reference string is left unchanged. + * + * @param params - Configuration object + * @param params.obj - The object to process. Can be a primitive value, array, or nested object + * @param params.langfuseClient - Langfuse client instance used to fetch media content + * @param params.resolveWith - The representation of the media content to replace the media reference string with. Currently only "base64DataUri" is supported. + * @param params.maxDepth - Optional. Default is 10. The maximum depth to traverse the object. + * + * @returns A deep copy of the input object with all media references replaced with base64 data URIs where possible + * + * @example + * ```typescript + * const obj = { + * image: "@@@langfuseMedia:type=image/jpeg|id=123|source=bytes@@@", + * nested: { + * pdf: "@@@langfuseMedia:type=application/pdf|id=456|source=bytes@@@" + * } + * }; + * + * const result = await LangfuseMedia.resolveMediaReferences({ + * obj, + * langfuseClient + * }); + * + * // Result: + * // { + * // image: "data:image/jpeg;base64,/9j/4AAQSkZJRg...", + * // nested: { + * // pdf: "data:application/pdf;base64,JVBERi0xLjcK..." + * // } + * // } + * ``` + */ + async resolveMediaReferences(params) { + const { + obj, + ...rest + } = params; + return LangfuseMedia.resolveMediaReferences({ + ...rest, + langfuseClient: this, + obj + }); + } + _updateSpan(body) { + this.updateSpanStateless(body); + return this; + } + _updateGeneration(body) { + this.updateGenerationStateless(body); + return this; + } +}; +var LangfuseObjectClient = class { + constructor({ + client, + id, + traceId, + observationId + }) { + this.client = client; + this.id = id; + this.traceId = traceId; + this.observationId = observationId; + } + event(body) { + return this.client.event({ + ...body, + traceId: this.traceId, + parentObservationId: this.observationId + }); + } + span(body) { + return this.client.span({ + ...body, + traceId: this.traceId, + parentObservationId: this.observationId + }); + } + generation(body) { + return this.client.generation({ + ...body, + traceId: this.traceId, + parentObservationId: this.observationId + }); + } + score(body) { + this.client.score({ + ...body, + traceId: this.traceId, + observationId: this.observationId + }); + return this; + } + getTraceUrl() { + return `${this.client.baseUrl}/trace/${this.traceId}`; + } +}; +var LangfuseTraceClient = class extends LangfuseObjectClient { + constructor(client, traceId) { + super({ + client, + id: traceId, + traceId, + observationId: null + }); + } + update(body) { + this.client.trace({ + ...body, + id: this.id + }); + return this; + } +}; +var LangfuseObservationClient = class extends LangfuseObjectClient { + constructor(client, id, traceId) { + super({ + client, + id, + traceId, + observationId: id + }); + } +}; +var LangfuseSpanClient = class extends LangfuseObservationClient { + constructor(client, id, traceId) { + super(client, id, traceId); + } + update(body) { + this.client._updateSpan({ + ...body, + id: this.id, + traceId: this.traceId + }); + return this; + } + end(body) { + this.client._updateSpan({ + ...body, + id: this.id, + traceId: this.traceId, + endTime: /* @__PURE__ */ new Date() + }); + return this; + } +}; +var LangfuseGenerationClient = class extends LangfuseObservationClient { + constructor(client, id, traceId) { + super(client, id, traceId); + } + update(body) { + this.client._updateGeneration({ + ...body, + id: this.id, + traceId: this.traceId + }); + return this; + } + end(body) { + this.client._updateGeneration({ + ...body, + id: this.id, + traceId: this.traceId, + endTime: /* @__PURE__ */ new Date() + }); + return this; + } +}; +var LangfuseEventClient = class extends LangfuseObservationClient { + constructor(client, id, traceId) { + super(client, id, traceId); + } +}; + +// node_modules/langfuse/lib/index.mjs +var cookieStore = { + getItem(key) { + try { + const nameEQ = key + "="; + const ca = document.cookie.split(";"); + for (let i = 0; i < ca.length; i++) { + let c = ca[i]; + while (c.charAt(0) == " ") { + c = c.substring(1, c.length); + } + if (c.indexOf(nameEQ) === 0) { + return decodeURIComponent(c.substring(nameEQ.length, c.length)); + } + } + } catch (err) { + } + return null; + }, + setItem(key, value) { + try { + const cdomain = "", expires = "", secure = ""; + const new_cookie_val = key + "=" + encodeURIComponent(value) + expires + "; path=/" + cdomain + secure; + document.cookie = new_cookie_val; + } catch (err) { + return; + } + }, + removeItem(name) { + try { + cookieStore.setItem(name, ""); + } catch (err) { + return; + } + }, + clear() { + document.cookie = ""; + }, + getAllKeys() { + const ca = document.cookie.split(";"); + const keys = []; + for (let i = 0; i < ca.length; i++) { + let c = ca[i]; + while (c.charAt(0) == " ") { + c = c.substring(1, c.length); + } + keys.push(c.split("=")[0]); + } + return keys; + } +}; +var createStorageLike = (store) => { + return { + getItem(key) { + return store.getItem(key); + }, + setItem(key, value) { + store.setItem(key, value); + }, + removeItem(key) { + store.removeItem(key); + }, + clear() { + store.clear(); + }, + getAllKeys() { + const keys = []; + for (const key in localStorage) { + keys.push(key); + } + return keys; + } + }; +}; +var checkStoreIsSupported = (storage, key = "__mplssupport__") => { + if (!window) { + return false; + } + try { + const val = "xyz"; + storage.setItem(key, val); + if (storage.getItem(key) !== val) { + return false; + } + storage.removeItem(key); + return true; + } catch (err) { + return false; + } +}; +var localStore = void 0; +var sessionStore = void 0; +var createMemoryStorage = () => { + const _cache = {}; + const store = { + getItem(key) { + return _cache[key]; + }, + setItem(key, value) { + _cache[key] = value !== null ? value : void 0; + }, + removeItem(key) { + delete _cache[key]; + }, + clear() { + for (const key in _cache) { + delete _cache[key]; + } + }, + getAllKeys() { + const keys = []; + for (const key in _cache) { + keys.push(key); + } + return keys; + } + }; + return store; +}; +var getStorage = (type, window2) => { + if (typeof window2 !== void 0 && window2) { + if (!localStorage) { + const _localStore = createStorageLike(window2.localStorage); + localStore = checkStoreIsSupported(_localStore) ? _localStore : void 0; + } + if (!sessionStore) { + const _sessionStore = createStorageLike(window2.sessionStorage); + sessionStore = checkStoreIsSupported(_sessionStore) ? _sessionStore : void 0; + } + } + switch (type) { + case "cookie": + return cookieStore || localStore || sessionStore || createMemoryStorage(); + case "localStorage": + return localStore || sessionStore || createMemoryStorage(); + case "sessionStorage": + return sessionStore || createMemoryStorage(); + case "memory": + return createMemoryStorage(); + default: + return createMemoryStorage(); + } +}; +var ContentType; +(function(ContentType2) { + ContentType2["Json"] = "application/json"; + ContentType2["FormData"] = "multipart/form-data"; + ContentType2["UrlEncoded"] = "application/x-www-form-urlencoded"; + ContentType2["Text"] = "text/plain"; +})(ContentType || (ContentType = {})); +var HttpClient = class { + constructor(apiConfig = {}) { + this.baseUrl = ""; + this.securityData = null; + this.abortControllers = /* @__PURE__ */ new Map(); + this.customFetch = (...fetchParams) => fetch(...fetchParams); + this.baseApiParams = { + credentials: "same-origin", + headers: {}, + redirect: "follow", + referrerPolicy: "no-referrer" + }; + this.setSecurityData = (data) => { + this.securityData = data; + }; + this.contentFormatters = { + [ContentType.Json]: (input) => input !== null && (typeof input === "object" || typeof input === "string") ? JSON.stringify(input) : input, + [ContentType.Text]: (input) => input !== null && typeof input !== "string" ? JSON.stringify(input) : input, + [ContentType.FormData]: (input) => Object.keys(input || {}).reduce((formData, key) => { + const property = input[key]; + formData.append(key, property instanceof Blob ? property : typeof property === "object" && property !== null ? JSON.stringify(property) : `${property}`); + return formData; + }, new FormData()), + [ContentType.UrlEncoded]: (input) => this.toQueryString(input) + }; + this.createAbortSignal = (cancelToken) => { + if (this.abortControllers.has(cancelToken)) { + const abortController2 = this.abortControllers.get(cancelToken); + if (abortController2) { + return abortController2.signal; + } + return void 0; + } + const abortController = new AbortController(); + this.abortControllers.set(cancelToken, abortController); + return abortController.signal; + }; + this.abortRequest = (cancelToken) => { + const abortController = this.abortControllers.get(cancelToken); + if (abortController) { + abortController.abort(); + this.abortControllers.delete(cancelToken); + } + }; + this.request = async ({ + body, + secure, + path, + type, + query, + format, + baseUrl, + cancelToken, + ...params + }) => { + const secureParams = (typeof secure === "boolean" ? secure : this.baseApiParams.secure) && this.securityWorker && await this.securityWorker(this.securityData) || {}; + const requestParams = this.mergeRequestParams(params, secureParams); + const queryString = query && this.toQueryString(query); + const payloadFormatter = this.contentFormatters[type || ContentType.Json]; + const responseFormat = format || requestParams.format; + return this.customFetch(`${baseUrl || this.baseUrl || ""}${path}${queryString ? `?${queryString}` : ""}`, { + ...requestParams, + headers: { + ...requestParams.headers || {}, + ...type && type !== ContentType.FormData ? { + "Content-Type": type + } : {} + }, + signal: (cancelToken ? this.createAbortSignal(cancelToken) : requestParams.signal) || null, + body: typeof body === "undefined" || body === null ? null : payloadFormatter(body) + }).then(async (response) => { + const r = response.clone(); + r.data = null; + r.error = null; + const data = !responseFormat ? r : await response[responseFormat]().then((data2) => { + if (r.ok) { + r.data = data2; + } else { + r.error = data2; + } + return r; + }).catch((e) => { + r.error = e; + return r; + }); + if (cancelToken) { + this.abortControllers.delete(cancelToken); + } + if (!response.ok) throw data; + return data.data; + }); + }; + Object.assign(this, apiConfig); + } + encodeQueryParam(key, value) { + const encodedKey = encodeURIComponent(key); + return `${encodedKey}=${encodeURIComponent(typeof value === "number" ? value : `${value}`)}`; + } + addQueryParam(query, key) { + return this.encodeQueryParam(key, query[key]); + } + addArrayQueryParam(query, key) { + const value = query[key]; + return value.map((v) => this.encodeQueryParam(key, v)).join("&"); + } + toQueryString(rawQuery) { + const query = rawQuery || {}; + const keys = Object.keys(query).filter((key) => "undefined" !== typeof query[key]); + return keys.map((key) => Array.isArray(query[key]) ? this.addArrayQueryParam(query, key) : this.addQueryParam(query, key)).join("&"); + } + addQueryParams(rawQuery) { + const queryString = this.toQueryString(rawQuery); + return queryString ? `?${queryString}` : ""; + } + mergeRequestParams(params1, params2) { + return { + ...this.baseApiParams, + ...params1, + ...params2 || {}, + headers: { + ...this.baseApiParams.headers || {}, + ...params1.headers || {}, + ...params2 && params2.headers || {} + } + }; + } +}; +var LangfusePublicApi = class extends HttpClient { + constructor() { + super(...arguments); + this.api = { + /** + * @description Add an item to an annotation queue + * + * @tags AnnotationQueues + * @name AnnotationQueuesCreateQueueItem + * @request POST:/api/public/annotation-queues/{queueId}/items + * @secure + */ + annotationQueuesCreateQueueItem: (queueId, data, params = {}) => this.request({ + path: `/api/public/annotation-queues/${queueId}/items`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params + }), + /** + * @description Remove an item from an annotation queue + * + * @tags AnnotationQueues + * @name AnnotationQueuesDeleteQueueItem + * @request DELETE:/api/public/annotation-queues/{queueId}/items/{itemId} + * @secure + */ + annotationQueuesDeleteQueueItem: (queueId, itemId, params = {}) => this.request({ + path: `/api/public/annotation-queues/${queueId}/items/${itemId}`, + method: "DELETE", + secure: true, + format: "json", + ...params + }), + /** + * @description Get an annotation queue by ID + * + * @tags AnnotationQueues + * @name AnnotationQueuesGetQueue + * @request GET:/api/public/annotation-queues/{queueId} + * @secure + */ + annotationQueuesGetQueue: (queueId, params = {}) => this.request({ + path: `/api/public/annotation-queues/${queueId}`, + method: "GET", + secure: true, + format: "json", + ...params + }), + /** + * @description Get a specific item from an annotation queue + * + * @tags AnnotationQueues + * @name AnnotationQueuesGetQueueItem + * @request GET:/api/public/annotation-queues/{queueId}/items/{itemId} + * @secure + */ + annotationQueuesGetQueueItem: (queueId, itemId, params = {}) => this.request({ + path: `/api/public/annotation-queues/${queueId}/items/${itemId}`, + method: "GET", + secure: true, + format: "json", + ...params + }), + /** + * @description Get items for a specific annotation queue + * + * @tags AnnotationQueues + * @name AnnotationQueuesListQueueItems + * @request GET:/api/public/annotation-queues/{queueId}/items + * @secure + */ + annotationQueuesListQueueItems: ({ + queueId, + ...query + }, params = {}) => this.request({ + path: `/api/public/annotation-queues/${queueId}/items`, + method: "GET", + query, + secure: true, + format: "json", + ...params + }), + /** + * @description Get all annotation queues + * + * @tags AnnotationQueues + * @name AnnotationQueuesListQueues + * @request GET:/api/public/annotation-queues + * @secure + */ + annotationQueuesListQueues: (query, params = {}) => this.request({ + path: `/api/public/annotation-queues`, + method: "GET", + query, + secure: true, + format: "json", + ...params + }), + /** + * @description Update an annotation queue item + * + * @tags AnnotationQueues + * @name AnnotationQueuesUpdateQueueItem + * @request PATCH:/api/public/annotation-queues/{queueId}/items/{itemId} + * @secure + */ + annotationQueuesUpdateQueueItem: (queueId, itemId, data, params = {}) => this.request({ + path: `/api/public/annotation-queues/${queueId}/items/${itemId}`, + method: "PATCH", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params + }), + /** + * @description Create a comment. Comments may be attached to different object types (trace, observation, session, prompt). + * + * @tags Comments + * @name CommentsCreate + * @request POST:/api/public/comments + * @secure + */ + commentsCreate: (data, params = {}) => this.request({ + path: `/api/public/comments`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params + }), + /** + * @description Get all comments + * + * @tags Comments + * @name CommentsGet + * @request GET:/api/public/comments + * @secure + */ + commentsGet: (query, params = {}) => this.request({ + path: `/api/public/comments`, + method: "GET", + query, + secure: true, + format: "json", + ...params + }), + /** + * @description Get a comment by id + * + * @tags Comments + * @name CommentsGetById + * @request GET:/api/public/comments/{commentId} + * @secure + */ + commentsGetById: (commentId, params = {}) => this.request({ + path: `/api/public/comments/${commentId}`, + method: "GET", + secure: true, + format: "json", + ...params + }), + /** + * @description Create a dataset item + * + * @tags DatasetItems + * @name DatasetItemsCreate + * @request POST:/api/public/dataset-items + * @secure + */ + datasetItemsCreate: (data, params = {}) => this.request({ + path: `/api/public/dataset-items`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params + }), + /** + * @description Delete a dataset item and all its run items. This action is irreversible. + * + * @tags DatasetItems + * @name DatasetItemsDelete + * @request DELETE:/api/public/dataset-items/{id} + * @secure + */ + datasetItemsDelete: (id, params = {}) => this.request({ + path: `/api/public/dataset-items/${id}`, + method: "DELETE", + secure: true, + format: "json", + ...params + }), + /** + * @description Get a dataset item + * + * @tags DatasetItems + * @name DatasetItemsGet + * @request GET:/api/public/dataset-items/{id} + * @secure + */ + datasetItemsGet: (id, params = {}) => this.request({ + path: `/api/public/dataset-items/${id}`, + method: "GET", + secure: true, + format: "json", + ...params + }), + /** + * @description Get dataset items + * + * @tags DatasetItems + * @name DatasetItemsList + * @request GET:/api/public/dataset-items + * @secure + */ + datasetItemsList: (query, params = {}) => this.request({ + path: `/api/public/dataset-items`, + method: "GET", + query, + secure: true, + format: "json", + ...params + }), + /** + * @description Create a dataset run item + * + * @tags DatasetRunItems + * @name DatasetRunItemsCreate + * @request POST:/api/public/dataset-run-items + * @secure + */ + datasetRunItemsCreate: (data, params = {}) => this.request({ + path: `/api/public/dataset-run-items`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params + }), + /** + * @description List dataset run items + * + * @tags DatasetRunItems + * @name DatasetRunItemsList + * @request GET:/api/public/dataset-run-items + * @secure + */ + datasetRunItemsList: (query, params = {}) => this.request({ + path: `/api/public/dataset-run-items`, + method: "GET", + query, + secure: true, + ...params + }), + /** + * @description Create a dataset + * + * @tags Datasets + * @name DatasetsCreate + * @request POST:/api/public/v2/datasets + * @secure + */ + datasetsCreate: (data, params = {}) => this.request({ + path: `/api/public/v2/datasets`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params + }), + /** + * @description Delete a dataset run and all its run items. This action is irreversible. + * + * @tags Datasets + * @name DatasetsDeleteRun + * @request DELETE:/api/public/datasets/{datasetName}/runs/{runName} + * @secure + */ + datasetsDeleteRun: (datasetName, runName, params = {}) => this.request({ + path: `/api/public/datasets/${datasetName}/runs/${runName}`, + method: "DELETE", + secure: true, + format: "json", + ...params + }), + /** + * @description Get a dataset + * + * @tags Datasets + * @name DatasetsGet + * @request GET:/api/public/v2/datasets/{datasetName} + * @secure + */ + datasetsGet: (datasetName, params = {}) => this.request({ + path: `/api/public/v2/datasets/${datasetName}`, + method: "GET", + secure: true, + format: "json", + ...params + }), + /** + * @description Get a dataset run and its items + * + * @tags Datasets + * @name DatasetsGetRun + * @request GET:/api/public/datasets/{datasetName}/runs/{runName} + * @secure + */ + datasetsGetRun: (datasetName, runName, params = {}) => this.request({ + path: `/api/public/datasets/${datasetName}/runs/${runName}`, + method: "GET", + secure: true, + format: "json", + ...params + }), + /** + * @description Get dataset runs + * + * @tags Datasets + * @name DatasetsGetRuns + * @request GET:/api/public/datasets/{datasetName}/runs + * @secure + */ + datasetsGetRuns: ({ + datasetName, + ...query + }, params = {}) => this.request({ + path: `/api/public/datasets/${datasetName}/runs`, + method: "GET", + query, + secure: true, + format: "json", + ...params + }), + /** + * @description Get all datasets + * + * @tags Datasets + * @name DatasetsList + * @request GET:/api/public/v2/datasets + * @secure + */ + datasetsList: (query, params = {}) => this.request({ + path: `/api/public/v2/datasets`, + method: "GET", + query, + secure: true, + format: "json", + ...params + }), + /** + * @description Check health of API and database + * + * @tags Health + * @name HealthHealth + * @request GET:/api/public/health + */ + healthHealth: (params = {}) => this.request({ + path: `/api/public/health`, + method: "GET", + format: "json", + ...params + }), + /** + * @description Batched ingestion for Langfuse Tracing. If you want to use tracing via the API, such as to build your own Langfuse client implementation, this is the only API route you need to implement. Within each batch, there can be multiple events. Each event has a type, an id, a timestamp, metadata and a body. Internally, we refer to this as the "event envelope" as it tells us something about the event but not the trace. We use the event id within this envelope to deduplicate messages to avoid processing the same event twice, i.e. the event id should be unique per request. The event.body.id is the ID of the actual trace and will be used for updates and will be visible within the Langfuse App. I.e. if you want to update a trace, you'd use the same body id, but separate event IDs. Notes: - Introduction to data model: https://langfuse.com/docs/tracing-data-model - Batch sizes are limited to 3.5 MB in total. You need to adjust the number of events per batch accordingly. - The API does not return a 4xx status code for input errors. Instead, it responds with a 207 status code, which includes a list of the encountered errors. + * + * @tags Ingestion + * @name IngestionBatch + * @request POST:/api/public/ingestion + * @secure + */ + ingestionBatch: (data, params = {}) => this.request({ + path: `/api/public/ingestion`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params + }), + /** + * @description Get a media record + * + * @tags Media + * @name MediaGet + * @request GET:/api/public/media/{mediaId} + * @secure + */ + mediaGet: (mediaId, params = {}) => this.request({ + path: `/api/public/media/${mediaId}`, + method: "GET", + secure: true, + format: "json", + ...params + }), + /** + * @description Get a presigned upload URL for a media record + * + * @tags Media + * @name MediaGetUploadUrl + * @request POST:/api/public/media + * @secure + */ + mediaGetUploadUrl: (data, params = {}) => this.request({ + path: `/api/public/media`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params + }), + /** + * @description Patch a media record + * + * @tags Media + * @name MediaPatch + * @request PATCH:/api/public/media/{mediaId} + * @secure + */ + mediaPatch: (mediaId, data, params = {}) => this.request({ + path: `/api/public/media/${mediaId}`, + method: "PATCH", + body: data, + secure: true, + type: ContentType.Json, + ...params + }), + /** + * @description Get metrics from the Langfuse project using a query object + * + * @tags Metrics + * @name MetricsMetrics + * @request GET:/api/public/metrics + * @secure + */ + metricsMetrics: (query, params = {}) => this.request({ + path: `/api/public/metrics`, + method: "GET", + query, + secure: true, + format: "json", + ...params + }), + /** + * @description Create a model + * + * @tags Models + * @name ModelsCreate + * @request POST:/api/public/models + * @secure + */ + modelsCreate: (data, params = {}) => this.request({ + path: `/api/public/models`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params + }), + /** + * @description Delete a model. Cannot delete models managed by Langfuse. You can create your own definition with the same modelName to override the definition though. + * + * @tags Models + * @name ModelsDelete + * @request DELETE:/api/public/models/{id} + * @secure + */ + modelsDelete: (id, params = {}) => this.request({ + path: `/api/public/models/${id}`, + method: "DELETE", + secure: true, + ...params + }), + /** + * @description Get a model + * + * @tags Models + * @name ModelsGet + * @request GET:/api/public/models/{id} + * @secure + */ + modelsGet: (id, params = {}) => this.request({ + path: `/api/public/models/${id}`, + method: "GET", + secure: true, + format: "json", + ...params + }), + /** + * @description Get all models + * + * @tags Models + * @name ModelsList + * @request GET:/api/public/models + * @secure + */ + modelsList: (query, params = {}) => this.request({ + path: `/api/public/models`, + method: "GET", + query, + secure: true, + format: "json", + ...params + }), + /** + * @description Get a observation + * + * @tags Observations + * @name ObservationsGet + * @request GET:/api/public/observations/{observationId} + * @secure + */ + observationsGet: (observationId, params = {}) => this.request({ + path: `/api/public/observations/${observationId}`, + method: "GET", + secure: true, + format: "json", + ...params + }), + /** + * @description Get a list of observations + * + * @tags Observations + * @name ObservationsGetMany + * @request GET:/api/public/observations + * @secure + */ + observationsGetMany: (query, params = {}) => this.request({ + path: `/api/public/observations`, + method: "GET", + query, + secure: true, + format: "json", + ...params + }), + /** + * @description Get all memberships for the organization associated with the API key (requires organization-scoped API key) + * + * @tags Organizations + * @name OrganizationsGetOrganizationMemberships + * @request GET:/api/public/organizations/memberships + * @secure + */ + organizationsGetOrganizationMemberships: (params = {}) => this.request({ + path: `/api/public/organizations/memberships`, + method: "GET", + secure: true, + format: "json", + ...params + }), + /** + * @description Get all projects for the organization associated with the API key (requires organization-scoped API key) + * + * @tags Organizations + * @name OrganizationsGetOrganizationProjects + * @request GET:/api/public/organizations/projects + * @secure + */ + organizationsGetOrganizationProjects: (params = {}) => this.request({ + path: `/api/public/organizations/projects`, + method: "GET", + secure: true, + format: "json", + ...params + }), + /** + * @description Get all memberships for a specific project (requires organization-scoped API key) + * + * @tags Organizations + * @name OrganizationsGetProjectMemberships + * @request GET:/api/public/projects/{projectId}/memberships + * @secure + */ + organizationsGetProjectMemberships: (projectId, params = {}) => this.request({ + path: `/api/public/projects/${projectId}/memberships`, + method: "GET", + secure: true, + format: "json", + ...params + }), + /** + * @description Create or update a membership for the organization associated with the API key (requires organization-scoped API key) + * + * @tags Organizations + * @name OrganizationsUpdateOrganizationMembership + * @request PUT:/api/public/organizations/memberships + * @secure + */ + organizationsUpdateOrganizationMembership: (data, params = {}) => this.request({ + path: `/api/public/organizations/memberships`, + method: "PUT", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params + }), + /** + * @description Create or update a membership for a specific project (requires organization-scoped API key). The user must already be a member of the organization. + * + * @tags Organizations + * @name OrganizationsUpdateProjectMembership + * @request PUT:/api/public/projects/{projectId}/memberships + * @secure + */ + organizationsUpdateProjectMembership: (projectId, data, params = {}) => this.request({ + path: `/api/public/projects/${projectId}/memberships`, + method: "PUT", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params + }), + /** + * @description Create a new project (requires organization-scoped API key) + * + * @tags Projects + * @name ProjectsCreate + * @request POST:/api/public/projects + * @secure + */ + projectsCreate: (data, params = {}) => this.request({ + path: `/api/public/projects`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params + }), + /** + * @description Create a new API key for a project (requires organization-scoped API key) + * + * @tags Projects + * @name ProjectsCreateApiKey + * @request POST:/api/public/projects/{projectId}/apiKeys + * @secure + */ + projectsCreateApiKey: (projectId, data, params = {}) => this.request({ + path: `/api/public/projects/${projectId}/apiKeys`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params + }), + /** + * @description Delete a project by ID (requires organization-scoped API key). Project deletion is processed asynchronously. + * + * @tags Projects + * @name ProjectsDelete + * @request DELETE:/api/public/projects/{projectId} + * @secure + */ + projectsDelete: (projectId, params = {}) => this.request({ + path: `/api/public/projects/${projectId}`, + method: "DELETE", + secure: true, + format: "json", + ...params + }), + /** + * @description Delete an API key for a project (requires organization-scoped API key) + * + * @tags Projects + * @name ProjectsDeleteApiKey + * @request DELETE:/api/public/projects/{projectId}/apiKeys/{apiKeyId} + * @secure + */ + projectsDeleteApiKey: (projectId, apiKeyId, params = {}) => this.request({ + path: `/api/public/projects/${projectId}/apiKeys/${apiKeyId}`, + method: "DELETE", + secure: true, + format: "json", + ...params + }), + /** + * @description Get Project associated with API key + * + * @tags Projects + * @name ProjectsGet + * @request GET:/api/public/projects + * @secure + */ + projectsGet: (params = {}) => this.request({ + path: `/api/public/projects`, + method: "GET", + secure: true, + format: "json", + ...params + }), + /** + * @description Get all API keys for a project (requires organization-scoped API key) + * + * @tags Projects + * @name ProjectsGetApiKeys + * @request GET:/api/public/projects/{projectId}/apiKeys + * @secure + */ + projectsGetApiKeys: (projectId, params = {}) => this.request({ + path: `/api/public/projects/${projectId}/apiKeys`, + method: "GET", + secure: true, + format: "json", + ...params + }), + /** + * @description Update a project by ID (requires organization-scoped API key). + * + * @tags Projects + * @name ProjectsUpdate + * @request PUT:/api/public/projects/{projectId} + * @secure + */ + projectsUpdate: (projectId, data, params = {}) => this.request({ + path: `/api/public/projects/${projectId}`, + method: "PUT", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params + }), + /** + * @description Create a new version for the prompt with the given `name` + * + * @tags Prompts + * @name PromptsCreate + * @request POST:/api/public/v2/prompts + * @secure + */ + promptsCreate: (data, params = {}) => this.request({ + path: `/api/public/v2/prompts`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params + }), + /** + * @description Get a prompt + * + * @tags Prompts + * @name PromptsGet + * @request GET:/api/public/v2/prompts/{promptName} + * @secure + */ + promptsGet: ({ + promptName, + ...query + }, params = {}) => this.request({ + path: `/api/public/v2/prompts/${promptName}`, + method: "GET", + query, + secure: true, + format: "json", + ...params + }), + /** + * @description Get a list of prompt names with versions and labels + * + * @tags Prompts + * @name PromptsList + * @request GET:/api/public/v2/prompts + * @secure + */ + promptsList: (query, params = {}) => this.request({ + path: `/api/public/v2/prompts`, + method: "GET", + query, + secure: true, + format: "json", + ...params + }), + /** + * @description Update labels for a specific prompt version + * + * @tags PromptVersion + * @name PromptVersionUpdate + * @request PATCH:/api/public/v2/prompts/{name}/versions/{version} + * @secure + */ + promptVersionUpdate: (name, version2, data, params = {}) => this.request({ + path: `/api/public/v2/prompts/${name}/versions/${version2}`, + method: "PATCH", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params + }), + /** + * @description Create a new user in the organization (requires organization-scoped API key) + * + * @tags Scim + * @name ScimCreateUser + * @request POST:/api/public/scim/Users + * @secure + */ + scimCreateUser: (data, params = {}) => this.request({ + path: `/api/public/scim/Users`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params + }), + /** + * @description Remove a user from the organization (requires organization-scoped API key). Note that this only removes the user from the organization but does not delete the user entity itself. + * + * @tags Scim + * @name ScimDeleteUser + * @request DELETE:/api/public/scim/Users/{userId} + * @secure + */ + scimDeleteUser: (userId, params = {}) => this.request({ + path: `/api/public/scim/Users/${userId}`, + method: "DELETE", + secure: true, + format: "json", + ...params + }), + /** + * @description Get SCIM Resource Types (requires organization-scoped API key) + * + * @tags Scim + * @name ScimGetResourceTypes + * @request GET:/api/public/scim/ResourceTypes + * @secure + */ + scimGetResourceTypes: (params = {}) => this.request({ + path: `/api/public/scim/ResourceTypes`, + method: "GET", + secure: true, + format: "json", + ...params + }), + /** + * @description Get SCIM Schemas (requires organization-scoped API key) + * + * @tags Scim + * @name ScimGetSchemas + * @request GET:/api/public/scim/Schemas + * @secure + */ + scimGetSchemas: (params = {}) => this.request({ + path: `/api/public/scim/Schemas`, + method: "GET", + secure: true, + format: "json", + ...params + }), + /** + * @description Get SCIM Service Provider Configuration (requires organization-scoped API key) + * + * @tags Scim + * @name ScimGetServiceProviderConfig + * @request GET:/api/public/scim/ServiceProviderConfig + * @secure + */ + scimGetServiceProviderConfig: (params = {}) => this.request({ + path: `/api/public/scim/ServiceProviderConfig`, + method: "GET", + secure: true, + format: "json", + ...params + }), + /** + * @description Get a specific user by ID (requires organization-scoped API key) + * + * @tags Scim + * @name ScimGetUser + * @request GET:/api/public/scim/Users/{userId} + * @secure + */ + scimGetUser: (userId, params = {}) => this.request({ + path: `/api/public/scim/Users/${userId}`, + method: "GET", + secure: true, + format: "json", + ...params + }), + /** + * @description List users in the organization (requires organization-scoped API key) + * + * @tags Scim + * @name ScimListUsers + * @request GET:/api/public/scim/Users + * @secure + */ + scimListUsers: (query, params = {}) => this.request({ + path: `/api/public/scim/Users`, + method: "GET", + query, + secure: true, + format: "json", + ...params + }), + /** + * @description Create a score configuration (config). Score configs are used to define the structure of scores + * + * @tags ScoreConfigs + * @name ScoreConfigsCreate + * @request POST:/api/public/score-configs + * @secure + */ + scoreConfigsCreate: (data, params = {}) => this.request({ + path: `/api/public/score-configs`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params + }), + /** + * @description Get all score configs + * + * @tags ScoreConfigs + * @name ScoreConfigsGet + * @request GET:/api/public/score-configs + * @secure + */ + scoreConfigsGet: (query, params = {}) => this.request({ + path: `/api/public/score-configs`, + method: "GET", + query, + secure: true, + format: "json", + ...params + }), + /** + * @description Get a score config + * + * @tags ScoreConfigs + * @name ScoreConfigsGetById + * @request GET:/api/public/score-configs/{configId} + * @secure + */ + scoreConfigsGetById: (configId, params = {}) => this.request({ + path: `/api/public/score-configs/${configId}`, + method: "GET", + secure: true, + format: "json", + ...params + }), + /** + * @description Create a score (supports both trace and session scores) + * + * @tags Score + * @name ScoreCreate + * @request POST:/api/public/scores + * @secure + */ + scoreCreate: (data, params = {}) => this.request({ + path: `/api/public/scores`, + method: "POST", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params + }), + /** + * @description Delete a score (supports both trace and session scores) + * + * @tags Score + * @name ScoreDelete + * @request DELETE:/api/public/scores/{scoreId} + * @secure + */ + scoreDelete: (scoreId, params = {}) => this.request({ + path: `/api/public/scores/${scoreId}`, + method: "DELETE", + secure: true, + ...params + }), + /** + * @description Get a list of scores (supports both trace and session scores) + * + * @tags ScoreV2 + * @name ScoreV2Get + * @request GET:/api/public/v2/scores + * @secure + */ + scoreV2Get: (query, params = {}) => this.request({ + path: `/api/public/v2/scores`, + method: "GET", + query, + secure: true, + format: "json", + ...params + }), + /** + * @description Get a score (supports both trace and session scores) + * + * @tags ScoreV2 + * @name ScoreV2GetById + * @request GET:/api/public/v2/scores/{scoreId} + * @secure + */ + scoreV2GetById: (scoreId, params = {}) => this.request({ + path: `/api/public/v2/scores/${scoreId}`, + method: "GET", + secure: true, + format: "json", + ...params + }), + /** + * @description Get a session. Please note that `traces` on this endpoint are not paginated, if you plan to fetch large sessions, consider `GET /api/public/traces?sessionId=` + * + * @tags Sessions + * @name SessionsGet + * @request GET:/api/public/sessions/{sessionId} + * @secure + */ + sessionsGet: (sessionId2, params = {}) => this.request({ + path: `/api/public/sessions/${sessionId2}`, + method: "GET", + secure: true, + format: "json", + ...params + }), + /** + * @description Get sessions + * + * @tags Sessions + * @name SessionsList + * @request GET:/api/public/sessions + * @secure + */ + sessionsList: (query, params = {}) => this.request({ + path: `/api/public/sessions`, + method: "GET", + query, + secure: true, + format: "json", + ...params + }), + /** + * @description Delete a specific trace + * + * @tags Trace + * @name TraceDelete + * @request DELETE:/api/public/traces/{traceId} + * @secure + */ + traceDelete: (traceId, params = {}) => this.request({ + path: `/api/public/traces/${traceId}`, + method: "DELETE", + secure: true, + format: "json", + ...params + }), + /** + * @description Delete multiple traces + * + * @tags Trace + * @name TraceDeleteMultiple + * @request DELETE:/api/public/traces + * @secure + */ + traceDeleteMultiple: (data, params = {}) => this.request({ + path: `/api/public/traces`, + method: "DELETE", + body: data, + secure: true, + type: ContentType.Json, + format: "json", + ...params + }), + /** + * @description Get a specific trace + * + * @tags Trace + * @name TraceGet + * @request GET:/api/public/traces/{traceId} + * @secure + */ + traceGet: (traceId, params = {}) => this.request({ + path: `/api/public/traces/${traceId}`, + method: "GET", + secure: true, + format: "json", + ...params + }), + /** + * @description Get list of traces + * + * @tags Trace + * @name TraceList + * @request GET:/api/public/traces + * @secure + */ + traceList: (query, params = {}) => this.request({ + path: `/api/public/traces`, + method: "GET", + query, + secure: true, + format: "json", + ...params + }) + }; + } +}; +var version = "3.38.20"; +var Langfuse = class extends LangfuseCore { + constructor(params) { + const langfuseConfig = utils.configLangfuseSDK(params); + super(langfuseConfig); + if (typeof window !== "undefined" && "Deno" in window === false) { + this._storageKey = params?.persistence_name ? `lf_${params.persistence_name}` : `lf_${langfuseConfig.publicKey}_langfuse`; + this._storage = getStorage(params?.persistence || "localStorage", window); + } else { + this._storageKey = `lf_${langfuseConfig.publicKey}_langfuse`; + this._storage = getStorage("memory", void 0); + } + this.api = new LangfusePublicApi({ + baseUrl: this.baseUrl, + baseApiParams: { + headers: { + "X-Langfuse-Sdk-Name": "langfuse-js", + "X-Langfuse-Sdk-Version": this.getLibraryVersion(), + "X-Langfuse-Sdk-Variant": this.getLibraryId(), + "X-Langfuse-Sdk-Integration": this.sdkIntegration, + "X-Langfuse-Public-Key": this.publicKey, + ...this.additionalHeaders, + ...this.constructAuthorizationHeader(this.publicKey, this.secretKey) + } + } + }).api; + } + getPersistedProperty(key) { + if (!this._storageCache) { + this._storageCache = JSON.parse(this._storage.getItem(this._storageKey) || "{}") || {}; + } + return this._storageCache[key]; + } + setPersistedProperty(key, value) { + if (!this._storageCache) { + this._storageCache = JSON.parse(this._storage.getItem(this._storageKey) || "{}") || {}; + } + if (value === null) { + delete this._storageCache[key]; + } else { + this._storageCache[key] = value; + } + this._storage.setItem(this._storageKey, JSON.stringify(this._storageCache)); + } + fetch(url, options) { + return fetch(url, options); + } + getLibraryId() { + return "langfuse"; + } + getLibraryVersion() { + return version; + } + getCustomUserAgent() { + return; + } +}; +var LangfuseSingleton = class _LangfuseSingleton { + /** + * Returns the singleton instance of the Langfuse client. + * @param params Optional parameters for initializing the Langfuse instance. Only used for the first call. + * @returns The singleton instance of the Langfuse client. + */ + static getInstance(params) { + if (!_LangfuseSingleton.instance) { + _LangfuseSingleton.instance = new Langfuse(params); + } + return _LangfuseSingleton.instance; + } +}; +LangfuseSingleton.instance = null; + +// src/adapters/langfuse-sink.ts +var SDK_INTEGRATION = PLUGIN_ID; +var TRACE_NAME = "ZCode Turn"; +var LangfuseTraceSink = class { + constructor(config) { + this.config = config; + } + config; + async publishTurn(turn) { + if (!this.config.publicKey || !this.config.secretKey) return; + const client = new Langfuse({ + publicKey: this.config.publicKey, + secretKey: this.config.secretKey, + baseUrl: this.config.baseUrl, + release: this.config.release, + environment: this.config.environment, + sdkIntegration: SDK_INTEGRATION, + flushAt: 1, + fetchRetryCount: 1, + requestTimeout: 8e3 + }); + try { + const traceInput = { + name: TRACE_NAME, + sessionId: turn.sessionId, + input: turn.prompt, + metadata: { + source: "zcode", + turnId: turn.turnId, + toolCount: turn.tools.length + }, + tags: ["zcode", "zcode-hook"] + }; + if (this.config.userId) traceInput.userId = this.config.userId; + const trace = client.trace(traceInput); + for (const tool of turn.tools) { + const span = trace.span({ + name: `tool.${tool.name}`, + input: tool.input, + metadata: { + toolId: tool.id, + startedAt: tool.startedAt, + endedAt: tool.endedAt + } + }); + if (tool.error) { + span.end({ + output: { error: tool.error }, + level: "ERROR", + statusMessage: tool.error + }); + } else { + span.end({ output: tool.output }); + } + } + const generation = trace.generation({ + name: "zcode.assistant", + input: turn.prompt, + metadata: { + turnId: turn.turnId + } + }); + generation.end({ output: turn.assistantMessage }); + trace.update({ + output: turn.assistantMessage, + metadata: { + source: "zcode", + turnId: turn.turnId, + toolCount: turn.tools.length + } + }); + await client.flushAsync(); + } finally { + await client.shutdownAsync(); + } + } +}; + +// src/hooks/entry.ts +function isRecord2(value) { + return typeof value === "object" && value !== null && !Array.isArray(value); +} +function parsePayload(raw) { + if (!raw.trim()) return {}; + try { + const parsed = JSON.parse(raw); + return isRecord2(parsed) ? parsed : {}; + } catch { + return {}; + } +} +async function readStdin() { + let raw = ""; + process.stdin.setEncoding("utf8"); + for await (const chunk of process.stdin) raw += chunk; + return raw; +} +function diagnostics(enabled, message) { + if (enabled) process.stderr.write(`[${PLUGIN_ID}] ${message} +`); +} +async function main() { + const config = readConfig(); + const payload = parsePayload(await readStdin()); + const event = eventName(payload) ?? "unknown"; + const session = sessionId(payload) ?? "?"; + diagnostics(config.debug, `event=${event} session=${session}`); + const sink = isConfigured(config) ? new LangfuseTraceSink(config) : new NoopTraceSink(); + const tracker = new TurnTracker( + new JsonStateStore(), + sink, + config, + { now: () => /* @__PURE__ */ new Date() }, + { next: randomUUID2 } + ); + try { + await tracker.handle(payload); + } catch (error) { + const kind = error instanceof Error ? error.name : "unknown"; + diagnostics(config.debug, `hook failed open (${kind})`); + } + process.stdout.write("{}\n"); +} +await main(); +/*! Bundled license information: + +mustache/mustache.mjs: + (*! + * mustache.js - Logic-less {{mustache}} templates with JavaScript + * http://github.com/janl/mustache.js + *) +*/ diff --git a/plugins/zcode-plugin-langfuse/hooks/hooks.json b/plugins/zcode-plugin-langfuse/hooks/hooks.json new file mode 100644 index 0000000..ab20687 --- /dev/null +++ b/plugins/zcode-plugin-langfuse/hooks/hooks.json @@ -0,0 +1,91 @@ +{ + "description": "Langfuse observability hooks for ZCode.", + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "process", + "command": "node", + "args": [ + "${ZCODE_PLUGIN_ROOT}/hooks/entry.mjs" + ], + "timeoutMs": 5000, + "statusMessage": "Langfuse: preparing session tracing…" + } + ] + } + ], + "UserPromptSubmit": [ + { + "hooks": [ + { + "type": "process", + "command": "node", + "args": [ + "${ZCODE_PLUGIN_ROOT}/hooks/entry.mjs" + ], + "timeoutMs": 5000 + } + ] + } + ], + "PreToolUse": [ + { + "hooks": [ + { + "type": "process", + "command": "node", + "args": [ + "${ZCODE_PLUGIN_ROOT}/hooks/entry.mjs" + ], + "timeoutMs": 5000 + } + ] + } + ], + "PostToolUse": [ + { + "hooks": [ + { + "type": "process", + "command": "node", + "args": [ + "${ZCODE_PLUGIN_ROOT}/hooks/entry.mjs" + ], + "timeoutMs": 5000 + } + ] + } + ], + "PostToolUseFailure": [ + { + "hooks": [ + { + "type": "process", + "command": "node", + "args": [ + "${ZCODE_PLUGIN_ROOT}/hooks/entry.mjs" + ], + "timeoutMs": 5000 + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "process", + "command": "node", + "args": [ + "${ZCODE_PLUGIN_ROOT}/hooks/entry.mjs" + ], + "timeoutMs": 20000, + "statusMessage": "Langfuse: sending trace…" + } + ] + } + ] + } +}