From 314522bf33f4c106fcbfb093177e1759cee58efa Mon Sep 17 00:00:00 2001 From: Praveen K B Date: Sun, 16 Aug 2026 09:21:20 +0530 Subject: [PATCH 1/2] feat: Updated Open AI SDK docs --- content/docs/ingest-data/ai-agents/openai.mdx | 541 ++++++++---------- 1 file changed, 247 insertions(+), 294 deletions(-) diff --git a/content/docs/ingest-data/ai-agents/openai.mdx b/content/docs/ingest-data/ai-agents/openai.mdx index 01b58e9..f9e6e3d 100644 --- a/content/docs/ingest-data/ai-agents/openai.mdx +++ b/content/docs/ingest-data/ai-agents/openai.mdx @@ -1,327 +1,280 @@ --- title: OpenAI -description: Log OpenAI API calls and responses to Parseable +description: Send OpenAI Python SDK traces to Parseable using OpenTelemetry --- -Log OpenAI API calls, responses, and token usage to Parseable for LLM observability. +import { Step, Steps } from 'fumadocs-ui/components/steps'; +import { Tab, Tabs } from 'fumadocs-ui/components/tabs'; -## Overview +The OpenAI Python SDK does not emit OpenTelemetry data on its own — there is no built-in `openai.instrument()`. (The separate `openai-agents` package has its own built-in tracing, but that's for building agents, not for instrumenting plain `openai` client calls.) To get spans out of a plain `client.chat.completions.create()` call, you need a third-party instrumentor that monkey-patches the client. This guide covers the two most common ones: -Integrate OpenAI with Parseable to: +- **[OpenLIT](https://github.com/openlit/openlit)** — one-call `openlit.init()`, ships its own OTLP exporter setup, includes token cost calculation out of the box. Also what [CrewAI](/ingest-data/ai-agents/crewai) and [LiteLLM SDK](/ingest-data/ai-agents/litellm-sdk) integrations in this hub use. +- **[OpenInference](https://github.com/Arize-ai/openinference)** — a standard OpenTelemetry instrumentor (`OpenAIInstrumentor().instrument(tracer_provider=...)`), so you set up the OTel `TracerProvider`/exporter yourself and it composes cleanly with other OpenInference instrumentors (e.g. `CrewAIInstrumentor`) on the same provider. -- **API Logging** - Track all API calls and responses -- **Token Usage** - Monitor token consumption and costs -- **Latency Tracking** - Measure response times -- **Error Analysis** - Debug failed requests -- **Prompt Engineering** - Analyze prompt effectiveness +Both emit GenAI semantic-convention spans and land in the same shape of Parseable dataset. Pick one — don't run both against the same client, they'll double-instrument. + +## How it works + +```text +Python application using OpenAI SDK + | + | OpenLIT or OpenInference patches the OpenAI client + | + | OTLP traces + v +Parseable + | + +--> openai-sdk-traces traces dataset in Parseable +``` + +Each chat completion produces a `chat ` span carrying GenAI attributes (prompt, response, tokens, cost) plus a child `POST` span for the underlying HTTP call to `api.openai.com`. If the model responds with tool calls, the follow-up request that sends tool results back is captured as its own linked span in the same trace. ## Prerequisites -- OpenAI API key -- Parseable instance accessible -- Python or Node.js application +Before you start, keep these ready: + +- A running Parseable instance +- A Parseable API key with ingest access +- Python 3.10 or newer +- An `OPENAI_API_KEY` + +## Set up OpenAI SDK with Parseable + + + + +### Install dependencies + + + + +```bash +pip install openai openlit +``` + + + + +```bash +pip install openai \ + openinference-instrumentation-openai \ + opentelemetry-sdk \ + opentelemetry-exporter-otlp-proto-http +``` + + + -## Python Integration + + -### Basic Wrapper +### Instrument the client before making requests + +Whichever instrumentor you pick, it must run before you construct the `OpenAI` client, so the patch is in place when the client makes its first call. + + + + +`openlit.init()` sets up its own `TracerProvider` and OTLP exporter — no separate OTel setup needed. ```python -import openai -import requests -import time -from datetime import datetime -from functools import wraps - -PARSEABLE_URL = "http://parseable:8000" -PARSEABLE_AUTH = ("admin", "admin") -STREAM = "openai-logs" - -def log_to_parseable(log_entry): - try: - requests.post( - f"{PARSEABLE_URL}/api/v1/ingest", - json=[log_entry], - auth=PARSEABLE_AUTH, - headers={"X-P-Stream": STREAM} - ) - except Exception as e: - print(f"Failed to log: {e}") - -def log_openai_call(func): - @wraps(func) - def wrapper(*args, **kwargs): - start_time = time.time() - error = None - response = None - - try: - response = func(*args, **kwargs) - return response - except Exception as e: - error = str(e) - raise - finally: - duration = time.time() - start_time - - log_entry = { - "timestamp": datetime.utcnow().isoformat() + "Z", - "model": kwargs.get("model", "unknown"), - "endpoint": func.__name__, - "duration_ms": round(duration * 1000, 2), - "success": error is None, - "error": error - } - - if response: - usage = getattr(response, "usage", None) - if usage: - log_entry["prompt_tokens"] = usage.prompt_tokens - log_entry["completion_tokens"] = usage.completion_tokens - log_entry["total_tokens"] = usage.total_tokens - - log_to_parseable(log_entry) - - return wrapper - -# Wrap OpenAI client -client = openai.OpenAI() - -@log_openai_call -def chat_completion(**kwargs): - return client.chat.completions.create(**kwargs) - -# Usage -response = chat_completion( - model="gpt-4", - messages=[{"role": "user", "content": "Hello!"}] +import os + +import openlit +from openai import OpenAI + +openlit.init( + otlp_endpoint=os.environ["PARSEABLE_URL"], # e.g. http://:8010 + otlp_headers={ + "X-API-Key": os.environ["PARSEABLE_API_KEY"], + "X-P-Stream": "openai-sdk-traces", + "X-P-Log-Source": "otel-traces", + }, + service_name="openai-sdk-demo", + environment="production", + disable_batch=True, + disable_metrics=True, + disable_events=True, ) + +client = OpenAI(api_key=os.environ["OPENAI_API_KEY"]) + +response = client.chat.completions.create( + model="gpt-4o-mini", + messages=[{"role": "user", "content": "Say hello in five words."}], +) +print(response.choices[0].message.content) ``` -### Comprehensive Logger +`disable_batch=True` exports each span as soon as it finishes, which is useful for short-lived scripts. Remove it for long-running services so spans batch and export on a timer instead. + + + + +OpenInference is a plain OTel instrumentor — you build the `TracerProvider` and exporter yourself, then hand them to `OpenAIInstrumentor().instrument(...)`. This is the same pattern the [CrewAI](/ingest-data/ai-agents/crewai) integration uses to combine `CrewAIInstrumentor` and `OpenAIInstrumentor` on one provider. ```python -import openai -import requests -import json -import hashlib -from datetime import datetime -from typing import Optional, Dict, Any - -class OpenAILogger: - def __init__(self, parseable_url: str, dataset: str, username: str, password: str): - self.parseable_url = parseable_url - self.dataset = dataset - self.auth = (username, password) - self.client = openai.OpenAI() - - def _log(self, entry: Dict[str, Any]): - try: - requests.post( - f"{self.parseable_url}/api/v1/ingest", - json=[entry], - auth=self.auth, - headers={"X-P-Stream": self.dataset}, - timeout=5 - ) - except Exception as e: - print(f"Logging failed: {e}") - - def _hash_content(self, content: str) -> str: - return hashlib.sha256(content.encode()).hexdigest()[:16] - - def chat(self, messages: list, model: str = "gpt-4", **kwargs) -> Any: - start_time = datetime.utcnow() - request_id = self._hash_content(json.dumps(messages) + str(start_time)) - - log_entry = { - "timestamp": start_time.isoformat() + "Z", - "request_id": request_id, - "type": "chat_completion", - "model": model, - "message_count": len(messages), - "system_prompt": next((m["content"][:200] for m in messages if m["role"] == "system"), None), - "user_prompt": next((m["content"][:500] for m in messages if m["role"] == "user"), None), - **{k: v for k, v in kwargs.items() if k in ["temperature", "max_tokens", "top_p"]} - } - - try: - response = self.client.chat.completions.create( - model=model, - messages=messages, - **kwargs - ) - - end_time = datetime.utcnow() - log_entry.update({ - "success": True, - "duration_ms": (end_time - start_time).total_seconds() * 1000, - "prompt_tokens": response.usage.prompt_tokens, - "completion_tokens": response.usage.completion_tokens, - "total_tokens": response.usage.total_tokens, - "finish_reason": response.choices[0].finish_reason, - "response_preview": response.choices[0].message.content[:200] if response.choices else None - }) - - self._log(log_entry) - return response - - except Exception as e: - log_entry.update({ - "success": False, - "error": str(e), - "error_type": type(e).__name__ - }) - self._log(log_entry) - raise - -# Usage -logger = OpenAILogger( - parseable_url="http://parseable:8000", - dataset="openai-logs", - username="admin", - password="admin" +import os + +from openai import OpenAI +from openinference.instrumentation.openai import OpenAIInstrumentor +from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter +from opentelemetry.sdk.resources import Resource +from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace.export import BatchSpanProcessor + +provider = TracerProvider( + resource=Resource.create({"service.name": "openai-sdk-demo"}) +) +exporter = OTLPSpanExporter( + endpoint=f"{os.environ['PARSEABLE_URL']}/v1/traces", + headers={ + "X-API-Key": os.environ["PARSEABLE_API_KEY"], + "X-P-Stream": "openai-sdk-traces", + "X-P-Log-Source": "otel-traces", + }, ) +provider.add_span_processor(BatchSpanProcessor(exporter)) +OpenAIInstrumentor().instrument(tracer_provider=provider) -response = logger.chat( - messages=[ - {"role": "system", "content": "You are a helpful assistant."}, - {"role": "user", "content": "What is the capital of France?"} - ], - model="gpt-4", - temperature=0.7 +client = OpenAI(api_key=os.environ["OPENAI_API_KEY"]) + +response = client.chat.completions.create( + model="gpt-4o-mini", + messages=[{"role": "user", "content": "Say hello in five words."}], ) +print(response.choices[0].message.content) ``` -## Node.js Integration - -```javascript -const OpenAI = require('openai'); -const axios = require('axios'); - -const PARSEABLE_URL = process.env.PARSEABLE_URL || 'http://parseable:8000'; -const PARSEABLE_AUTH = Buffer.from('admin:admin').toString('base64'); - -class OpenAILogger { - constructor() { - this.client = new OpenAI(); - } - - async log(entry) { - try { - await axios.post(`${PARSEABLE_URL}/api/v1/ingest`, [entry], { - headers: { - 'Authorization': `Basic ${PARSEABLE_AUTH}`, - 'X-P-Stream': 'openai-logs', - 'Content-Type': 'application/json' - } - }); - } catch (error) { - console.error('Logging failed:', error.message); - } - } - - async chat(messages, options = {}) { - const startTime = Date.now(); - const model = options.model || 'gpt-4'; - - const logEntry = { - timestamp: new Date().toISOString(), - type: 'chat_completion', - model, - message_count: messages.length - }; - - try { - const response = await this.client.chat.completions.create({ - model, - messages, - ...options - }); - - logEntry.success = true; - logEntry.duration_ms = Date.now() - startTime; - logEntry.prompt_tokens = response.usage?.prompt_tokens; - logEntry.completion_tokens = response.usage?.completion_tokens; - logEntry.total_tokens = response.usage?.total_tokens; - logEntry.finish_reason = response.choices[0]?.finish_reason; - - await this.log(logEntry); - return response; - - } catch (error) { - logEntry.success = false; - logEntry.error = error.message; - logEntry.error_type = error.constructor.name; - await this.log(logEntry); - throw error; - } - } -} - -// Usage -const logger = new OpenAILogger(); -const response = await logger.chat([ - { role: 'user', content: 'Hello!' } -], { model: 'gpt-4' }); +`OTLPSpanExporter` here builds the URL as `{PARSEABLE_URL}/v1/traces` explicitly — unlike OpenLIT, it does not append the path for you. `BatchSpanProcessor` batches on a timer by default; for short scripts call `provider.force_flush()` (or `provider.shutdown()`) before exit so spans aren't lost. + + + + +The dataset named in `X-P-Stream` is created automatically on first ingest if it does not already exist. + + + + +### Tool calls + +Tool-calling requests instrument the same way under both instrumentors — no extra setup. OpenAI SDK-level tool call and result messages get captured as part of the same trace. + +```python +tools = [{ + "type": "function", + "function": { + "name": "get_weather", + "description": "Get current weather for a city.", + "parameters": { + "type": "object", + "properties": {"city": {"type": "string"}}, + "required": ["city"], + }, + }, +}] + +response = client.chat.completions.create( + model="gpt-4o-mini", + messages=[{"role": "user", "content": "What's the weather in Bengaluru?"}], + tools=tools, + tool_choice="auto", +) + +message = response.choices[0].message +if message.tool_calls: + messages = [{"role": "user", "content": "What's the weather in Bengaluru?"}, message] + for call in message.tool_calls: + # ... execute the tool, then append its result ... + messages.append({ + "role": "tool", + "tool_call_id": call.id, + "content": '{"city": "Bengaluru", "temp_c": 28}', + }) + client.chat.completions.create(model="gpt-4o-mini", messages=messages) +``` + + + + +### Send a few requests + +Run the application a few times with different models and prompts, including at least one tool-calling request, to see the full range of spans in Parseable. + + + + +## What you get in Parseable + +Open `openai-sdk-traces` from the Traces page. Each chat completion appears as a `chat ` span carrying the full set of GenAI attributes, with a child `POST` span for the HTTP call. Multiple calls in one process share `service.instance.id`, and tool-call follow-up requests link back to the originating trace via `span_trace_id`. + +## Useful fields + +| Field | Meaning | +| --- | --- | +| `gen_ai.provider.name` | Always `openai` for this integration | +| `gen_ai.request.model` | The model requested by the application | +| `gen_ai.response.model` | The model version that actually served the request | +| `gen_ai.operation.name` | The GenAI operation, such as `chat` | +| `gen_ai.input.messages` | The request messages, including system/user/tool roles | +| `gen_ai.output.messages` | The response messages and finish reason | +| `gen_ai.usage.input_tokens` | Input token count | +| `gen_ai.usage.output_tokens` | Output token count | +| `gen_ai.usage.cost` | Computed request cost (OpenLIT computes this; OpenInference may not, depending on version) | +| `gen_ai.server.time_to_first_token` | Time to first token | +| `span_status_code` | Whether the span completed successfully (`1` = OK) | +| `span_trace_id` / `span_parent_span_id` | Use these to reconstruct the chat span and its child HTTP span | + +## Query examples + +Total requests and tokens by model: + +```sql +SELECT + "gen_ai.request.model" AS model, + COUNT(*) AS requests, + SUM(CAST("gen_ai.usage.input_tokens" AS BIGINT)) AS input_tokens, + SUM(CAST("gen_ai.usage.output_tokens" AS BIGINT)) AS output_tokens +FROM "openai-sdk-traces" +WHERE "gen_ai.operation.name" = 'chat' +GROUP BY model; ``` -## Querying OpenAI Logs +Error rate by model: ```sql --- Token usage over time -SELECT - DATE_TRUNC('hour', timestamp) as hour, - SUM(total_tokens) as total_tokens, - SUM(prompt_tokens) as prompt_tokens, - SUM(completion_tokens) as completion_tokens, - COUNT(*) as request_count -FROM "openai-logs" -WHERE timestamp > NOW() - INTERVAL '24 hours' -GROUP BY hour -ORDER BY hour DESC - --- Average latency by model -SELECT - model, - AVG(duration_ms) as avg_latency, - PERCENTILE_CONT(0.95) WITHIN GROUP (ORDER BY duration_ms) as p95_latency, - COUNT(*) as requests -FROM "openai-logs" -WHERE success = true -GROUP BY model - --- Error rate -SELECT - DATE_TRUNC('hour', timestamp) as hour, - COUNT(*) as total, - SUM(CASE WHEN success = false THEN 1 ELSE 0 END) as errors, - ROUND(SUM(CASE WHEN success = false THEN 1 ELSE 0 END)::float / COUNT(*) * 100, 2) as error_rate -FROM "openai-logs" -GROUP BY hour -ORDER BY hour DESC - --- Cost estimation (approximate) -SELECT - model, - SUM(prompt_tokens) / 1000.0 * 0.03 as prompt_cost, - SUM(completion_tokens) / 1000.0 * 0.06 as completion_cost, - SUM(prompt_tokens) / 1000.0 * 0.03 + SUM(completion_tokens) / 1000.0 * 0.06 as total_cost -FROM "openai-logs" -WHERE timestamp > NOW() - INTERVAL '30 days' -GROUP BY model +SELECT + "gen_ai.request.model" AS model, + COUNT(*) AS total, + SUM(CASE WHEN span_status_code != 1 THEN 1 ELSE 0 END) AS errors +FROM "openai-sdk-traces" +WHERE "gen_ai.operation.name" = 'chat' +GROUP BY model; ``` -## Best Practices +## OpenLIT or OpenInference + +Use **OpenLIT** when you want a single `init()` call, built-in cost calculation, and don't need to compose with other non-OpenInference instrumentors. + +Use **OpenInference** when you're already building an OTel `TracerProvider` for other instrumentors (e.g. combining `CrewAIInstrumentor` and `OpenAIInstrumentor` on one provider, as the [CrewAI integration](/ingest-data/ai-agents/crewai) does), or you want direct control over the exporter and processors. + +## Troubleshooting + +- **No traces appear** + + Confirm the instrumentor runs before the `OpenAI` client is constructed. If the client is imported and instantiated at module load time before instrumentation runs, it cannot be patched. + +- **Traces appear late or not at all in short scripts** + + OpenLIT: set `disable_batch=True` in `openlit.init()`. OpenInference: call `provider.force_flush()` or `provider.shutdown()` before the process exits — `BatchSpanProcessor` batches on a timer by default. + +- **Prompt or response text appears in telemetry and that's a concern** -1. **Hash Sensitive Data** - Don't log full prompts if sensitive -2. **Track Request IDs** - Correlate requests across systems -3. **Monitor Costs** - Set up alerts for token usage -4. **Log Errors** - Capture error details for debugging -5. **Sample High Volume** - Consider sampling for high-traffic apps + OpenLIT: pass `capture_message_content=False` to `openlit.init()`. OpenInference: check the instrumentor's config for a content-masking option, or set `OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT=false` (or the instrumentor-specific env var) before instrumenting. -## Next Steps +## See also -- Configure [Anthropic](/ingest-data/ai-agents/anthropic) logging -- Set up [LangChain](/ingest-data/ai-agents/langchain) tracing -- Create [dashboards](/user-guide/dashboards) for LLM metrics -- Set up [alerts](/user-guide/alerting) for cost thresholds +- [LiteLLM SDK](/ingest-data/ai-agents/litellm-sdk) +- [CrewAI](/ingest-data/ai-agents/crewai) +- [Pydantic AI](/ingest-data/ai-agents/pydantic-ai) +- [Traces](/user-guide/traces) From 9e4b87e14bea000fcc8e6defb1f79759b0b25454 Mon Sep 17 00:00:00 2001 From: Praveen K B Date: Sun, 16 Aug 2026 10:10:47 +0530 Subject: [PATCH 2/2] test: add Vercel Analytics and Speed Insights for drain testing Temporary instrumentation to trigger real Speed Insights and Web Analytics beacon events from Vercel's preview deployment, so the corresponding drain types can be validated end-to-end against Parseable. Co-Authored-By: Claude Sonnet 5 --- app/layout.tsx | 4 ++ package.json | 2 + pnpm-lock.yaml | 107 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 113 insertions(+) diff --git a/app/layout.tsx b/app/layout.tsx index a149a1a..333c78c 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -2,6 +2,8 @@ import "./global.css"; import { RootProvider } from "fumadocs-ui/provider/next"; import { Inter } from "next/font/google"; import type { ReactNode } from "react"; +import { Analytics as VercelAnalytics } from "@vercel/analytics/next"; +import { SpeedInsights } from "@vercel/speed-insights/next"; import Analytics from "../components/GoogleAnalytics"; import KoalaAnalytics from "../components/KoalaAnalytics"; import { SearchProvider } from "../components/SearchProvider"; @@ -40,6 +42,8 @@ export default function Layout({ children }: { children: ReactNode }) { {gaId && } {koalaApiKey && } + + ); diff --git a/package.json b/package.json index 0cceb74..5fa4ea1 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,8 @@ "@radix-ui/react-popover": "^1.1.15", "@tabler/icons-react": "^3.30.0", "@types/lodash": "^4.17.17", + "@vercel/analytics": "^2.0.1", + "@vercel/speed-insights": "^2.0.0", "class-variance-authority": "^0.7.1", "fumadocs-core": "16.4.1", "fumadocs-mdx": "14.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4f04084..f41378f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -35,6 +35,12 @@ importers: '@types/lodash': specifier: ^4.17.17 version: 4.17.17 + '@vercel/analytics': + specifier: ^2.0.1 + version: 2.0.1(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) + '@vercel/speed-insights': + specifier: ^2.0.0 + version: 2.0.0(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3) class-variance-authority: specifier: ^0.7.1 version: 0.7.1 @@ -565,89 +571,105 @@ packages: resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-arm@1.2.4': resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-ppc64@1.2.4': resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-riscv64@1.2.4': resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-s390x@1.2.4': resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-libvips-linux-x64@1.2.4': resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-libvips-linuxmusl-arm64@1.2.4': resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-libvips-linuxmusl-x64@1.2.4': resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-linux-arm64@0.34.5': resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [glibc] '@img/sharp-linux-arm@0.34.5': resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm] os: [linux] + libc: [glibc] '@img/sharp-linux-ppc64@0.34.5': resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [ppc64] os: [linux] + libc: [glibc] '@img/sharp-linux-riscv64@0.34.5': resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [riscv64] os: [linux] + libc: [glibc] '@img/sharp-linux-s390x@0.34.5': resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [s390x] os: [linux] + libc: [glibc] '@img/sharp-linux-x64@0.34.5': resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [glibc] '@img/sharp-linuxmusl-arm64@0.34.5': resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [arm64] os: [linux] + libc: [musl] '@img/sharp-linuxmusl-x64@0.34.5': resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} cpu: [x64] os: [linux] + libc: [musl] '@img/sharp-wasm32@0.34.5': resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} @@ -833,24 +855,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@next/swc-linux-arm64-musl@16.1.1': resolution: {integrity: sha512-MFHrgL4TXNQbBPzkKKur4Fb5ICEJa87HM7fczFs2+HWblM7mMLdco3dvyTI+QmLBU9xgns/EeeINSZD6Ar+oLg==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@next/swc-linux-x64-gnu@16.1.1': resolution: {integrity: sha512-20bYDfgOQAPUkkKBnyP9PTuHiJGM7HzNBbuqmD0jiFVZ0aOldz+VnJhbxzjcSabYsnNjMPsE0cyzEudpYxsrUQ==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@next/swc-linux-x64-musl@16.1.1': resolution: {integrity: sha512-9pRbK3M4asAHQRkwaXwu601oPZHghuSC8IXNENgbBSyImHv/zY4K5udBusgdHkvJ/Tcr96jJwQYOll0qU8+fPA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@next/swc-win32-arm64-msvc@16.1.1': resolution: {integrity: sha512-bdfQkggaLgnmYrFkSQfsHfOhk/mCYmjnrbRCGgkMcoOBZ4n+TRRSLmT/CU5SATzlBJ9TpioUyBW/vWFXTqQRiA==} @@ -1405,24 +1431,28 @@ packages: engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-arm64-musl@4.1.6': resolution: {integrity: sha512-8kjivE5xW0qAQ9HX9reVFmZj3t+VmljDLVRJpVBEoTR+3bKMnvC7iLcoSGNIUJGOZy1mLVq7x/gerVg0T+IsYw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] + libc: [musl] '@tailwindcss/oxide-linux-x64-gnu@4.1.6': resolution: {integrity: sha512-A4spQhwnWVpjWDLXnOW9PSinO2PTKJQNRmL/aIl2U/O+RARls8doDfs6R41+DAXK0ccacvRyDpR46aVQJJCoCg==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [glibc] '@tailwindcss/oxide-linux-x64-musl@4.1.6': resolution: {integrity: sha512-YRee+6ZqdzgiQAHVSLfl3RYmqeeaWVCk796MhXhLQu2kJu2COHBkqlqsqKYx3p8Hmk5pGCQd2jTAoMWWFeyG2A==} engines: {node: '>= 10'} cpu: [x64] os: [linux] + libc: [musl] '@tailwindcss/oxide-wasm32-wasi@4.1.6': resolution: {integrity: sha512-qAp4ooTYrBQ5pk5jgg54/U1rCJ/9FLYOkkQ/nTE+bVMseMfB6O7J8zb19YTpWuu4UdfRf5zzOrNKfl6T64MNrQ==} @@ -1610,41 +1640,49 @@ packages: resolution: {integrity: sha512-jon9M7DKRLGZ9VYSkFMflvNqu9hDtOCEnO2QAryFWgT6o6AXU8du56V7YqnaLKr6rAbZBWYsYpikF226v423QA==} cpu: [arm64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-arm64-musl@1.7.2': resolution: {integrity: sha512-c8Cg4/h+kQ63pL43wBNaVMmOjXI/X62wQmru51qjfTvI7kmCy5uHTJvK/9LrF0G8Jdx8r34d019P1DVJmhXQpA==} cpu: [arm64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-ppc64-gnu@1.7.2': resolution: {integrity: sha512-A+lcwRFyrjeJmv3JJvhz5NbcCkLQL6Mk16kHTNm6/aGNc4FwPHPE4DR9DwuCvCnVHvF5IAd9U4VIs/VvVir5lg==} cpu: [ppc64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-gnu@1.7.2': resolution: {integrity: sha512-hQQ4TJQrSQW8JlPm7tRpXN8OCNP9ez7PajJNjRD1ZTHQAy685OYqPrKjfaMw/8LiHCt8AZ74rfUVHP9vn0N69Q==} cpu: [riscv64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-riscv64-musl@1.7.2': resolution: {integrity: sha512-NoAGbiqrxtY8kVooZ24i70CjLDlUFI7nDj3I9y54U94p+3kPxwd2L692YsdLa+cqQ0VoqMWoehDFp21PKRUoIQ==} cpu: [riscv64] os: [linux] + libc: [musl] '@unrs/resolver-binding-linux-s390x-gnu@1.7.2': resolution: {integrity: sha512-KaZByo8xuQZbUhhreBTW+yUnOIHUsv04P8lKjQ5otiGoSJ17ISGYArc+4vKdLEpGaLbemGzr4ZeUbYQQsLWFjA==} cpu: [s390x] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-gnu@1.7.2': resolution: {integrity: sha512-dEidzJDubxxhUCBJ/SHSMJD/9q7JkyfBMT77Px1npl4xpg9t0POLvnWywSk66BgZS/b2Hy9Y1yFaoMTFJUe9yg==} cpu: [x64] os: [linux] + libc: [glibc] '@unrs/resolver-binding-linux-x64-musl@1.7.2': resolution: {integrity: sha512-RvP+Ux3wDjmnZDT4XWFfNBRVG0fMsc+yVzNFUqOflnDfZ9OYujv6nkh+GOr+watwrW4wdp6ASfG/e7bkDradsw==} cpu: [x64] os: [linux] + libc: [musl] '@unrs/resolver-binding-wasm32-wasi@1.7.2': resolution: {integrity: sha512-y797JBmO9IsvXVRCKDXOxjyAE4+CcZpla2GSoBQ33TVb3ILXuFnMrbR/QQZoauBYeOFuu4w3ifWLw52sdHGz6g==} @@ -1666,6 +1704,61 @@ packages: cpu: [x64] os: [win32] + '@vercel/analytics@2.0.1': + resolution: {integrity: sha512-MTQG6V9qQrt1tsDeF+2Uoo5aPjqbVPys1xvnIftXSJYG2SrwXRHnqEvVoYID7BTruDz4lCd2Z7rM1BdkUehk2g==} + peerDependencies: + '@remix-run/react': ^2 + '@sveltejs/kit': ^1 || ^2 + next: '>= 13' + nuxt: '>= 3' + react: ^18 || ^19 || ^19.0.0-rc + svelte: '>= 4' + vue: ^3 + vue-router: ^4 + peerDependenciesMeta: + '@remix-run/react': + optional: true + '@sveltejs/kit': + optional: true + next: + optional: true + nuxt: + optional: true + react: + optional: true + svelte: + optional: true + vue: + optional: true + vue-router: + optional: true + + '@vercel/speed-insights@2.0.0': + resolution: {integrity: sha512-jwkNcrTeafWxjmWq4AHBaptSqZiJkYU5adLC9QBSqeim0GcqDMgN5Ievh8OG1rJ6W3A4l1oiP7qr9CWxGuzu3w==} + peerDependencies: + '@sveltejs/kit': ^1 || ^2 + next: '>= 13' + nuxt: '>= 3' + react: ^18 || ^19 || ^19.0.0-rc + svelte: '>= 4' + vue: ^3 + vue-router: ^4 + peerDependenciesMeta: + '@sveltejs/kit': + optional: true + next: + optional: true + nuxt: + optional: true + react: + optional: true + svelte: + optional: true + vue: + optional: true + vue-router: + optional: true + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -2846,24 +2939,28 @@ packages: engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [glibc] lightningcss-linux-arm64-musl@1.29.2: resolution: {integrity: sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==} engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] + libc: [musl] lightningcss-linux-x64-gnu@1.29.2: resolution: {integrity: sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [glibc] lightningcss-linux-x64-musl@1.29.2: resolution: {integrity: sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==} engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] + libc: [musl] lightningcss-win32-arm64-msvc@1.29.2: resolution: {integrity: sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==} @@ -5488,6 +5585,16 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.7.2': optional: true + '@vercel/analytics@2.0.1(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)': + optionalDependencies: + next: 16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + + '@vercel/speed-insights@2.0.0(next@16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(react@19.2.3)': + optionalDependencies: + next: 16.1.1(@babel/core@7.28.5)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + react: 19.2.3 + acorn-jsx@5.3.2(acorn@8.14.1): dependencies: acorn: 8.14.1