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/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) 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