diff --git a/.circleci/scripts/run_integration.sh b/.circleci/scripts/run_integration.sh index f3166e5db39..c03220224d2 100644 --- a/.circleci/scripts/run_integration.sh +++ b/.circleci/scripts/run_integration.sh @@ -169,7 +169,7 @@ start_proxy() { INTEGRATION_UPSTREAM_URL="$INTEGRATION_UPSTREAM_URL" \ LITELLM_MASTER_KEY="$LITELLM_MASTER_KEY" LITELLM_SALT_KEY="$LITELLM_SALT_KEY" LITELLM_UI_PATH="$LITELLM_UI_PATH" PROXY_BASE_URL="http://127.0.0.1:$port" \ LITELLM_LICENSE="${LITELLM_LICENSE:-}" \ - LITELLM_MODE=PRODUCTION STORE_MODEL_IN_DB=True "${cost_map_env[@]}" \ + LITELLM_MODE=PRODUCTION STORE_MODEL_IN_DB=True LITELLM_ENABLE_MCP_STDIO=true "${cost_map_env[@]}" \ AWS_EC2_METADATA_DISABLED=true DO_NOT_TRACK=1 COVERAGE_FILE="$coverage_data" \ "${proxy_command[@]}" --config tests/integration/proxy_config.yaml \ --host 127.0.0.1 --port "$port" --num_workers 1 --telemetry False \ diff --git a/.github/workflows/test-e2e-changed.yml b/.github/workflows/test-e2e-changed.yml index 8e03a902383..228e23f60d7 100644 --- a/.github/workflows/test-e2e-changed.yml +++ b/.github/workflows/test-e2e-changed.yml @@ -176,6 +176,7 @@ jobs: TESTS: ${{ needs.detect.outputs.tests }} E2E_FIXTURE_MODE: live E2E_PROVIDER_EDGE_HOST_REACHABLE: '1' + E2E_OWNED_GATEWAY: '1' COLUMNS: '400' run: | umask 077 diff --git a/.github/workflows/test-mcp-dependency-resolution.yml b/.github/workflows/test-mcp-dependency-resolution.yml index 463d6a7e9e8..8f70375a181 100644 --- a/.github/workflows/test-mcp-dependency-resolution.yml +++ b/.github/workflows/test-mcp-dependency-resolution.yml @@ -17,7 +17,7 @@ concurrency: jobs: resolve: runs-on: ubuntu-latest - timeout-minutes: 15 + timeout-minutes: 25 strategy: fail-fast: false matrix: diff --git a/.github/workflows/test-rust.yml b/.github/workflows/test-rust.yml index b0935263d28..07e2c3f554c 100644 --- a/.github/workflows/test-rust.yml +++ b/.github/workflows/test-rust.yml @@ -89,7 +89,7 @@ jobs: rust-test: runs-on: ubuntu-latest - timeout-minutes: 20 + timeout-minutes: 30 defaults: run: working-directory: litellm-rust diff --git a/.gitignore b/.gitignore index ac8e2919372..399458eced5 100644 --- a/.gitignore +++ b/.gitignore @@ -58,6 +58,7 @@ litellm/proxy/tests/package-lock.json ui/litellm-dashboard/.next ui/litellm-dashboard/node_modules ui/litellm-dashboard/next-env.d.ts +*.tsbuildinfo ui/litellm-dashboard/package.json ui/litellm-dashboard/package-lock.json helm/litellm-helm/*.tgz diff --git a/backend/main.py b/backend/main.py index 292ece48e7d..e0cef90c979 100644 --- a/backend/main.py +++ b/backend/main.py @@ -8,9 +8,13 @@ Run with: uvicorn backend.main:app --host 0.0.0.0 --port 4001 """ +from collections.abc import AsyncGenerator, Mapping from contextlib import asynccontextmanager +from typing import Final -from fastapi.routing import Mount +from starlette.applications import Starlette +from starlette.routing import Mount +from starlette.types import Lifespan # See gateway/main.py for why we assemble DATABASE_URL(s) here before # importing proxy_server. @@ -43,14 +47,16 @@ def _is_backend_route(route) -> bool: # See gateway/main.py for why the trim runs inside the lifespan instead of at # module scope. -_proxy_lifespan = app.router.lifespan_context +_proxy_lifespan: Final = app.router.lifespan_context @asynccontextmanager -async def _backend_lifespan(app_): - async with _proxy_lifespan(app_): +async def _backend_lifespan( + app_: Starlette, lifespan: Lifespan[Starlette] = _proxy_lifespan +) -> AsyncGenerator[Mapping[str, object], None]: + async with lifespan(app_) as state: app_.router.routes = [r for r in app_.router.routes if _is_backend_route(r)] - yield + yield state if state is not None else {} app.router.lifespan_context = _backend_lifespan diff --git a/deploy/lens/README.md b/deploy/lens/README.md index 315d072b501..4b65c88afcd 100644 --- a/deploy/lens/README.md +++ b/deploy/lens/README.md @@ -19,11 +19,13 @@ The URL, database, and retention settings can also come from `CLICKHOUSE_URL`, ` Retention changes require a proxy restart. ClickHouse removes expired rows during background merges, not immediately at startup. Enable request/response logging to analyze LLM requests. Lens can only inspect content you actually retain -In Lens, click **Set up analysis**, choose an existing virtual key or **Create worker key**, then **Generate setup command**. The LiteLLM address is filled in for you; change it only if the server running Docker needs a different network address. Copy the command and run it on your server. The dialog changes to **Analyzer connected** when the container checks in +In **Lens > Investigations**, click **Connect worker**, choose an analysis model and monthly limit, then **Get install command**. Use **Advanced options** to select an existing virtual key or change the proxy URL if the server running Docker needs a different network address. Copy the command and run it on your server. The dashboard shows **Worker connected** when the container checks in The command already contains the compatible worker image and one worker token. The selected virtual key stays on the proxy; its secret is never sent to the worker. No source checkout, environment file, or second LiteLLM deployment is needed. Keep the command private because it includes the token. The LiteLLM release provides the dashboard and APIs; the container only runs background analysis -The dashboard and Compose file pin a verified worker image by digest. The image uses Linux amd64, and the generated command selects that platform. Worker image releases are independent of proxy releases: update the pinned image when changing their API contract. CI also publishes immutable commit tags for reproducible builds +The dashboard and Compose file pin a verified worker image by digest. The image uses Linux amd64, and the generated command selects that platform. CI also publishes immutable `:sha-` tags for successful worker builds on `main`. Keep the worker image compatible with your gateway version + +After upgrading the gateway, update the worker image and redeploy it while keeping its proxy URL and token. Existing containers do not update automatically. If an investigation reports a worker compatibility error, update the image before retrying For deployments managed with Compose, download `compose.yaml` and provide `LITELLM_URL` and `LENS_WORKER_TOKEN` in an environment file. Its default image is already selected: diff --git a/deploy/lens/compose.yaml b/deploy/lens/compose.yaml index 4d1224fd41e..fc9850fcb04 100644 --- a/deploy/lens/compose.yaml +++ b/deploy/lens/compose.yaml @@ -1,6 +1,6 @@ services: lens-worker: - image: ${LENS_WORKER_IMAGE:-ghcr.io/berriai/litellm-lens-worker@sha256:67eba741c1b97c749975c5c38e2370a603e1105babc908d613c1b79d7b995393} + image: ${LENS_WORKER_IMAGE:-ghcr.io/berriai/litellm-lens-worker@sha256:44f0597c7583dcfef999ece9a8bc02cfeb9f0f5167a1221cee3bd10b1b79271b} environment: LITELLM_URL: ${LITELLM_URL:?Set the URL reachable from this container} LENS_WORKER_TOKEN: ${LENS_WORKER_TOKEN:?Create a worker credential in the Lens UI} diff --git a/gateway/main.py b/gateway/main.py index 61b885b27e4..fb4ae830808 100644 --- a/gateway/main.py +++ b/gateway/main.py @@ -9,9 +9,13 @@ Run with: uvicorn gateway.main:app --host 0.0.0.0 --port 4000 """ +from collections.abc import AsyncGenerator, Mapping from contextlib import asynccontextmanager +from typing import Final -from fastapi.routing import Mount +from starlette.applications import Starlette +from starlette.routing import Mount +from starlette.types import Lifespan # Assemble DATABASE_URL (+ DATABASE_URL_READ_REPLICA) from the discrete # DATABASE_* env vars before proxy_server imports spin up Prisma. Handles @@ -54,14 +58,16 @@ def _is_gateway_route(route) -> bool: # register routes. A module-load filter would miss routes added during # startup; running inside the lifespan, after the inner __aenter__, catches # them while still completing before uvicorn opens the listener. -_proxy_lifespan = app.router.lifespan_context +_proxy_lifespan: Final = app.router.lifespan_context @asynccontextmanager -async def _gateway_lifespan(app_): - async with _proxy_lifespan(app_): +async def _gateway_lifespan( + app_: Starlette, lifespan: Lifespan[Starlette] = _proxy_lifespan +) -> AsyncGenerator[Mapping[str, object], None]: + async with lifespan(app_) as state: app_.router.routes = [r for r in app_.router.routes if _is_gateway_route(r)] - yield + yield state if state is not None else {} app.router.lifespan_context = _gateway_lifespan diff --git a/litellm-proxy-extras/litellm_proxy_extras/migrations/20261001200000_add_autorouter_daily_spend/migration.sql b/litellm-proxy-extras/litellm_proxy_extras/migrations/20261001200000_add_autorouter_daily_spend/migration.sql new file mode 100644 index 00000000000..ce166b4df45 --- /dev/null +++ b/litellm-proxy-extras/litellm_proxy_extras/migrations/20261001200000_add_autorouter_daily_spend/migration.sql @@ -0,0 +1,17 @@ +CREATE TABLE IF NOT EXISTS "LiteLLM_AutoRouterDailySpend" ( + "date" TEXT NOT NULL, + "api_key" TEXT NOT NULL, + "user_id" TEXT NOT NULL, + "router_name" TEXT NOT NULL, + "router_type" TEXT NOT NULL, + "turns" INTEGER NOT NULL DEFAULT 0, + "spend" DOUBLE PRECISION NOT NULL DEFAULT 0, + "saved_spend" DOUBLE PRECISION NOT NULL DEFAULT 0, + "savings_estimated_turns" INTEGER NOT NULL DEFAULT 0, + "savings_estimated_actual_spend" DOUBLE PRECISION NOT NULL DEFAULT 0, + "savings_estimated_saved_spend" DOUBLE PRECISION NOT NULL DEFAULT 0, + "classifier_cost" DOUBLE PRECISION NOT NULL DEFAULT 0, + "classifier_cost_recorded_turns" INTEGER NOT NULL DEFAULT 0, + + CONSTRAINT "LiteLLM_AutoRouterDailySpend_pkey" PRIMARY KEY ("date", "api_key", "user_id", "router_name", "router_type") +); diff --git a/litellm-proxy-extras/litellm_proxy_extras/migrations/20261002220000_lens_worker_scope_index/migration.sql b/litellm-proxy-extras/litellm_proxy_extras/migrations/20261002220000_lens_worker_scope_index/migration.sql new file mode 100644 index 00000000000..124e5713994 --- /dev/null +++ b/litellm-proxy-extras/litellm_proxy_extras/migrations/20261002220000_lens_worker_scope_index/migration.sql @@ -0,0 +1,3 @@ +CREATE INDEX IF NOT EXISTS "LiteLLM_LensWorker_active_scope_idx" +ON "LiteLLM_LensWorker" USING GIN ((data->'scope') jsonb_path_ops) +WHERE data @> '{"revoked": false}'::jsonb; diff --git a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma index 6f285e9dc39..aba89526cf6 100644 --- a/litellm-proxy-extras/litellm_proxy_extras/schema.prisma +++ b/litellm-proxy-extras/litellm_proxy_extras/schema.prisma @@ -1744,6 +1744,27 @@ model LiteLLM_AutoRouterUserSession { @@index([user_id, last_turn_at], map: "idx_autorouter_user_session_user_last_turn") } +// Auto-routed requests per UTC request day and router: the selected-day money behind the +// auto-router usage view. Written in the same statement as the session rollup, so a day row +// and its session row never disagree; corrected in the same transaction as late baselines. +model LiteLLM_AutoRouterDailySpend { + date String + api_key String + user_id String + router_name String + router_type String + turns Int @default(0) + spend Float @default(0) + saved_spend Float @default(0) + savings_estimated_turns Int @default(0) + savings_estimated_actual_spend Float @default(0) + savings_estimated_saved_spend Float @default(0) + classifier_cost Float @default(0) + classifier_cost_recorded_turns Int @default(0) + + @@id([date, api_key, user_id, router_name, router_type]) +} + // Shadow eval: evaluation of an auto-router against one or more keys' live traffic, in // either direction. forward duplicates the requests the keys did not route through the // router through it, answering whether they should adopt it; reverse duplicates the diff --git a/litellm-proxy-extras/litellm_proxy_extras/utils.py b/litellm-proxy-extras/litellm_proxy_extras/utils.py index 3acc19d397d..245244250ee 100644 --- a/litellm-proxy-extras/litellm_proxy_extras/utils.py +++ b/litellm-proxy-extras/litellm_proxy_extras/utils.py @@ -78,6 +78,23 @@ class _InvalidIndex: table_size: str MAX_MIGRATE_DEPLOY_ATTEMPTS = 4 +LIBPQ_URL_PARAMS: Final = frozenset( + { + "sslmode", + "sslcert", + "sslkey", + "sslrootcert", + "sslpassword", + "application_name", + "connect_timeout", + "client_encoding", + "options", + "service", + "gssencmode", + "krbsrvname", + "target_session_attrs", + } +) @dataclass(frozen=True) @@ -336,9 +353,8 @@ class ProxyExtrasDBManager: pass @staticmethod - def _failed_migration_logs(migration_name: str) -> Optional[str]: - """Return failed migration logs, or None if the ledger is unavailable.""" - database_url = os.getenv("DATABASE_URL") + def _read_migration_ledger(query: str, params: tuple[str, ...]) -> "tuple[object, ...] | None": + database_url: Final = os.getenv("DATABASE_URL") if not database_url: return None @@ -347,28 +363,37 @@ class ProxyExtrasDBManager: except ImportError: return None - cleaned_url = ProxyExtrasDBManager._strip_prisma_query_params(database_url) - ledger_table = psycopg.sql.SQL("{}.{}").format( - psycopg.sql.Identifier( - ProxyExtrasDBManager._prisma_schema_param(database_url) or "public" - ), + cleaned_url: Final = ProxyExtrasDBManager._strip_prisma_query_params(database_url) + ledger_table: Final = psycopg.sql.SQL("{}.{}").format( + psycopg.sql.Identifier(ProxyExtrasDBManager._prisma_schema_param(database_url) or "public"), psycopg.sql.Identifier("_prisma_migrations"), ) try: - with psycopg.connect( - cleaned_url, connect_timeout=10, autocommit=True - ) as conn: - row = conn.execute( - psycopg.sql.SQL( - "SELECT logs FROM {} " - "WHERE migration_name = %s AND finished_at IS NULL " - "AND rolled_back_at IS NULL" - ).format(ledger_table), - (migration_name,), - ).fetchone() + with psycopg.connect(cleaned_url, connect_timeout=10, autocommit=True) as conn: + row: Final = conn.execute(psycopg.sql.SQL(query).format(ledger_table), params).fetchone() except (psycopg.OperationalError, psycopg.DatabaseError): return None - return (row[0] or "") if row else "" + return tuple(row) if row is not None else () + + @staticmethod + def _failed_migration_logs(migration_name: str, started_at: str) -> Optional[str]: + row: Final = ProxyExtrasDBManager._read_migration_ledger( + "SELECT logs FROM {} WHERE migration_name = %s AND started_at = %s::timestamptz " + "AND finished_at IS NULL AND rolled_back_at IS NULL", + (migration_name, started_at), + ) + if row is None: + return None + return row[0] if row and isinstance(row[0], str) else "" + + @staticmethod + def _failed_migration_recovered(migration_name: str, started_at: str) -> bool: + row: Final = ProxyExtrasDBManager._read_migration_ledger( + "SELECT 1 FROM {} WHERE migration_name = %s AND started_at = %s::timestamptz " + "AND (finished_at IS NOT NULL OR rolled_back_at IS NOT NULL)", + (migration_name, started_at), + ) + return bool(row) @staticmethod def _resolve_specific_migration(migration_name: str): @@ -689,30 +714,43 @@ class ProxyExtrasDBManager: @staticmethod def _strip_prisma_query_params(url: str) -> str: - """Remove Prisma-specific query params (connection_limit, pool_timeout, - schema, etc.) from DATABASE_URL so psycopg can parse it.""" + """Rewrite a Prisma-dialect URL for libpq: drop the Prisma-only params + (connection_limit, pool_timeout, schema, pgbouncer, sslaccept, ...) and + translate Prisma's TLS params back, since libpq reads ``sslcert`` as a + client certificate where Prisma reads it as the CA.""" from urllib.parse import parse_qsl, quote, urlencode, urlparse, urlunparse - parsed = urlparse(url) + parsed: Final = urlparse(url) if not parsed.query: return url - libpq_params = { - "sslmode", - "sslcert", - "sslkey", - "sslrootcert", - "sslpassword", - "application_name", - "connect_timeout", - "client_encoding", - "options", - "service", - "gssencmode", - "krbsrvname", - "target_session_attrs", - } - kept = [(k, v) for k, v in parse_qsl(parsed.query) if k in libpq_params] - return urlunparse(parsed._replace(query=urlencode(kept, quote_via=quote))) + pairs: Final = tuple(parse_qsl(parsed.query)) + kept: Final = tuple((k, v) for k, v in pairs if k in LIBPQ_URL_PARAMS) + sslaccept: Final = next((v for k, v in pairs if k == "sslaccept"), None) + libpq_pairs: Final = ProxyExtrasDBManager._libpq_tls_params(kept, sslaccept) + return urlunparse(parsed._replace(query=urlencode(libpq_pairs, quote_via=quote))) + + @staticmethod + def _libpq_tls_params( + pairs: "tuple[tuple[str, str], ...]", sslaccept: "str | None" + ) -> "tuple[tuple[str, str], ...]": + """Undo ``translate_libpq_ssl_params``. Prisma's ``sslcert`` is the CA and + ``sslaccept=strict`` checks chain and hostname, which libpq only does in + ``sslmode=verify-full``, so strict becomes ``sslrootcert`` plus + ``verify-full`` whatever ``sslmode`` said (``disable`` stays off). Prisma + defaults an absent ``sslaccept`` to ``accept_invalid_certs`` and anything + else to strict. Without strict it checks nothing, so the CA is dropped and + ``sslmode`` is kept as is: libpq only verifies when a root cert is present. + A URL that also carries ``sslkey`` is libpq's own client-certificate form + and is kept.""" + keys: Final = frozenset(k for k, _ in pairs) + if "sslcert" not in keys or "sslkey" in keys: + return pairs + sslmode: Final = next((v for k, v in pairs if k == "sslmode"), None) + rest: Final = tuple((k, v) for k, v in pairs if k not in ("sslcert", "sslmode")) + if sslaccept in (None, "accept_invalid_certs") or sslmode == "disable": + return rest if sslmode is None else rest + (("sslmode", sslmode),) + root_cert: Final = tuple(("sslrootcert", v) for k, v in pairs if k == "sslcert" and "sslrootcert" not in keys) + return rest + root_cert + (("sslmode", "verify-full"),) @staticmethod def _warn_if_db_ahead_of_head(migrations_dir: str) -> None: @@ -1072,6 +1110,11 @@ class ProxyExtrasDBManager: return match.group(1) if match else None return None + @staticmethod + def _v2_failed_migration_started_at(stderr: str, migration_name: str) -> "str | None": + match: Final = re.search(rf"`{re.escape(migration_name)}` migration started at ([^\r\n]+?) failed", stderr) + return match.group(1) if match else None + @staticmethod def _v2_roll_back_migration_best_effort(migration_name: str) -> None: from litellm_proxy_extras.migration_lock import migration_environment @@ -1100,8 +1143,11 @@ class ProxyExtrasDBManager: if "P3009" in stderr: migration_name = ProxyExtrasDBManager._v2_failed_migration_name(stderr) - if migration_name: - ledger_logs = ProxyExtrasDBManager._failed_migration_logs(migration_name) + started_at: Final = ( + ProxyExtrasDBManager._v2_failed_migration_started_at(stderr, migration_name) if migration_name else None + ) + if migration_name and started_at: + ledger_logs: Final = ProxyExtrasDBManager._failed_migration_logs(migration_name, started_at) if ledger_logs and _MIGRATION_DEADLOCK_MARKER in ledger_logs: logger.info( "Migration %s failed in a concurrent migrate deploy " @@ -1110,6 +1156,14 @@ class ProxyExtrasDBManager: ) ProxyExtrasDBManager._v2_roll_back_migration_best_effort(migration_name) return budget.spend() + if ProxyExtrasDBManager._failed_migration_recovered(migration_name, started_at): + logger.info( + "Migration %s started at %s was already rolled back or completed by a concurrent " + "migrate deploy, retrying", + migration_name, + started_at, + ) + return budget.spend() raise RuntimeError( "Migration completion could not be verified. LiteLLM startup has stopped.\n\n" f"Prisma migration history (migration name and start time):\n{stderr}\n\n" diff --git a/litellm-proxy-extras/pyproject.toml b/litellm-proxy-extras/pyproject.toml index c6d6060acfa..79549a88cd9 100644 --- a/litellm-proxy-extras/pyproject.toml +++ b/litellm-proxy-extras/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "litellm-proxy-extras" -version = "0.4.104" +version = "0.4.105" description = "Additional files for the LiteLLM Proxy. Reduces the size of the main litellm package." readme = "README.md" requires-python = ">=3.9" @@ -30,7 +30,7 @@ required-version = ">=0.10.9" module-root = "" [tool.commitizen] -version = "0.4.104" +version = "0.4.105" version_files = [ "pyproject.toml:^version", "../pyproject.toml:litellm-proxy-extras==", diff --git a/litellm-rust/Cargo.lock b/litellm-rust/Cargo.lock index 40552b19e43..54e814994b4 100644 --- a/litellm-rust/Cargo.lock +++ b/litellm-rust/Cargo.lock @@ -4156,6 +4156,7 @@ dependencies = [ "litellm-storage-clickhouse", "litellm-token-counter", "litellm-traces", + "litellm-traces-clickhouse", "litellm-tracing", "prost", "pyo3", @@ -4369,6 +4370,7 @@ dependencies = [ "thiserror 2.0.19", "tokio", "url", + "wiremock", ] [[package]] @@ -4451,19 +4453,30 @@ dependencies = [ name = "litellm-traces" version = "0.1.0" dependencies = [ - "askama", - "base64 0.22.1", "criterion", + "indexmap 2.14.0", + "opentelemetry-proto", + "prost", + "rstest", + "serde", + "serde_json", + "strum", + "thiserror 2.0.19", +] + +[[package]] +name = "litellm-traces-clickhouse" +version = "0.1.0" +dependencies = [ + "askama", "flate2", "futures-util", "hmac 0.12.1", - "indexmap 2.14.0", "litellm-http", "litellm-migrate", "litellm-storage-clickhouse", + "litellm-traces", "moka", - "opentelemetry-proto", - "prost", "rstest", "serde", "serde_json", diff --git a/litellm-rust/Cargo.toml b/litellm-rust/Cargo.toml index f4cb2ecbe59..7c32e82eb4c 100644 --- a/litellm-rust/Cargo.toml +++ b/litellm-rust/Cargo.toml @@ -13,6 +13,7 @@ litellm-config = { path = "crates/config" } litellm-router = { path = "crates/router" } litellm-tracing = { path = "crates/tracing" } litellm-traces = { path = "crates/traces" } +litellm-traces-clickhouse = { path = "crates/traces-clickhouse" } litellm-storage-clickhouse = { path = "crates/storage-clickhouse" } litellm-migrate = { path = "crates/migrate" } litellm-migrate-macros = { path = "crates/migrate-macros" } diff --git a/litellm-rust/crates/python-bridge/Cargo.toml b/litellm-rust/crates/python-bridge/Cargo.toml index 2b505f08eca..c3a86009111 100644 --- a/litellm-rust/crates/python-bridge/Cargo.toml +++ b/litellm-rust/crates/python-bridge/Cargo.toml @@ -22,6 +22,7 @@ tiktoken = ["litellm-token-counter/tiktoken"] fancy-regex.workspace = true litellm-tracing.workspace = true litellm-traces.workspace = true +litellm-traces-clickhouse.workspace = true litellm-storage-clickhouse.workspace = true litellm-host.workspace = true bytes.workspace = true diff --git a/litellm-rust/crates/python-bridge/src/routes/traces.rs b/litellm-rust/crates/python-bridge/src/routes/traces.rs index f81fc6a6d75..a8e7f441eff 100644 --- a/litellm-rust/crates/python-bridge/src/routes/traces.rs +++ b/litellm-rust/crates/python-bridge/src/routes/traces.rs @@ -2,10 +2,8 @@ use std::collections::BTreeMap; use litellm_host_python::{FromPythonCache, ToPythonCache}; use litellm_http::ClientVariant; -use litellm_traces::{ - Config, Error, InsertTable, Parameter, QueryAccessError, QueryReaders, QueryScope, ReadQuery, - Shared, -}; +use litellm_traces::{QueryScope, ReadQuery, Shared}; +use litellm_traces_clickhouse::{Config, Error, InsertTable, Parameter, QueryReaders}; use prost::Message; use pyo3::{ exceptions::{PyOverflowError, PyRuntimeError, PyValueError}, @@ -31,38 +29,56 @@ pub fn trace_encode_error<'py>(py: Python<'py>, message: &str) -> Bound<'py, PyB } fn map_error(error: Error) -> PyErr { + map_error_ref(&error) +} + +fn map_error_ref(error: &Error) -> PyErr { + use litellm_storage_clickhouse::Error as StorageError; + match error { Error::InvalidRow | Error::InvalidTable | Error::InvalidSchema - | Error::EmptySql - | Error::InvalidQuery => PyValueError::new_err(error.to_string()), + | Error::InvalidQuery + | Error::InvalidParameters + | Error::InvalidScope => PyValueError::new_err(error.to_string()), Error::InsertTooLarge => PyOverflowError::new_err(error.to_string()), - Error::InvalidUrl - | Error::QueryFailed(_) - | Error::InsertFailed(_) - | Error::SchemaFailed(_) - | Error::ResponseTooLarge - | Error::InvalidResponse - | Error::Transport => PyRuntimeError::new_err(error.to_string()), + Error::SchemaFailed(_) + | Error::SchemaTransport + | Error::MissingSecret + | Error::Busy + | Error::ProvisionFailed(_) + | Error::ProvisionTransport + | Error::InvalidResponse => PyRuntimeError::new_err(error.to_string()), + Error::Cached(source) => map_error_ref(source), + Error::Storage(source) => match source { + StorageError::InvalidRow + | StorageError::InvalidTable + | StorageError::InvalidSchema + | StorageError::EmptySql + | StorageError::InvalidParameters + | StorageError::InvalidQuery => PyValueError::new_err(error.to_string()), + StorageError::InsertTooLarge => PyOverflowError::new_err(error.to_string()), + StorageError::InvalidUrl + | StorageError::QueryFailed(_) + | StorageError::InsertFailed(_) + | StorageError::SchemaFailed(_) + | StorageError::ResponseTooLarge + | StorageError::InvalidResponse + | StorageError::Transport => PyRuntimeError::new_err(error.to_string()), + }, } } fn map_sql_error(error: Error) -> PyErr { match error { - Error::QueryFailed(400 | 404) => PyValueError::new_err(error.to_string()), + Error::Storage(litellm_storage_clickhouse::Error::QueryFailed(400 | 404)) => { + PyValueError::new_err(error.to_string()) + } error => map_error(error), } } -fn map_query_access_error(error: QueryAccessError) -> PyErr { - match error { - QueryAccessError::Storage(error) => map_sql_error(error), - QueryAccessError::InvalidScope => PyValueError::new_err(error.to_string()), - error => PyRuntimeError::new_err(error.to_string()), - } -} - #[pyclass(frozen)] pub struct NativeTraceConfig { inner: Config, @@ -105,7 +121,13 @@ impl NativeTraceStorage { crate::execution::run_async( py, async move { - litellm_traces::ensure_schema(&client, &connection, &database, retention_days).await + litellm_traces_clickhouse::ensure_schema( + &client, + &connection, + &database, + retention_days, + ) + .await }, map_error, ) @@ -115,7 +137,7 @@ impl NativeTraceStorage { &self, py: Python<'py>, table: &str, - #[pyo3(from_py_with = insert_rows_from_py)] rows: Vec, + #[pyo3(from_py_with = insert_rows_from_py)] rows: Vec, ) -> PyResult> { let table = InsertTable::parse(table).map_err(map_error)?; let client = crate::http::host_client(py, ClientVariant::NoRedirect)?; @@ -124,8 +146,14 @@ impl NativeTraceStorage { crate::execution::run_async( py, async move { - litellm_traces::insert_shared_rows(&client, &connection, &database, table, rows) - .await + litellm_traces_clickhouse::insert_shared_rows( + &client, + &connection, + &database, + table, + rows, + ) + .await }, map_error, ) @@ -139,7 +167,9 @@ impl NativeTraceStorage { secret: String, ) -> PyResult> { if sql.trim().is_empty() { - return Err(map_error(Error::EmptySql)); + return Err(map_error( + litellm_storage_clickhouse::Error::EmptySql.into(), + )); } let readers = self.query_readers.clone(); let client = crate::http::host_client(py, ClientVariant::NoRedirect)?; @@ -148,11 +178,9 @@ impl NativeTraceStorage { async move { let _permit = readers.acquire()?; let connection = readers.connection(&client, &scope, &secret).await?; - litellm_traces::query_sql(&client, &connection, &sql) - .await - .map_err(QueryAccessError::Storage) + litellm_traces_clickhouse::query_sql(&client, &connection, &sql).await }, - map_query_access_error, + map_sql_error, ) } @@ -169,32 +197,9 @@ impl NativeTraceStorage { async move { let _permit = readers.acquire()?; let connection = readers.connection(&client, &scope, &secret).await?; - litellm_traces::query_help(&client, &connection) - .await - .map_err(QueryAccessError::Storage) + litellm_traces_clickhouse::query_help(&client, &connection).await }, - map_query_access_error, - ) - } - - fn lens_query<'py>( - &self, - py: Python<'py>, - name: &str, - #[pyo3(from_py_with = litellm_host_python::from_py_argument)] parameters: BTreeMap< - String, - Parameter, - >, - ) -> PyResult> { - let query = litellm_traces::LensQuery::parse(name).map_err(map_error)?; - let connection = self.config.storage().reader().clone(); - let client = crate::http::host_client(py, ClientVariant::NoRedirect)?; - crate::execution::run_async( - py, - async move { - litellm_traces::execute_read(&client, &connection, query.sql(), ¶meters).await - }, - map_error, + map_sql_error, ) } @@ -207,13 +212,20 @@ impl NativeTraceStorage { Parameter, >, ) -> PyResult> { - let query = ReadQuery::parse(query).map_err(map_error)?; + let query = + ReadQuery::parse(query).map_err(|error| PyValueError::new_err(error.to_string()))?; let connection = self.config.storage().reader().clone(); let client = crate::http::host_client(py, ClientVariant::NoRedirect)?; crate::execution::run_async( py, async move { - litellm_traces::execute_named_read(&client, &connection, query, ¶meters).await + litellm_traces_clickhouse::execute_named_read( + &client, + &connection, + query, + ¶meters, + ) + .await }, map_error, ) @@ -229,13 +241,15 @@ pub fn trace_decode_otlp<'py>( let spans = py .detach(|| litellm_traces::decode_otlp(body, content_type)) .map_err(|error| match error { - litellm_traces::DecodeError::TooLarge => PyOverflowError::new_err(error.to_string()), + litellm_traces::Error::TooLarge => PyOverflowError::new_err(error.to_string()), _ => PyValueError::new_err(error.to_string()), })?; spans_to_py(py, &spans).map(Bound::into_any) } -fn insert_rows_from_py(value: &Bound<'_, PyAny>) -> PyResult> { +fn insert_rows_from_py( + value: &Bound<'_, PyAny>, +) -> PyResult> { let mut resources = FromPythonCache::default(); value .try_iter()? @@ -320,6 +334,54 @@ mod tests { use super::*; use rstest::rstest; + #[rstest] + #[case::row(Error::InvalidRow, "ValueError")] + #[case::insert_budget(Error::InsertTooLarge, "OverflowError")] + #[case::scope(Error::InvalidScope, "ValueError")] + #[case::schema(Error::SchemaFailed(503), "RuntimeError")] + #[case::reader(Error::MissingSecret, "RuntimeError")] + #[case::storage( + Error::Storage(litellm_storage_clickhouse::Error::InvalidUrl), + "RuntimeError" + )] + #[case::cached_scope(Error::Cached(std::sync::Arc::new(Error::InvalidScope)), "ValueError")] + fn trace_failures_preserve_public_exception_types( + #[case] error: Error, + #[case] exception_name: &str, + ) { + Python::initialize(); + Python::attach(|py| { + let message = error.to_string(); + let exception = map_error(error); + assert_eq!(exception.get_type(py).name().unwrap(), exception_name); + assert_eq!( + exception.value(py).str().unwrap().to_str().unwrap(), + message + ); + }); + } + + #[rstest] + #[case::invalid_sql(400, "ValueError")] + #[case::missing_table(404, "ValueError")] + #[case::unavailable(503, "RuntimeError")] + fn wrapped_query_status_preserves_public_exception_type( + #[case] status: u16, + #[case] exception_name: &str, + ) { + Python::initialize(); + Python::attach(|py| { + let error = Error::Storage(litellm_storage_clickhouse::Error::QueryFailed(status)); + let message = error.to_string(); + let exception = map_sql_error(error); + assert_eq!(exception.get_type(py).name().unwrap(), exception_name); + assert_eq!( + exception.value(py).str().unwrap().to_str().unwrap(), + message + ); + }); + } + #[rstest] fn insert_projection_preserves_identity_without_merging_equal_resources() { Python::initialize(); @@ -365,5 +427,6 @@ mod tests { #[pyfunction] pub fn trace_normalized_field_definitions<'py>(py: Python<'py>) -> PyResult> { - litellm_host_python::Pythonized(litellm_traces::NORMALIZED_FIELD_DEFINITIONS).into_pyobject(py) + litellm_host_python::Pythonized(litellm_traces_clickhouse::NORMALIZED_FIELD_DEFINITIONS) + .into_pyobject(py) } diff --git a/litellm-rust/crates/storage-clickhouse/README.md b/litellm-rust/crates/storage-clickhouse/AGENTS.md similarity index 70% rename from litellm-rust/crates/storage-clickhouse/README.md rename to litellm-rust/crates/storage-clickhouse/AGENTS.md index 676260f6043..959ffdffb88 100644 --- a/litellm-rust/crates/storage-clickhouse/README.md +++ b/litellm-rust/crates/storage-clickhouse/AGENTS.md @@ -2,4 +2,4 @@ `litellm-storage-clickhouse` exports `Storage`, a writer and bounded reader derived from one ClickHouse URL and database. It also exports bounded HTTP read and insert execution -The crate has no trace tables, OTLP types, or named trace queries. `litellm-traces` supplies those rules and uses this storage for both trace rows and spend rows +The crate has no trace tables, OTLP types, or named trace queries. `litellm-traces-clickhouse` supplies those rules and uses this storage for both trace rows and spend rows diff --git a/litellm-rust/crates/storage-clickhouse/Cargo.toml b/litellm-rust/crates/storage-clickhouse/Cargo.toml index f7f85c0dd8d..acce941f2c9 100644 --- a/litellm-rust/crates/storage-clickhouse/Cargo.toml +++ b/litellm-rust/crates/storage-clickhouse/Cargo.toml @@ -18,3 +18,4 @@ url.workspace = true litellm-http = { workspace = true, features = ["test-support"] } rstest.workspace = true tokio.workspace = true +wiremock.workspace = true diff --git a/litellm-rust/crates/storage-clickhouse/src/error.rs b/litellm-rust/crates/storage-clickhouse/src/error.rs index d283fb9021e..43202b8fa15 100644 --- a/litellm-rust/crates/storage-clickhouse/src/error.rs +++ b/litellm-rust/crates/storage-clickhouse/src/error.rs @@ -10,6 +10,8 @@ pub enum Error { InvalidSchema, #[error("SQL query must not be empty")] EmptySql, + #[error("invalid ClickHouse query parameters")] + InvalidParameters, #[error("unknown ClickHouse read query")] InvalidQuery, #[error("ClickHouse query failed with HTTP status {0}")] diff --git a/litellm-rust/crates/storage-clickhouse/src/lib.rs b/litellm-rust/crates/storage-clickhouse/src/lib.rs index 6a6a957d456..7ab2aa9bc0a 100644 --- a/litellm-rust/crates/storage-clickhouse/src/lib.rs +++ b/litellm-rust/crates/storage-clickhouse/src/lib.rs @@ -4,7 +4,7 @@ mod read; pub use error::Error; pub use insert::{insert_compressed_rows, insert_encoded_rows}; -pub use read::{Parameter, execute_read}; +pub use read::{Parameter, Query, READ_LIMITS, ReadLimits, execute_read, fetch, fetch_json}; use url::Url; #[derive(Clone)] diff --git a/litellm-rust/crates/storage-clickhouse/src/read.rs b/litellm-rust/crates/storage-clickhouse/src/read.rs index 99c6a5120f3..59d5b0de558 100644 --- a/litellm-rust/crates/storage-clickhouse/src/read.rs +++ b/litellm-rust/crates/storage-clickhouse/src/read.rs @@ -1,17 +1,30 @@ use std::{collections::BTreeMap, time::Duration}; use litellm_http::Client; -use serde::Deserialize; +use serde::{Deserialize, Serialize, de::DeserializeOwned}; use crate::{Connection, Error}; -const MAX_RESPONSE_BYTES: usize = 4 * 1024 * 1024; +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct ReadLimits { + pub result_rows: u64, + pub response_bytes: usize, + pub execution_seconds: u64, +} -#[derive(Debug, Deserialize)] +pub const READ_LIMITS: ReadLimits = ReadLimits { + result_rows: 1000, + response_bytes: 4 * 1024 * 1024, + execution_seconds: 10, +}; + +#[derive(Debug, Deserialize, Serialize)] #[serde(untagged)] pub enum Parameter { Text(String), Integer(i64), + Unsigned(u64), + Float(f64), Strings(Vec), } @@ -20,6 +33,8 @@ impl Parameter { match self { Self::Text(value) => escaped(value), Self::Integer(value) => value.to_string(), + Self::Unsigned(value) => value.to_string(), + Self::Float(value) => value.to_string(), Self::Strings(values) => format!( "[{}]", values @@ -74,9 +89,12 @@ pub async fn execute_read( .clear() .extend_pairs(existing_pairs) .append_pair("readonly", "1") - .append_pair("max_result_rows", "1000") + .append_pair("max_result_rows", &READ_LIMITS.result_rows.to_string()) .append_pair("result_overflow_mode", "throw") - .append_pair("max_execution_time", "10") + .append_pair( + "max_execution_time", + &READ_LIMITS.execution_seconds.to_string(), + ) .append_pair("wait_end_of_query", "1") .append_pair("default_format", "JSON"); @@ -97,7 +115,7 @@ pub async fn execute_read( let mut body = Vec::new(); while let Some(chunk) = response.chunk().await.map_err(|_| Error::Transport)? { - if body.len() + chunk.len() > MAX_RESPONSE_BYTES { + if body.len() + chunk.len() > READ_LIMITS.response_bytes { return Err(Error::ResponseTooLarge); } body.extend_from_slice(&chunk); @@ -111,3 +129,45 @@ pub async fn execute_read( } String::from_utf8(body).map_err(|_| Error::InvalidResponse) } + +pub trait Query { + type Params: Serialize; + type Row: DeserializeOwned; + + const SQL: &'static str; +} + +#[derive(Deserialize)] +struct Rows { + data: Vec, +} + +fn parameters(params: &T) -> Result, Error> { + let value = serde_json::to_value(params).map_err(|_| Error::InvalidParameters)?; + serde_json::from_value(value).map_err(|_| Error::InvalidParameters) +} + +pub async fn fetch( + client: &Client, + connection: &Connection, + params: &Q::Params, +) -> Result, Error> { + let body = execute_read(client, connection, Q::SQL, ¶meters(params)?).await?; + decode_rows::(&body) +} + +pub async fn fetch_json( + client: &Client, + connection: &Connection, + params: &Q::Params, +) -> Result { + let body = execute_read(client, connection, Q::SQL, ¶meters(params)?).await?; + decode_rows::(&body)?; + Ok(body) +} + +fn decode_rows(body: &str) -> Result, Error> { + serde_json::from_str::>(body) + .map(|rows| rows.data) + .map_err(|_| Error::InvalidResponse) +} diff --git a/litellm-rust/crates/storage-clickhouse/tests/transport.rs b/litellm-rust/crates/storage-clickhouse/tests/transport.rs index 0c7ea233522..f58e52b4940 100644 --- a/litellm-rust/crates/storage-clickhouse/tests/transport.rs +++ b/litellm-rust/crates/storage-clickhouse/tests/transport.rs @@ -1,7 +1,7 @@ use std::collections::BTreeMap; use litellm_http::Client; -use litellm_storage_clickhouse::{Connection, Error, execute_read, insert_encoded_rows}; +use litellm_storage_clickhouse::{Connection, Error, Query, execute_read, insert_encoded_rows}; use rstest::rstest; #[rstest] @@ -32,3 +32,82 @@ async fn read_rejects_empty_sql() { Err(Error::EmptySql) )); } + +#[derive(serde::Serialize)] +struct QueryParams { + signed: i64, + unsigned: u64, + float: f64, + text: String, + strings: Vec, +} + +#[derive(Debug, serde::Deserialize, PartialEq)] +struct QueryRow { + answer: String, +} + +struct TypedQuery; + +impl litellm_storage_clickhouse::Query for TypedQuery { + type Params = QueryParams; + type Row = QueryRow; + const SQL: &'static str = "SELECT typed_parameters"; +} + +#[rstest] +#[case::valid( + r#"{"meta":[],"data":[{"answer":"ok"}],"rows":1,"statistics":{"elapsed":0.1}}"#, + true +)] +#[case::wrong_type(r#"{"data":[{"answer":1}]}"#, false)] +#[case::missing_column(r#"{"data":[{}]}"#, false)] +#[case::exception(r#"{"data":[],"exception":"failed"}"#, false)] +#[tokio::test] +async fn typed_fetch_encodes_parameters_and_validates_rows( + #[case] body: &str, + #[case] valid: bool, +) { + use litellm_storage_clickhouse::{fetch, fetch_json}; + use wiremock::{ + Mock, MockServer, ResponseTemplate, + matchers::{body_string, query_param}, + }; + + let server = MockServer::start().await; + Mock::given(body_string(TypedQuery::SQL)) + .and(query_param("param_signed", i64::MIN.to_string())) + .and(query_param("param_unsigned", u64::MAX.to_string())) + .and(query_param("param_float", "12.5")) + .and(query_param("param_text", "line\\nbreak")) + .and(query_param("param_strings", "['a\\'b','雪']")) + .and(query_param("readonly", "1")) + .and(query_param("max_result_rows", "1000")) + .respond_with(ResponseTemplate::new(200).set_body_string(body)) + .expect(2) + .mount(&server) + .await; + let client = Client::no_redirect_for_test(); + let connection = Connection::parse(&server.uri()).unwrap(); + let params = QueryParams { + signed: i64::MIN, + unsigned: u64::MAX, + float: 12.5, + text: "line\nbreak".into(), + strings: vec!["a'b".into(), "雪".into()], + }; + let rows = fetch::(&client, &connection, ¶ms).await; + let envelope = fetch_json::(&client, &connection, ¶ms).await; + if valid { + assert_eq!( + rows.unwrap(), + vec![QueryRow { + answer: "ok".into() + }] + ); + assert_eq!(envelope.unwrap(), body); + } else { + assert!(matches!(rows, Err(Error::InvalidResponse))); + assert!(matches!(envelope, Err(Error::InvalidResponse))); + } +} diff --git a/litellm-rust/crates/traces-clickhouse/AGENTS.md b/litellm-rust/crates/traces-clickhouse/AGENTS.md new file mode 100644 index 00000000000..ae0c1c8eeb9 --- /dev/null +++ b/litellm-rust/crates/traces-clickhouse/AGENTS.md @@ -0,0 +1,7 @@ +- Own trace schema, row encoding, SQL query adapters and reader provisioning; consume domain types from `litellm-traces` +- Keep generic ClickHouse connections and HTTP execution in `litellm-storage-clickhouse`; keep PyO3 conversion in `python-bridge` +- Keep schema definitions only in `migrations/NNNN_description.sql`, embedded by `litellm_migrate::migrate!` +- Require typed query parameters and SELECT-only readers with server-side limits and tenant isolation +- Bound insert time and encoded bytes; preserve shared values and explicit retry deduplication +- Test storage behavior through the public API against ClickHouse +- Expose one top-level `Error` enum in `src/error.rs`; own trace failures and wrap storage errors with `#[from]` or `#[source]` diff --git a/litellm-rust/crates/traces-clickhouse/Cargo.toml b/litellm-rust/crates/traces-clickhouse/Cargo.toml new file mode 100644 index 00000000000..39edd786201 --- /dev/null +++ b/litellm-rust/crates/traces-clickhouse/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "litellm-traces-clickhouse" +version = "0.1.0" +edition.workspace = true +license.workspace = true +repository.workspace = true + +[dependencies] +askama.workspace = true +flate2.workspace = true +futures-util.workspace = true +hmac = "0.12.1" +litellm-http.workspace = true +litellm-migrate.workspace = true +litellm-storage-clickhouse.workspace = true +litellm-traces.workspace = true +moka.workspace = true +serde.workspace = true +serde_json.workspace = true +sha2.workspace = true +strum.workspace = true +thiserror.workspace = true +time = { workspace = true, features = ["formatting"] } +tokio.workspace = true +url.workspace = true + +[dev-dependencies] +litellm-http = { workspace = true, features = ["test-support"] } +rstest.workspace = true +testcontainers-modules = { version = "0.15.0", features = ["clickhouse"] } +wiremock.workspace = true diff --git a/litellm-rust/crates/traces/build.rs b/litellm-rust/crates/traces-clickhouse/build.rs similarity index 100% rename from litellm-rust/crates/traces/build.rs rename to litellm-rust/crates/traces-clickhouse/build.rs diff --git a/litellm-rust/crates/traces/migrations/0001_otel_traces.sql b/litellm-rust/crates/traces-clickhouse/migrations/0001_otel_traces.sql similarity index 100% rename from litellm-rust/crates/traces/migrations/0001_otel_traces.sql rename to litellm-rust/crates/traces-clickhouse/migrations/0001_otel_traces.sql diff --git a/litellm-rust/crates/traces/migrations/0002_otel_traces_ttl.sql b/litellm-rust/crates/traces-clickhouse/migrations/0002_otel_traces_ttl.sql similarity index 100% rename from litellm-rust/crates/traces/migrations/0002_otel_traces_ttl.sql rename to litellm-rust/crates/traces-clickhouse/migrations/0002_otel_traces_ttl.sql diff --git a/litellm-rust/crates/traces/migrations/0003_agent_traces.sql b/litellm-rust/crates/traces-clickhouse/migrations/0003_agent_traces.sql similarity index 100% rename from litellm-rust/crates/traces/migrations/0003_agent_traces.sql rename to litellm-rust/crates/traces-clickhouse/migrations/0003_agent_traces.sql diff --git a/litellm-rust/crates/traces/migrations/0004_agent_traces_ttl.sql b/litellm-rust/crates/traces-clickhouse/migrations/0004_agent_traces_ttl.sql similarity index 100% rename from litellm-rust/crates/traces/migrations/0004_agent_traces_ttl.sql rename to litellm-rust/crates/traces-clickhouse/migrations/0004_agent_traces_ttl.sql diff --git a/litellm-rust/crates/traces/migrations/0005_agent_traces_mv.sql b/litellm-rust/crates/traces-clickhouse/migrations/0005_agent_traces_mv.sql similarity index 100% rename from litellm-rust/crates/traces/migrations/0005_agent_traces_mv.sql rename to litellm-rust/crates/traces-clickhouse/migrations/0005_agent_traces_mv.sql diff --git a/litellm-rust/crates/traces/migrations/0006_spend_logs.sql b/litellm-rust/crates/traces-clickhouse/migrations/0006_spend_logs.sql similarity index 100% rename from litellm-rust/crates/traces/migrations/0006_spend_logs.sql rename to litellm-rust/crates/traces-clickhouse/migrations/0006_spend_logs.sql diff --git a/litellm-rust/crates/traces/migrations/0007_spend_logs_ttl.sql b/litellm-rust/crates/traces-clickhouse/migrations/0007_spend_logs_ttl.sql similarity index 100% rename from litellm-rust/crates/traces/migrations/0007_spend_logs_ttl.sql rename to litellm-rust/crates/traces-clickhouse/migrations/0007_spend_logs_ttl.sql diff --git a/litellm-rust/crates/traces-clickhouse/migrations/0008_trace_user.sql b/litellm-rust/crates/traces-clickhouse/migrations/0008_trace_user.sql new file mode 100644 index 00000000000..845c93aea21 --- /dev/null +++ b/litellm-rust/crates/traces-clickhouse/migrations/0008_trace_user.sql @@ -0,0 +1,2 @@ +ALTER TABLE {database}.otel_traces + ADD COLUMN IF NOT EXISTS UserId String DEFAULT '' diff --git a/litellm-rust/crates/traces-clickhouse/migrations/0009_trace_rollup_ownership.sql b/litellm-rust/crates/traces-clickhouse/migrations/0009_trace_rollup_ownership.sql new file mode 100644 index 00000000000..fd696349cf5 --- /dev/null +++ b/litellm-rust/crates/traces-clickhouse/migrations/0009_trace_rollup_ownership.sql @@ -0,0 +1,3 @@ +ALTER TABLE {database}.agent_traces_by_key + ADD COLUMN IF NOT EXISTS UserIds SimpleAggregateFunction(groupUniqArrayArray, Array(String)) DEFAULT [], + ADD COLUMN IF NOT EXISTS IdentifiedLlmCount SimpleAggregateFunction(sum, UInt64) DEFAULT 0 diff --git a/litellm-rust/crates/traces-clickhouse/migrations/0010_trace_cost_completeness.sql b/litellm-rust/crates/traces-clickhouse/migrations/0010_trace_cost_completeness.sql new file mode 100644 index 00000000000..af87a6bf40b --- /dev/null +++ b/litellm-rust/crates/traces-clickhouse/migrations/0010_trace_cost_completeness.sql @@ -0,0 +1,22 @@ +ALTER TABLE {database}.agent_traces_by_key_mv MODIFY QUERY +SELECT + TeamId, ApiKeyHash, TraceId, groupUniqArray(UserId) AS UserIds, + min(Timestamp) AS StartTs, + max(Timestamp + toIntervalNanosecond(Duration)) AS EndTs, + any(ServiceName) AS ServiceName, + anyLastIf(toNullable(SpanName), ParentSpanId = '') AS RootName, + anyLastIf(toNullable(InputPreview), ParentSpanId = '') AS RootInput, + anyLastIf(toNullable(StatusCode), ParentSpanId = '') AS RootStatus, + count() AS SpanCount, + countIf(ObservationType = 'agent') AS AgentCount, + countIf(ObservationType = 'llm') AS LlmCount, + countIf(ObservationType = 'llm' AND LiteLLMRequestId != '') AS IdentifiedLlmCount, + countIf(ObservationType = 'tool') AS ToolCount, + countIf(StatusCode = 'STATUS_CODE_ERROR') AS ErrorCount, + sum(InputTokens) AS InputTokens, + sum(OutputTokens) AS OutputTokens, + groupUniqArrayIf(toString(Model), Model != '') AS Models, + groupUniqArrayIf(SpanName, ObservationType = 'agent') AS AgentNames, + groupArrayIf(LiteLLMRequestId, ObservationType = 'llm' OR LiteLLMRequestId != '') AS RequestIds +FROM {database}.otel_traces +GROUP BY TeamId, ApiKeyHash, TraceId diff --git a/litellm-rust/crates/traces-clickhouse/migrations/0011_otel_traces_framework.sql b/litellm-rust/crates/traces-clickhouse/migrations/0011_otel_traces_framework.sql new file mode 100644 index 00000000000..1d6c2c83769 --- /dev/null +++ b/litellm-rust/crates/traces-clickhouse/migrations/0011_otel_traces_framework.sql @@ -0,0 +1 @@ +ALTER TABLE {database}.otel_traces ADD COLUMN IF NOT EXISTS Framework LowCardinality(String) AFTER AgentName diff --git a/litellm-rust/crates/traces/query/lens_agents.sql b/litellm-rust/crates/traces-clickhouse/query/lens_agents.sql similarity index 100% rename from litellm-rust/crates/traces/query/lens_agents.sql rename to litellm-rust/crates/traces-clickhouse/query/lens_agents.sql diff --git a/litellm-rust/crates/traces/query/lens_availability.sql b/litellm-rust/crates/traces-clickhouse/query/lens_availability.sql similarity index 100% rename from litellm-rust/crates/traces/query/lens_availability.sql rename to litellm-rust/crates/traces-clickhouse/query/lens_availability.sql diff --git a/litellm-rust/crates/traces/query/lens_content.sql b/litellm-rust/crates/traces-clickhouse/query/lens_content.sql similarity index 100% rename from litellm-rust/crates/traces/query/lens_content.sql rename to litellm-rust/crates/traces-clickhouse/query/lens_content.sql diff --git a/litellm-rust/crates/traces/query/lens_evidence.sql b/litellm-rust/crates/traces-clickhouse/query/lens_evidence.sql similarity index 100% rename from litellm-rust/crates/traces/query/lens_evidence.sql rename to litellm-rust/crates/traces-clickhouse/query/lens_evidence.sql diff --git a/litellm-rust/crates/traces/query/lens_sample.sql b/litellm-rust/crates/traces-clickhouse/query/lens_sample.sql similarity index 100% rename from litellm-rust/crates/traces/query/lens_sample.sql rename to litellm-rust/crates/traces-clickhouse/query/lens_sample.sql diff --git a/litellm-rust/crates/traces-clickhouse/query/list_traces.sql b/litellm-rust/crates/traces-clickhouse/query/list_traces.sql new file mode 100644 index 00000000000..c52adf7ef49 --- /dev/null +++ b/litellm-rust/crates/traces-clickhouse/query/list_traces.sql @@ -0,0 +1,48 @@ +WITH page AS ( +SELECT TraceId AS trace_id, + hex(SHA256(concat(TeamId, char(0), ApiKeyHash, char(0), TraceId))) AS trace_ref, + if(length(groupUniqArrayArray(UserIds)) = 1, arrayElement(groupUniqArrayArray(UserIds), 1), '') AS user_id, TeamId AS team_id, ApiKeyHash AS api_key_hash, + ifNull(any(RootName), '') AS name, any(ServiceName) AS service, + ifNull(any(RootInput), '') AS input_preview, ifNull(any(RootStatus), '') AS status, + toUnixTimestamp64Milli(min(StartTs)) AS start_ms, + min(StartTs) AS trace_start, max(EndTs) AS trace_end, + dateDiff('millisecond', min(StartTs), max(EndTs)) AS duration_ms, + sum(SpanCount) AS span_count, + sum(AgentCount) AS agent_invocations, + sum(LlmCount) AS llm_calls, sum(ToolCount) AS tool_calls, + sum(InputTokens) AS input_tokens, sum(OutputTokens) AS output_tokens, + groupUniqArrayArray(Models) AS models, sum(ErrorCount) AS error_count, + arrayDistinct(if(sum(IdentifiedLlmCount) != sum(LlmCount), + arrayConcat(groupArrayArray(RequestIds), ['']), + groupArrayArray(RequestIds))) AS request_ids +FROM agent_traces_by_key +WHERE ({all_teams:UInt8} = 1 + OR ({user_id:String} != '' AND UserIds = [{user_id:String}]) + OR has({team_ids:Array(String)}, TeamId)) +GROUP BY TeamId, ApiKeyHash, TraceId +HAVING min(StartTs) >= fromUnixTimestamp64Milli({start_ms:Int64}) + AND min(StartTs) < fromUnixTimestamp64Milli({end_ms:Int64}) + AND ({cursor_ms:Int64} = 0 OR (toUnixTimestamp64Milli(min(StartTs)), trace_ref) + < ({cursor_ms:Int64}, {cursor_trace_id:String})) +ORDER BY start_ms DESC, trace_ref DESC +LIMIT {limit:UInt32} +) +SELECT page.* EXCEPT (trace_start, trace_end), + identities.agent_names AS agent_names, identities.agent_count AS agent_count, + identities.frameworks AS frameworks +FROM page +LEFT JOIN ( + SELECT TeamId, ApiKeyHash, TraceId, + arraySort(groupUniqArrayIf(AgentName, AgentName != '')) AS agent_names, + arraySort(groupUniqArrayIf(toString(Framework), Framework != '')) AS frameworks, + uniqExactIf(if(AgentName = '', SpanName, AgentName), ObservationType = 'agent') AS agent_count + FROM otel_traces + WHERE Timestamp >= (SELECT min(trace_start) FROM page) + AND Timestamp <= (SELECT max(trace_end) FROM page) + AND TraceId IN (SELECT trace_id FROM page) + AND (TeamId, ApiKeyHash, TraceId) IN (SELECT team_id, api_key_hash, trace_id FROM page) + GROUP BY TeamId, ApiKeyHash, TraceId +) AS identities +ON page.team_id = identities.TeamId AND page.api_key_hash = identities.ApiKeyHash + AND page.trace_id = identities.TraceId +ORDER BY page.start_ms DESC, page.trace_ref DESC diff --git a/litellm-rust/crates/traces-clickhouse/query/span_detail.sql b/litellm-rust/crates/traces-clickhouse/query/span_detail.sql new file mode 100644 index 00000000000..7db742ea3ee --- /dev/null +++ b/litellm-rust/crates/traces-clickhouse/query/span_detail.sql @@ -0,0 +1,24 @@ +SELECT o.SpanId AS span_id, o.Input AS input, + if(o.Output = '' AND o.ObservationType = 'agent', answer.output, o.Output) AS output, + o.SpanAttributes AS attributes +FROM otel_traces AS o +LEFT JOIN ( + SELECT TeamId, ApiKeyHash, ParentSpanId AS parent_span_id, argMax(Output, Timestamp) AS output + FROM otel_traces + WHERE TraceId = {trace_id:String} AND ParentSpanId = {span_id:String} + AND ObservationType = 'llm' AND Output != '' + AND ({all_teams:UInt8} = 1 + OR ({user_id:String} != '' AND UserId = {user_id:String}) + OR has({team_ids:Array(String)}, TeamId)) + AND ({trace_ref:String} = '' OR + hex(SHA256(concat(TeamId, char(0), ApiKeyHash, char(0), TraceId))) = {trace_ref:String}) + GROUP BY TeamId, ApiKeyHash, ParentSpanId +) AS answer ON answer.parent_span_id = o.SpanId + AND answer.TeamId = o.TeamId AND answer.ApiKeyHash = o.ApiKeyHash +WHERE o.TraceId = {trace_id:String} AND o.SpanId = {span_id:String} + AND ({all_teams:UInt8} = 1 + OR ({user_id:String} != '' AND o.UserId = {user_id:String}) + OR has({team_ids:Array(String)}, o.TeamId)) + AND ({trace_ref:String} = '' OR + hex(SHA256(concat(o.TeamId, char(0), o.ApiKeyHash, char(0), o.TraceId))) = {trace_ref:String}) +LIMIT 1 diff --git a/litellm-rust/crates/traces/query/span_error.sql b/litellm-rust/crates/traces-clickhouse/query/span_error.sql similarity index 78% rename from litellm-rust/crates/traces/query/span_error.sql rename to litellm-rust/crates/traces-clickhouse/query/span_error.sql index b4710006389..e1226c4d23c 100644 --- a/litellm-rust/crates/traces/query/span_error.sql +++ b/litellm-rust/crates/traces-clickhouse/query/span_error.sql @@ -4,8 +4,9 @@ SELECT SpanId AS span_id, hex(SHA256(StatusMessage)) AS version FROM otel_traces WHERE TraceId = {trace_id:String} AND SpanId = {span_id:String} - AND (empty({team_ids:Array(String)}) OR TeamId IN {team_ids:Array(String)}) - AND ({api_key_hash:String} = '' OR ApiKeyHash = {api_key_hash:String}) + AND ({all_teams:UInt8} = 1 + OR ({user_id:String} != '' AND UserId = {user_id:String}) + OR has({team_ids:Array(String)}, TeamId)) AND ({trace_ref:String} = '' OR hex(SHA256(concat(TeamId, char(0), ApiKeyHash, char(0), TraceId))) = {trace_ref:String}) AND ({error_version:String} = '' OR hex(SHA256(StatusMessage)) = {error_version:String}) diff --git a/litellm-rust/crates/traces/query/spend_by_response_ids.sql b/litellm-rust/crates/traces-clickhouse/query/spend_by_response_ids.sql similarity index 57% rename from litellm-rust/crates/traces/query/spend_by_response_ids.sql rename to litellm-rust/crates/traces-clickhouse/query/spend_by_response_ids.sql index 285e9235629..963c832232b 100644 --- a/litellm-rust/crates/traces/query/spend_by_response_ids.sql +++ b/litellm-rust/crates/traces-clickhouse/query/spend_by_response_ids.sql @@ -1,9 +1,10 @@ -SELECT request_id, response_id, team_id, api_key, spend, +SELECT request_id, response_id, team_id, api_key, user, spend, toUnixTimestamp64Milli(start_time) AS start_ms FROM spend_logs FINAL WHERE response_id IN {response_ids:Array(String)} AND start_time >= fromUnixTimestamp64Milli({start_ms:Int64}) AND start_time < fromUnixTimestamp64Milli({end_ms:Int64}) - AND (empty({team_ids:Array(String)}) OR team_id IN {team_ids:Array(String)}) - AND ({api_key_hash:String} = '' OR api_key = {api_key_hash:String}) + AND ({all_teams:UInt8} = 1 + OR ({user_id:String} != '' AND user = {user_id:String}) + OR has({team_ids:Array(String)}, team_id)) ORDER BY start_time DESC diff --git a/litellm-rust/crates/traces-clickhouse/query/trace_identity.sql b/litellm-rust/crates/traces-clickhouse/query/trace_identity.sql new file mode 100644 index 00000000000..e3881b150b7 --- /dev/null +++ b/litellm-rust/crates/traces-clickhouse/query/trace_identity.sql @@ -0,0 +1,8 @@ +SELECT hex(SHA256(concat(TeamId, char(0), ApiKeyHash, char(0), TraceId))) AS trace_ref +FROM otel_traces +WHERE TraceId = {trace_id:String} + AND ({all_teams:UInt8} = 1 + OR ({user_id:String} != '' AND UserId = {user_id:String}) + OR has({team_ids:Array(String)}, TeamId)) +GROUP BY TeamId, ApiKeyHash, TraceId +LIMIT 2 diff --git a/litellm-rust/crates/traces/query/trace_spans.sql b/litellm-rust/crates/traces-clickhouse/query/trace_spans.sql similarity index 69% rename from litellm-rust/crates/traces/query/trace_spans.sql rename to litellm-rust/crates/traces-clickhouse/query/trace_spans.sql index dab3ac2e877..f00329d8136 100644 --- a/litellm-rust/crates/traces/query/trace_spans.sql +++ b/litellm-rust/crates/traces-clickhouse/query/trace_spans.sql @@ -1,16 +1,18 @@ SELECT o.SpanId AS span_id, o.ParentSpanId AS parent_span_id, o.SpanName AS name, - o.ObservationType AS type, o.AgentName AS agent, o.StatusCode AS status, + o.ObservationType AS type, o.AgentName AS agent, + o.Framework AS framework, o.StatusCode AS status, substringUTF8(o.StatusMessage, 1, 128) AS status_message, lengthUTF8(o.StatusMessage) > 128 AS error_truncated, toUnixTimestamp64Nano(o.Timestamp) AS start_ns, o.Duration AS duration_ns, o.ServiceName AS service, o.InputPreview AS input_preview, o.Model AS model, o.InputTokens AS input_tokens, o.OutputTokens AS output_tokens, o.LiteLLMRequestId AS litellm_request_id, - o.TeamId AS team_id, o.ApiKeyHash AS api_key_hash + o.UserId AS user_id, o.TeamId AS team_id, o.ApiKeyHash AS api_key_hash FROM otel_traces AS o WHERE o.TraceId = {trace_id:String} - AND (empty({team_ids:Array(String)}) OR o.TeamId IN {team_ids:Array(String)}) - AND ({api_key_hash:String} = '' OR o.ApiKeyHash = {api_key_hash:String}) + AND ({all_teams:UInt8} = 1 + OR ({user_id:String} != '' AND o.UserId = {user_id:String}) + OR has({team_ids:Array(String)}, o.TeamId)) AND ({trace_ref:String} = '' OR hex(SHA256(concat(o.TeamId, char(0), o.ApiKeyHash, char(0), o.TraceId))) = {trace_ref:String}) ORDER BY o.Timestamp, o.EngineReceivedMs, o.StatusMessage diff --git a/litellm-rust/crates/traces/src/config.rs b/litellm-rust/crates/traces-clickhouse/src/config.rs similarity index 78% rename from litellm-rust/crates/traces/src/config.rs rename to litellm-rust/crates/traces-clickhouse/src/config.rs index ec88fca2e3f..362a0a88dc7 100644 --- a/litellm-rust/crates/traces/src/config.rs +++ b/litellm-rust/crates/traces-clickhouse/src/config.rs @@ -1,4 +1,5 @@ -use litellm_storage_clickhouse::{Error, Storage}; +use crate::Error; +use litellm_storage_clickhouse::Storage; #[derive(Clone)] pub struct Config { @@ -8,7 +9,7 @@ pub struct Config { impl Config { pub fn new(database: String, url: &str, retention_days: u32) -> Result { - crate::schema_statements(&database, retention_days)?; + super::schema_statements(&database, retention_days)?; Ok(Self { storage: Storage::new(database, url)?, retention_days, diff --git a/litellm-rust/crates/traces-clickhouse/src/error.rs b/litellm-rust/crates/traces-clickhouse/src/error.rs new file mode 100644 index 00000000000..1cdbedb5ff9 --- /dev/null +++ b/litellm-rust/crates/traces-clickhouse/src/error.rs @@ -0,0 +1,37 @@ +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("invalid ClickHouse insert row")] + InvalidRow, + #[error("invalid ClickHouse insert table")] + InvalidTable, + #[error("database must be a nonempty SQL identifier and retention must be positive")] + InvalidSchema, + #[error("unknown ClickHouse read query")] + InvalidQuery, + #[error("invalid ClickHouse query parameters")] + InvalidParameters, + #[error("ClickHouse returned an invalid or failed JSON query response")] + InvalidResponse, + #[error("ClickHouse insert exceeds the encoded size limit")] + InsertTooLarge, + #[error("ClickHouse schema setup failed with HTTP status {0}")] + SchemaFailed(u16), + #[error("ClickHouse schema setup transport failed")] + SchemaTransport, + #[error("trace SQL queries require a configured proxy master key")] + MissingSecret, + #[error("invalid trace query scope")] + InvalidScope, + #[error("trace SQL query concurrency limit exceeded")] + Busy, + #[error( + "ClickHouse reader provisioning failed with HTTP status {0}; the configured connection must be allowed to manage users, row policies, and SELECT grants on the trace tables" + )] + ProvisionFailed(u16), + #[error("ClickHouse reader provisioning transport failed")] + ProvisionTransport, + #[error(transparent)] + Storage(#[from] litellm_storage_clickhouse::Error), + #[error(transparent)] + Cached(#[from] std::sync::Arc), +} diff --git a/litellm-rust/crates/traces/src/insert.rs b/litellm-rust/crates/traces-clickhouse/src/insert.rs similarity index 98% rename from litellm-rust/crates/traces/src/insert.rs rename to litellm-rust/crates/traces-clickhouse/src/insert.rs index 01a1eecfd7b..ac9fb474ed0 100644 --- a/litellm-rust/crates/traces/src/insert.rs +++ b/litellm-rust/crates/traces-clickhouse/src/insert.rs @@ -12,7 +12,8 @@ use serde_json::Value; use sha2::{Digest, Sha256}; use time::{OffsetDateTime, format_description::well_known::Rfc3339}; -use crate::{Connection, Error, Shared}; +use super::{Connection, Error}; +use litellm_traces::Shared; const MAX_INSERT_BYTES: usize = 64 * 1024 * 1024; @@ -71,6 +72,7 @@ pub async fn insert_shared_rows( body, ) .await + .map_err(Error::from) } fn shared_rows(rows: Vec>) -> Vec { @@ -226,8 +228,8 @@ mod tests { use rstest::rstest; use serde_json::json; + use super::Error; use super::{shared_rows, write_rows}; - use crate::Error; #[rstest] fn encoded_limit_counts_utf8_bytes_across_rows() { diff --git a/litellm-rust/crates/traces-clickhouse/src/lib.rs b/litellm-rust/crates/traces-clickhouse/src/lib.rs new file mode 100644 index 00000000000..ba2f776b51a --- /dev/null +++ b/litellm-rust/crates/traces-clickhouse/src/lib.rs @@ -0,0 +1,21 @@ +mod config; +mod error; +mod insert; +pub mod query; +mod query_access; +mod schema; +mod sql; +mod table; + +pub use config::Config; +pub use error::Error; +pub use insert::{InsertRow, InsertTable, encode_rows, insert_rows, insert_shared_rows}; +pub use litellm_storage_clickhouse::{Connection, Parameter}; +pub use litellm_traces::{QueryScope, ReadQuery}; +pub use query::{QueryHelp, execute_read, query_help, query_sql}; +pub use query_access::QueryReaders; +pub use schema::{ + NORMALIZED_FIELD_DEFINITIONS, NormalizedFieldDefinition, ensure_schema, schema_statements, +}; +pub use sql::execute_named_read; +pub use table::TraceTable; diff --git a/litellm-rust/crates/traces-clickhouse/src/query.rs b/litellm-rust/crates/traces-clickhouse/src/query.rs new file mode 100644 index 00000000000..9f9c5463b7e --- /dev/null +++ b/litellm-rust/crates/traces-clickhouse/src/query.rs @@ -0,0 +1,494 @@ +use std::collections::{BTreeMap, BTreeSet}; + +use crate::TraceTable; +use futures_util::{ + StreamExt, + stream::{self, TryStreamExt}, +}; +use litellm_http::Client; +use serde::{Deserialize, Serialize, Serializer}; +use serde_json::Value; +use strum::IntoEnumIterator; + +use super::{ + Connection, Error, NORMALIZED_FIELD_DEFINITIONS, NormalizedFieldDefinition, Parameter, + query_access::READER_LIMITS, +}; + +mod guide; +pub mod lens; +pub mod named; +mod number; + +const SAMPLE_ROWS: usize = 200; +const MAX_FIELDS: usize = 200; +const MAX_DEPTH: usize = 16; +const METADATA_SQL: &str = "SELECT metadata FROM spend_logs FINAL \ + WHERE start_time >= now() - INTERVAL 7 DAY AND length(metadata) <= 8192 \ + LIMIT 201"; +const METADATA_SCOPE: &str = "Up to 200 unordered rows from the last 7 days, excluding metadata larger than 8192 bytes; up to 200 paths and 16 levels. Missing paths may exist outside this sample. Array indexes are 1-based and describe sampled positions, not a fixed schema"; +const ATTRIBUTE_SCOPE: &str = "Distinct keys from up to 200 unordered spans in the last 7 days; up to 200 keys per map. Missing keys may exist outside this sample"; + +#[derive(Deserialize)] +struct Rows { + data: Vec, +} + +#[derive(Deserialize)] +struct MetadataRow { + metadata: String, +} + +#[derive(Deserialize)] +struct AttributeRow { + key: String, +} + +#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)] +#[serde(untagged)] +enum PathPart { + Key(String), + Index(usize), +} + +#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize, strum::Display)] +#[serde(rename_all = "lowercase")] +#[strum(serialize_all = "lowercase")] +enum JsonKind { + Array, + Boolean, + Integer, + Null, + Number, + Object, + String, +} + +impl JsonKind { + fn of(value: &Value) -> Self { + match value { + Value::Null => Self::Null, + Value::Bool(_) => Self::Boolean, + Value::Number(number) if number.is_i64() || number.is_u64() => Self::Integer, + Value::Number(_) => Self::Number, + Value::String(_) => Self::String, + Value::Array(_) => Self::Array, + Value::Object(_) => Self::Object, + } + } +} + +#[derive(Clone, Copy, Debug, Serialize, strum::Display)] +enum MapValueType { + String, +} + +#[derive(Serialize)] +struct MetadataField { + path: Vec, + types: BTreeSet, + expression: String, +} + +#[derive(Deserialize, Serialize)] +struct ColumnSchema { + name: String, + #[serde(rename = "type")] + kind: String, + #[serde(flatten)] + details: BTreeMap, +} + +#[derive(Serialize)] +struct TableSchema { + name: TraceTable, + columns: Vec, +} + +trait Unobserved { + fn unobserved() -> Self; +} + +enum Discovery { + Observed(T), + Unavailable(String), +} + +impl Serialize for Discovery { + fn serialize(&self, serializer: S) -> Result { + #[derive(Serialize)] + struct Unavailable<'a, T> { + #[serde(flatten)] + sample: T, + error: &'a str, + } + match self { + Self::Observed(sample) => sample.serialize(serializer), + Self::Unavailable(error) => Unavailable { + sample: T::unobserved(), + error, + } + .serialize(serializer), + } + } +} + +#[derive(Serialize)] +struct MetadataSample { + fields: Vec, + sampled_rows: usize, + invalid_json_rows: usize, + truncated: bool, +} + +impl Unobserved for MetadataSample { + fn unobserved() -> Self { + Self { + fields: Vec::new(), + sampled_rows: 0, + invalid_json_rows: 0, + truncated: true, + } + } +} + +#[derive(Serialize)] +struct MetadataCatalog { + table: TraceTable, + column: &'static str, + #[serde(flatten)] + discovery: Discovery, + sample_sql: &'static str, + scope: &'static str, +} + +#[derive(Serialize)] +struct AttributeField { + key: String, + #[serde(rename = "type")] + kind: MapValueType, + expression: String, +} + +#[derive(Serialize)] +struct AttributeSample { + fields: Vec, + truncated: bool, +} + +impl Unobserved for AttributeSample { + fn unobserved() -> Self { + Self { + fields: Vec::new(), + truncated: true, + } + } +} + +#[derive(Serialize)] +struct AttributeCatalog { + table: TraceTable, + column: &'static str, + #[serde(flatten)] + discovery: Discovery, + discovery_sql: String, + scope: &'static str, +} + +#[derive(Serialize)] +struct NormalizedField { + table: TraceTable, + name: &'static str, + column: &'static str, + #[serde(rename = "type")] + kind: &'static str, + meaning: &'static str, +} + +impl From<&NormalizedFieldDefinition> for NormalizedField { + fn from(field: &NormalizedFieldDefinition) -> Self { + Self { + table: TraceTable::OtelTraces, + name: field.name, + column: field.clickhouse_column, + kind: field.clickhouse_type, + meaning: field.meaning, + } + } +} + +#[derive(Serialize)] +struct Relationship { + left: &'static str, + right: &'static str, + additional_predicates: &'static str, + meaning: &'static str, +} + +const RELATIONSHIPS: [Relationship; 1] = [Relationship { + left: "otel_traces.LiteLLMRequestId", + right: "spend_logs.response_id", + additional_predicates: "otel_traces.TeamId = spend_logs.team_id AND (otel_traces.TeamId != '' OR (otel_traces.UserId != '' AND otel_traces.UserId = spend_logs.user) OR (otel_traces.ApiKeyHash != '' AND otel_traces.ApiKeyHash = spend_logs.api_key))", + meaning: "The normalized ID is the response ID, not request_id. Cached requests can share response_id; joins may return multiple spend rows", +}]; + +#[derive(Serialize)] +pub struct QueryHelp { + dialect: &'static str, + access: &'static str, + response: &'static str, + tables: Vec, + normalized_fields: Vec, + metadata: MetadataCatalog, + attributes: Vec, + relationships: &'static [Relationship], + examples: [guide::Example; 5], + gotchas: [String; 11], + guide: String, +} + +pub async fn execute_read( + client: &Client, + connection: &Connection, + sql: &str, + parameters: &BTreeMap, +) -> Result { + litellm_storage_clickhouse::execute_read(client, connection, sql, parameters) + .await + .map_err(Error::from) +} + +pub async fn query_sql( + client: &Client, + connection: &Connection, + sql: &str, +) -> Result { + execute_read(client, connection, sql, &BTreeMap::new()).await +} + +async fn rows( + client: &Client, + connection: &Connection, + sql: &str, +) -> Result, Error> { + let body = query_sql(client, connection, sql).await?; + serde_json::from_str::>(&body) + .map(|result| result.data) + .map_err(|_| Error::InvalidResponse) +} + +fn literal(value: &str) -> String { + format!("'{}'", value.replace('\\', "\\\\").replace('\'', "\\'")) +} + +fn metadata_expression(path: &[PathPart]) -> String { + let arguments = path + .iter() + .map(|part| match part { + PathPart::Key(key) => literal(key), + PathPart::Index(index) => index.to_string(), + }) + .collect::>() + .join(", "); + format!("JSONExtractRaw(metadata, {arguments})") +} + +fn discover( + value: &Value, + path: Vec, + fields: &mut BTreeMap, BTreeSet>, +) -> bool { + if path.len() > MAX_DEPTH || (fields.len() >= MAX_FIELDS && !fields.contains_key(&path)) { + return true; + } + if !path.is_empty() { + fields + .entry(path.clone()) + .or_default() + .insert(JsonKind::of(value)); + } + match value { + Value::Object(object) => object.iter().fold(false, |limited, (key, value)| { + let child = path + .iter() + .cloned() + .chain([PathPart::Key(key.clone())]) + .collect(); + discover(value, child, fields) | limited + }), + Value::Array(array) => array + .iter() + .enumerate() + .fold(false, |limited, (index, value)| { + let child = path + .iter() + .cloned() + .chain([PathPart::Index(index + 1)]) + .collect(); + discover(value, child, fields) | limited + }), + _ => false, + } +} + +fn metadata_sample(sample: &[MetadataRow]) -> MetadataSample { + let (fields, limited, invalid_rows) = sample.iter().take(SAMPLE_ROWS).fold( + (BTreeMap::new(), sample.len() > SAMPLE_ROWS, 0), + |(fields, limited, invalid_rows), row| match serde_json::from_str::(&row.metadata) { + Ok(value) => { + let mut fields = fields; + let limited = limited | discover(&value, Vec::new(), &mut fields); + (fields, limited, invalid_rows) + } + Err(_) => (fields, limited, invalid_rows + 1), + }, + ); + let fields: Vec<_> = fields + .into_iter() + .map(|(path, types)| MetadataField { + expression: metadata_expression(&path), + path, + types, + }) + .collect(); + MetadataSample { + fields, + sampled_rows: sample.len().min(SAMPLE_ROWS), + invalid_json_rows: invalid_rows, + truncated: limited, + } +} + +pub async fn query_help(client: &Client, connection: &Connection) -> Result { + let tables = stream::iter(TraceTable::iter()) + .then(|table| async move { + Ok::<_, Error>(TableSchema { + name: table, + columns: rows::( + client, + connection, + &format!("DESCRIBE TABLE {table}"), + ) + .await?, + }) + }) + .try_collect::>() + .await?; + let metadata = MetadataCatalog { + table: TraceTable::SpendLogs, + column: "metadata", + discovery: match rows::(client, connection, METADATA_SQL).await { + Ok(sample) => Discovery::Observed(metadata_sample(&sample)), + Err(error) => Discovery::Unavailable(error.to_string()), + }, + sample_sql: METADATA_SQL, + scope: METADATA_SCOPE, + }; + let attributes = stream::iter(["SpanAttributes", "ResourceAttributes"]) + .then(|column| async move { + let sql = format!( + "SELECT DISTINCT arrayJoin(mapKeys({column})) AS key FROM \ + (SELECT {column} FROM otel_traces WHERE Timestamp >= now() - INTERVAL 7 DAY \ + LIMIT 200) ORDER BY key LIMIT 201" + ); + let discovery = match rows::(client, connection, &sql).await { + Ok(keys) => Discovery::Observed(AttributeSample { + truncated: keys.len() > MAX_FIELDS, + fields: keys + .into_iter() + .take(MAX_FIELDS) + .map(|row| AttributeField { + expression: format!("{column}[{}]", literal(&row.key)), + key: row.key, + kind: MapValueType::String, + }) + .collect(), + }), + Err(error) => Discovery::Unavailable(error.to_string()), + }; + AttributeCatalog { + table: TraceTable::OtelTraces, + column, + discovery, + discovery_sql: sql, + scope: ATTRIBUTE_SCOPE, + } + }) + .collect::>() + .await; + let guide = guide::QueryGuide { + tables: &tables, + normalized_fields: &NORMALIZED_FIELD_DEFINITIONS, + metadata: &metadata, + attributes: &attributes, + limits: &READER_LIMITS, + }; + Ok(QueryHelp { + dialect: "ClickHouse SQL", + access: "Request-log visibility enforced by ClickHouse row policies; proxy admins see all rows, users see their own rows and permitted teams", + response: "ClickHouse JSON envelope: meta, data, rows, statistics; 64-bit integers may be strings", + examples: guide.examples()?, + gotchas: guide.gotchas()?, + guide: guide::render(&guide)?, + normalized_fields: NORMALIZED_FIELD_DEFINITIONS + .iter() + .map(NormalizedField::from) + .collect(), + relationships: &RELATIONSHIPS, + tables, + metadata, + attributes, + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use rstest::rstest; + use serde_json::json; + + #[rstest] + fn metadata_discovery_preserves_mixed_types_and_reports_invalid_rows() { + let sample = [ + MetadataRow { + metadata: r#"{"x": 1}"#.into(), + }, + MetadataRow { + metadata: r#"{"x": "one"}"#.into(), + }, + MetadataRow { + metadata: "invalid".into(), + }, + ]; + let catalog = json!(metadata_sample(&sample)); + assert_eq!( + catalog["fields"], + json!([{ + "path": ["x"], "types": ["integer", "string"], "expression": "JSONExtractRaw(metadata, 'x')" + }]) + ); + assert_eq!(catalog["invalid_json_rows"], 1); + assert_eq!(catalog["sampled_rows"], sample.len()); + } + + #[rstest] + #[case::rows(SAMPLE_ROWS + 1, 1)] + #[case::paths(1, MAX_FIELDS + 1)] + fn metadata_discovery_reports_truncation(#[case] row_count: usize, #[case] field_count: usize) { + let metadata: BTreeMap<_, _> = (0..field_count) + .map(|index| (format!("field{index}"), index)) + .collect(); + let sample: Vec<_> = (0..row_count) + .map(|_| MetadataRow { + metadata: json!(metadata).to_string(), + }) + .collect(); + let catalog = json!(metadata_sample(&sample)); + assert_eq!(catalog["truncated"], true); + assert_eq!(catalog["sampled_rows"], row_count.min(SAMPLE_ROWS)); + assert_eq!( + catalog["fields"].as_array().unwrap().len(), + field_count.min(MAX_FIELDS) + ); + } +} diff --git a/litellm-rust/crates/traces/src/query/guide.rs b/litellm-rust/crates/traces-clickhouse/src/query/guide.rs similarity index 93% rename from litellm-rust/crates/traces/src/query/guide.rs rename to litellm-rust/crates/traces-clickhouse/src/query/guide.rs index 3bf7336648d..a0b60f9666f 100644 --- a/litellm-rust/crates/traces/src/query/guide.rs +++ b/litellm-rust/crates/traces-clickhouse/src/query/guide.rs @@ -1,8 +1,8 @@ use askama::Template; use serde::Serialize; -use super::{AttributeCatalog, MetadataCatalog, TableSchema}; -use crate::{Error, NormalizedFieldDefinition}; +use super::{AttributeCatalog, Discovery, MetadataCatalog, TableSchema}; +use crate::{Error, NormalizedFieldDefinition, query_access::ReaderLimits}; #[derive(Template)] #[template(path = "query_help.jinja", escape = "none", blocks = [ @@ -33,6 +33,7 @@ pub(super) struct QueryGuide<'a> { pub normalized_fields: &'a [NormalizedFieldDefinition], pub metadata: &'a MetadataCatalog, pub attributes: &'a [AttributeCatalog], + pub limits: &'a ReaderLimits, } #[derive(Serialize)] diff --git a/litellm-rust/crates/traces-clickhouse/src/query/lens.rs b/litellm-rust/crates/traces-clickhouse/src/query/lens.rs new file mode 100644 index 00000000000..56242cc3c62 --- /dev/null +++ b/litellm-rust/crates/traces-clickhouse/src/query/lens.rs @@ -0,0 +1,173 @@ +use litellm_storage_clickhouse::Query; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Deserialize, Serialize)] +pub struct LensAccessParams { + #[serde(deserialize_with = "super::number::deserialize")] + pub all_teams: u8, + pub team: String, + pub key_hash: String, +} + +pub struct LensAvailability; + +#[derive(Debug, Deserialize, Serialize)] +pub struct LensAvailabilityParams { + #[serde(flatten)] + pub access: LensAccessParams, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct LensAvailabilityRow { + #[serde(deserialize_with = "super::number::deserialize")] + pub traces: u8, + #[serde(deserialize_with = "super::number::deserialize")] + pub requests: u8, +} + +impl Query for LensAvailability { + type Params = LensAvailabilityParams; + type Row = LensAvailabilityRow; + + const SQL: &'static str = include_str!("../../query/lens_availability.sql"); +} + +pub struct LensAgents; + +#[derive(Debug, Deserialize, Serialize)] +pub struct LensAgentsParams { + #[serde(flatten)] + pub access: LensAccessParams, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct LensAgentsRow { + pub agent_name: String, +} + +impl Query for LensAgents { + type Params = LensAgentsParams; + type Row = LensAgentsRow; + + const SQL: &'static str = include_str!("../../query/lens_agents.sql"); +} + +pub struct LensSample; + +#[derive(Debug, Deserialize, Serialize)] +pub struct LensSampleParams { + #[serde(flatten)] + pub access: LensAccessParams, + pub source: String, + #[serde(deserialize_with = "super::number::deserialize")] + pub start: u64, + #[serde(deserialize_with = "super::number::deserialize")] + pub end: u64, + pub agent_name: String, + pub service: String, + pub filter_keys: Vec, + pub filter_values: Vec, + pub selected_team: String, + pub execution_ids: Vec, + #[serde(deserialize_with = "super::number::deserialize")] + pub sample_cap: u64, + #[serde(deserialize_with = "super::number::deserialize")] + pub sample_percent: f64, + #[serde(deserialize_with = "super::number::deserialize")] + pub preview: u8, + pub after: String, + #[serde(deserialize_with = "super::number::deserialize")] + pub limit: u32, + #[serde(deserialize_with = "super::number::deserialize")] + pub offset: u64, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct LensSampleRow { + pub source: String, + pub trace_id: String, + pub team_id: String, + pub trace_ref: String, + pub name: String, + pub start_time: String, + #[serde(deserialize_with = "super::number::deserialize")] + pub span_count: u64, + #[serde(deserialize_with = "super::number::deserialize")] + pub root_seen: u8, + pub service: String, + pub attributes: Vec<(String, String)>, + #[serde(deserialize_with = "super::number::deserialize")] + pub eligible: u64, + #[serde(deserialize_with = "super::number::deserialize")] + pub position: u64, + #[serde(deserialize_with = "super::number::deserialize")] + pub selected: f64, + pub selection_key: String, +} + +impl Query for LensSample { + type Params = LensSampleParams; + type Row = LensSampleRow; + + const SQL: &'static str = include_str!("../../query/lens_sample.sql"); +} + +pub struct LensContent; + +#[derive(Debug, Deserialize, Serialize)] +pub struct LensContentParams { + #[serde(flatten)] + pub access: LensAccessParams, + pub source: String, + pub id: String, + pub record_team: String, + pub trace_ref: String, + pub cursor: String, + #[serde(deserialize_with = "super::number::deserialize")] + pub offset: u32, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct LensContentRow { + pub span_id: String, + pub parent_span_id: String, + pub name: String, + pub kind: String, + pub content: String, + #[serde(deserialize_with = "super::number::deserialize")] + pub truncated: u8, +} + +impl Query for LensContent { + type Params = LensContentParams; + type Row = LensContentRow; + + const SQL: &'static str = include_str!("../../query/lens_content.sql"); +} + +pub struct LensEvidence; + +#[derive(Debug, Deserialize, Serialize)] +pub struct LensEvidenceParams { + #[serde(flatten)] + pub access: LensAccessParams, + pub source: String, + pub id: String, + pub record_team: String, + pub trace_ref: String, + pub span: String, + pub quote: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct LensEvidenceRow { + #[serde(deserialize_with = "super::number::deserialize")] + pub count: u64, +} + +impl Query for LensEvidence { + type Params = LensEvidenceParams; + type Row = LensEvidenceRow; + + const SQL: &'static str = include_str!("../../query/lens_evidence.sql"); +} diff --git a/litellm-rust/crates/traces-clickhouse/src/query/named.rs b/litellm-rust/crates/traces-clickhouse/src/query/named.rs new file mode 100644 index 00000000000..bd90d9fd281 --- /dev/null +++ b/litellm-rust/crates/traces-clickhouse/src/query/named.rs @@ -0,0 +1,320 @@ +use litellm_storage_clickhouse::Query; +use litellm_traces::query::named as contracts; +use serde::{Deserialize, Serialize}; + +pub use contracts::ReadAccessParams; + +#[derive(Deserialize, Serialize)] +#[serde(remote = "contracts::ListTracesParams")] +struct ListTracesParamsEncoding { + #[serde(flatten)] + pub access: contracts::ReadAccessParams, + #[serde(deserialize_with = "super::number::deserialize")] + pub start_ms: i64, + #[serde(deserialize_with = "super::number::deserialize")] + pub end_ms: i64, + #[serde(deserialize_with = "super::number::deserialize")] + pub cursor_ms: i64, + pub cursor_trace_id: String, + #[serde(deserialize_with = "super::number::deserialize")] + pub limit: u32, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct ListTracesParams( + #[serde(with = "ListTracesParamsEncoding")] pub contracts::ListTracesParams, +); + +impl From for ListTracesParams { + fn from(value: contracts::ListTracesParams) -> Self { + Self(value) + } +} + +#[derive(Deserialize, Serialize)] +#[serde(remote = "contracts::ListTracesRow")] +struct ListTracesRowEncoding { + pub trace_id: String, + pub trace_ref: String, + pub team_id: String, + pub api_key_hash: String, + pub user_id: String, + pub name: String, + pub service: String, + pub input_preview: String, + pub status: String, + #[serde(deserialize_with = "super::number::deserialize")] + pub start_ms: i64, + #[serde(deserialize_with = "super::number::deserialize")] + pub duration_ms: i64, + #[serde(deserialize_with = "super::number::deserialize")] + pub span_count: u64, + #[serde(deserialize_with = "super::number::deserialize")] + pub agent_count: u64, + #[serde(deserialize_with = "super::number::deserialize")] + pub agent_invocations: u64, + #[serde(default)] + pub agent_names: Vec, + #[serde(default)] + pub frameworks: Vec, + #[serde(deserialize_with = "super::number::deserialize")] + pub llm_calls: u64, + #[serde(deserialize_with = "super::number::deserialize")] + pub tool_calls: u64, + #[serde(deserialize_with = "super::number::deserialize")] + pub input_tokens: u64, + #[serde(deserialize_with = "super::number::deserialize")] + pub output_tokens: u64, + pub models: Vec, + #[serde(deserialize_with = "super::number::deserialize")] + pub error_count: u64, + pub request_ids: Vec, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct ListTracesRow(#[serde(with = "ListTracesRowEncoding")] pub contracts::ListTracesRow); + +pub use contracts::TraceSpansParams; + +#[derive(Deserialize, Serialize)] +#[serde(remote = "contracts::TraceSpansRow")] +struct TraceSpansRowEncoding { + pub span_id: String, + pub parent_span_id: String, + pub name: String, + #[serde(rename = "type")] + pub kind: String, + pub agent: String, + #[serde(default)] + pub framework: String, + pub status: String, + pub status_message: String, + #[serde(deserialize_with = "super::number::deserialize")] + pub error_truncated: u8, + #[serde(deserialize_with = "super::number::deserialize")] + pub start_ns: i64, + #[serde(deserialize_with = "super::number::deserialize")] + pub duration_ns: u64, + pub service: String, + pub input_preview: String, + pub model: String, + #[serde(deserialize_with = "super::number::deserialize")] + pub input_tokens: u32, + #[serde(deserialize_with = "super::number::deserialize")] + pub output_tokens: u32, + pub litellm_request_id: String, + pub team_id: String, + pub api_key_hash: String, + pub user_id: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct TraceSpansRow(#[serde(with = "TraceSpansRowEncoding")] pub contracts::TraceSpansRow); + +pub use contracts::SpanDetailParams; + +pub use contracts::SpanDetailRow; + +#[derive(Deserialize, Serialize)] +#[serde(remote = "contracts::SpanErrorParams")] +struct SpanErrorParamsEncoding { + #[serde(flatten)] + pub access: contracts::ReadAccessParams, + pub trace_id: String, + pub trace_ref: String, + pub span_id: String, + #[serde(deserialize_with = "super::number::deserialize")] + pub error_offset: u64, + pub error_version: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct SpanErrorParams( + #[serde(with = "SpanErrorParamsEncoding")] pub contracts::SpanErrorParams, +); + +impl From for SpanErrorParams { + fn from(value: contracts::SpanErrorParams) -> Self { + Self(value) + } +} + +#[derive(Deserialize, Serialize)] +#[serde(remote = "contracts::SpanErrorRow")] +struct SpanErrorRowEncoding { + pub span_id: String, + pub message: String, + #[serde(deserialize_with = "super::number::deserialize")] + pub total_chars: u64, + pub version: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct SpanErrorRow(#[serde(with = "SpanErrorRowEncoding")] pub contracts::SpanErrorRow); + +#[derive(Deserialize, Serialize)] +#[serde(remote = "contracts::SpendByResponseIdsParams")] +struct SpendByResponseIdsParamsEncoding { + #[serde(flatten)] + pub access: contracts::ReadAccessParams, + pub response_ids: Vec, + #[serde(deserialize_with = "super::number::deserialize")] + pub start_ms: i64, + #[serde(deserialize_with = "super::number::deserialize")] + pub end_ms: i64, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct SpendByResponseIdsParams( + #[serde(with = "SpendByResponseIdsParamsEncoding")] pub contracts::SpendByResponseIdsParams, +); + +impl From for SpendByResponseIdsParams { + fn from(value: contracts::SpendByResponseIdsParams) -> Self { + Self(value) + } +} + +#[derive(Deserialize, Serialize)] +#[serde(remote = "contracts::SpendByResponseIdsRow")] +struct SpendByResponseIdsRowEncoding { + pub request_id: String, + pub response_id: String, + pub team_id: String, + pub api_key: String, + pub user: String, + #[serde(deserialize_with = "super::number::deserialize")] + pub spend: f64, + #[serde(deserialize_with = "super::number::deserialize")] + pub start_ms: i64, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct SpendByResponseIdsRow( + #[serde(with = "SpendByResponseIdsRowEncoding")] pub contracts::SpendByResponseIdsRow, +); + +pub struct ListTraces; + +impl Query for ListTraces { + type Params = ListTracesParams; + type Row = ListTracesRow; + + const SQL: &'static str = include_str!("../../query/list_traces.sql"); +} + +pub struct TraceSpans; + +impl Query for TraceSpans { + type Params = TraceSpansParams; + type Row = TraceSpansRow; + + const SQL: &'static str = include_str!("../../query/trace_spans.sql"); +} + +pub struct SpanDetail; + +impl Query for SpanDetail { + type Params = SpanDetailParams; + type Row = SpanDetailRow; + + const SQL: &'static str = include_str!("../../query/span_detail.sql"); +} + +pub struct SpanError; + +impl Query for SpanError { + type Params = SpanErrorParams; + type Row = SpanErrorRow; + + const SQL: &'static str = include_str!("../../query/span_error.sql"); +} + +pub struct SpendByResponseIds; + +impl Query for SpendByResponseIds { + type Params = SpendByResponseIdsParams; + type Row = SpendByResponseIdsRow; + + const SQL: &'static str = include_str!("../../query/spend_by_response_ids.sql"); +} + +pub use contracts::{TraceIdentityParams, TraceIdentityRow}; + +pub struct TraceIdentity; + +impl Query for TraceIdentity { + type Params = TraceIdentityParams; + type Row = TraceIdentityRow; + const SQL: &'static str = include_str!("../../query/trace_identity.sql"); +} + +#[cfg(test)] +mod tests { + use super::*; + use rstest::rstest; + use serde_json::{Value, json}; + + fn round_trip(wire: Value, quoted: bool) { + let encoded = Value::Object( + wire.as_object() + .unwrap() + .iter() + .map(|(name, value)| { + let encoded = if quoted && value.is_number() && name != "all_teams" { + json!(value.to_string()) + } else { + value.clone() + }; + (name.clone(), encoded) + }) + .collect(), + ); + let decoded: T = serde_json::from_value(encoded).unwrap(); + assert_eq!(serde_json::to_value(decoded).unwrap(), wire); + } + + #[rstest] + #[case::unquoted(false)] + #[case::quoted(true)] + fn rows_decode_into_neutral_contracts(#[case] quoted: bool) { + round_trip::( + json!({"trace_id": "trace", "trace_ref": "ref", "team_id": "team", "api_key_hash": "key", "user_id": "user", "name": "agent", "service": "service", "input_preview": "input", "status": "ok", "start_ms": -1, "duration_ms": 20, "span_count": u64::MAX, "agent_count": 1, "agent_invocations": 2, "agent_names": ["agent"], "frameworks": ["claude-agent-sdk"], "llm_calls": 3, "tool_calls": 4, "input_tokens": 5, "output_tokens": 6, "models": ["model"], "error_count": 0, "request_ids": ["request"]}), + quoted, + ); + round_trip::( + json!({"span_id": "span", "parent_span_id": "parent", "name": "agent", "type": "agent", "agent": "agent", "framework": "claude-agent-sdk", "status": "error", "status_message": "error", "error_truncated": 1, "start_ns": -1, "duration_ns": u64::MAX, "service": "service", "input_preview": "input", "model": "model", "input_tokens": u32::MAX, "output_tokens": 6, "litellm_request_id": "request", "team_id": "team", "api_key_hash": "key", "user_id": "user"}), + quoted, + ); + round_trip::( + json!({"span_id": "span", "input": "input", "output": "output", "attributes": {"count": "42"}}), + quoted, + ); + round_trip::( + json!({"span_id": "span", "message": "error", "total_chars": u64::MAX, "version": "version"}), + quoted, + ); + round_trip::( + json!({"request_id": "request", "response_id": "response", "team_id": "team", "api_key": "key", "user": "user", "spend": 0.125, "start_ms": -1}), + quoted, + ); + } + + #[rstest] + #[case::unquoted(false)] + #[case::quoted(true)] + fn parameters_preserve_flattened_multi_team_access(#[case] quoted: bool) { + round_trip::( + json!({"all_teams": 0, "user_id": "user", "team_ids": ["team-a", "team-b"], "start_ms": -1, "end_ms": 10, "cursor_ms": 0, "cursor_trace_id": "", "limit": u32::MAX}), + quoted, + ); + round_trip::( + json!({"all_teams": 0, "user_id": "", "team_ids": [], "trace_id": "trace", "trace_ref": "ref", "span_id": "span", "error_offset": u64::MAX, "error_version": "version"}), + quoted, + ); + round_trip::( + json!({"all_teams": 0, "user_id": "user", "team_ids": ["team-a", "team-b"], "response_ids": ["response"], "start_ms": -1, "end_ms": 10}), + quoted, + ); + } +} diff --git a/litellm-rust/crates/traces-clickhouse/src/query/number.rs b/litellm-rust/crates/traces-clickhouse/src/query/number.rs new file mode 100644 index 00000000000..f6af195e7a3 --- /dev/null +++ b/litellm-rust/crates/traces-clickhouse/src/query/number.rs @@ -0,0 +1,44 @@ +use serde::{Deserialize, Deserializer, de::DeserializeOwned}; + +pub(super) fn deserialize<'de, D, T>(deserializer: D) -> Result +where + D: Deserializer<'de>, + T: DeserializeOwned, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum Number { + Quoted(String), + Unquoted(serde_json::Number), + } + match Number::deserialize(deserializer)? { + Number::Quoted(value) => serde_json::from_str(&value), + Number::Unquoted(value) => serde_json::from_value(serde_json::Value::Number(value)), + } + .map_err(serde::de::Error::custom) +} + +#[cfg(test)] +mod tests { + use crate::query::named::SpanErrorRow; + use rstest::rstest; + + #[rstest] + #[case::quoted_max(serde_json::json!(u64::MAX.to_string()), Some(u64::MAX))] + #[case::unquoted_max(serde_json::json!(u64::MAX), Some(u64::MAX))] + #[case::overflow(serde_json::json!("18446744073709551616"), None)] + #[case::negative(serde_json::json!(-1), None)] + #[case::fraction(serde_json::json!(1.5), None)] + fn numeric_rows_enforce_integer_range( + #[case] value: serde_json::Value, + #[case] expected: Option, + ) { + let row = serde_json::from_value::(serde_json::json!({ + "span_id": "span", "message": "error", "total_chars": value, "version": "hash" + })); + match expected { + Some(value) => assert_eq!(row.unwrap().0.total_chars, value), + None => assert!(row.is_err()), + } + } +} diff --git a/litellm-rust/crates/traces-clickhouse/src/query_access.rs b/litellm-rust/crates/traces-clickhouse/src/query_access.rs new file mode 100644 index 00000000000..e6ca322098e --- /dev/null +++ b/litellm-rust/crates/traces-clickhouse/src/query_access.rs @@ -0,0 +1,245 @@ +use std::{sync::Arc, time::Duration}; + +use hmac::{Hmac, Mac}; +use litellm_http::Client; +use litellm_storage_clickhouse::READ_LIMITS; +use litellm_traces::QueryScope; +use moka::future::Cache; +use strum::IntoEnumIterator; + +use sha2::{Digest, Sha256}; +use tokio::sync::{OwnedSemaphorePermit, Semaphore}; + +use super::{Connection, Error, TraceTable}; + +const MIB: u64 = 1024 * 1024; + +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub(crate) struct ReaderLimits { + pub result_rows: u64, + pub result_bytes: u64, + pub memory_bytes: u64, + pub execution_seconds: u64, +} + +impl ReaderLimits { + pub fn result_mib(&self) -> u64 { + self.result_bytes / MIB + } + + pub fn memory_mib(&self) -> u64 { + self.memory_bytes / MIB + } +} + +pub(crate) const READER_LIMITS: ReaderLimits = ReaderLimits { + result_rows: READ_LIMITS.result_rows, + result_bytes: READ_LIMITS.response_bytes as u64, + memory_bytes: 256 * MIB, + execution_seconds: READ_LIMITS.execution_seconds, +}; + +#[derive(Clone)] +pub struct QueryReaders { + writer: Connection, + database: String, + readers: Cache, + slots: Arc, +} + +impl QueryReaders { + pub fn new(writer: Connection, database: String) -> Self { + Self { + writer, + database, + readers: Cache::builder().max_capacity(1024).build(), + slots: Arc::new(Semaphore::new(8)), + } + } + + pub fn acquire(&self) -> Result { + self.slots + .clone() + .try_acquire_owned() + .map_err(|_| Error::Busy) + } + + pub async fn connection( + &self, + client: &Client, + scope: &QueryScope, + secret: &str, + ) -> Result { + scope.validate().map_err(|_| Error::InvalidScope)?; + if secret.is_empty() { + return Err(Error::MissingSecret); + } + let identity = serde_json::to_vec(&("litellm_trace_reader_v1", &self.database, scope)) + .map_err(|_| Error::InvalidScope)?; + let user = format!("litellm_traces_{:x}", Sha256::digest(&identity)); + let password = credential(secret, b"password", &identity)?; + self.readers + .try_get_with( + user.clone(), + self.provision(client, scope, &user, &password), + ) + .await + .map_err(Error::Cached) + } + + async fn provision( + &self, + client: &Client, + scope: &QueryScope, + user: &str, + password: &str, + ) -> Result { + let database = &self.database; + if database.is_empty() + || !database + .bytes() + .all(|c| c.is_ascii_alphanumeric() || c == b'_') + { + return Err(Error::InvalidScope); + } + let password_hash = format!("{:x}", Sha256::digest(password)); + let ReaderLimits { + result_rows, + result_bytes, + memory_bytes, + execution_seconds, + } = READER_LIMITS; + self.execute( + client, + format!( + "CREATE USER IF NOT EXISTS {user} IDENTIFIED WITH sha256_hash BY '{password_hash}' \ + SETTINGS readonly = 1 CONST, max_execution_time = {execution_seconds} CONST, \ + max_result_rows = {result_rows} CONST, max_result_bytes = {result_bytes} CONST, \ + result_overflow_mode = 'throw' CONST, max_memory_usage = {memory_bytes} CONST, \ + max_threads = 2 CONST, max_concurrent_queries_for_user = 8 CONST" + ), + ) + .await?; + self.execute( + client, + format!("ALTER USER {user} IDENTIFIED WITH sha256_hash BY '{password_hash}'"), + ) + .await?; + for table in TraceTable::iter() { + let predicate = predicate(scope, table); + self.execute( + client, + format!( + "CREATE ROW POLICY IF NOT EXISTS {user}_allow ON `{database}`.{table} \ + USING 1 TO {user}" + ), + ) + .await?; + self.execute( + client, + format!( + "CREATE ROW POLICY IF NOT EXISTS {user}_scope ON `{database}`.{table} \ + AS RESTRICTIVE USING {predicate} TO {user}" + ), + ) + .await?; + } + for table in TraceTable::iter() { + self.execute( + client, + format!("GRANT SELECT ON `{database}`.{table} TO {user}"), + ) + .await?; + } + Connection::configured( + &self.writer.url()[..url::Position::AfterPath], + database, + user, + password, + ) + .map_err(Error::Storage) + } + + async fn execute(&self, client: &Client, sql: String) -> Result<(), Error> { + let response = client + .post(self.writer.url().clone()) + .timeout(Duration::from_secs(15)) + .body(sql) + .send() + .await + .map_err(|_| Error::ProvisionTransport)?; + if !response.status().is_success() { + return Err(Error::ProvisionFailed(response.status().as_u16())); + } + Ok(()) + } +} + +fn predicate(scope: &QueryScope, table: TraceTable) -> String { + let team = match table { + TraceTable::OtelTraces | TraceTable::AgentTracesByKey => "TeamId", + TraceTable::SpendLogs => "team_id", + }; + match scope { + QueryScope::All => "1".to_owned(), + QueryScope::Owned { user_id, team_ids } => { + let owner = literal(user_id); + let user_clause = match table { + TraceTable::OtelTraces => format!("UserId = {owner}"), + TraceTable::AgentTracesByKey => format!("UserIds = [{owner}]"), + TraceTable::SpendLogs => format!("user = {owner}"), + }; + let teams = team_ids + .iter() + .map(|value| literal(value)) + .collect::>() + .join(", "); + let team_clause = if team_ids.is_empty() { + "0".to_owned() + } else { + format!("{team} IN ({teams})") + }; + format!("({owner} != '' AND {user_clause}) OR ({team_clause})") + } + } +} + +fn credential(secret: &str, purpose: &[u8], identity: &[u8]) -> Result { + let mut mac = + Hmac::::new_from_slice(secret.as_bytes()).map_err(|_| Error::MissingSecret)?; + mac.update(purpose); + mac.update(identity); + Ok(format!("{:x}", mac.finalize().into_bytes())) +} + +fn literal(value: &str) -> String { + format!("'{}'", value.replace('\\', "\\\\").replace('\'', "\\'")) +} + +#[cfg(test)] +mod tests { + use super::*; + use rstest::rstest; + + #[rstest] + #[case::otel(TraceTable::OtelTraces, "TeamId", "UserId = ''")] + #[case::agent(TraceTable::AgentTracesByKey, "TeamId", "UserIds = ['']")] + #[case::spend(TraceTable::SpendLogs, "team_id", "user = ''")] + fn predicates_preserve_scope_and_escape_values( + #[case] table: TraceTable, + #[case] team: &str, + #[case] user: &str, + ) { + assert_eq!(predicate(&QueryScope::All, table), "1"); + assert_eq!( + predicate( + &QueryScope::Owned { + user_id: String::new(), + team_ids: vec!["team'\\".into()] + }, + table + ), + format!("('' != '' AND {user}) OR ({team} IN ('team\\'\\\\'))") + ); + } +} diff --git a/litellm-rust/crates/traces-clickhouse/src/schema.rs b/litellm-rust/crates/traces-clickhouse/src/schema.rs new file mode 100644 index 00000000000..4ebfbbf595f --- /dev/null +++ b/litellm-rust/crates/traces-clickhouse/src/schema.rs @@ -0,0 +1,136 @@ +use litellm_http::Client; +use litellm_migrate::Migration; +use serde::Serialize; +use std::time::Duration; + +use super::Connection; +use super::Error; + +const SCHEMA_REQUEST_TIMEOUT: Duration = Duration::from_secs(30); + +const MIGRATIONS: &[Migration] = litellm_migrate::migrate!("migrations"); + +pub fn schema_statements(database: &str, retention_days: u32) -> Result, Error> { + if database.is_empty() + || !database + .bytes() + .all(|c| c.is_ascii_alphanumeric() || c == b'_') + || retention_days == 0 + { + return Err(Error::InvalidSchema); + } + let database = format!("`{database}`"); + Ok( + std::iter::once(format!("CREATE DATABASE IF NOT EXISTS {database}")) + .chain(MIGRATIONS.iter().map(|migration| { + migration + .sql + .replace("{database}", &database) + .replace("{retention_days}", &retention_days.to_string()) + })) + .collect(), + ) +} + +pub async fn ensure_schema( + client: &Client, + connection: &Connection, + database: &str, + retention_days: u32, +) -> Result<(), Error> { + ensure_schema_with_timeout( + client, + connection, + database, + retention_days, + SCHEMA_REQUEST_TIMEOUT, + ) + .await +} + +async fn ensure_schema_with_timeout( + client: &Client, + connection: &Connection, + database: &str, + retention_days: u32, + request_timeout: Duration, +) -> Result<(), Error> { + for statement in schema_statements(database, retention_days)? { + let response = client + .post(connection.url().clone()) + .timeout(request_timeout) + .body(statement) + .send() + .await + .map_err(|_| Error::SchemaTransport)?; + if !response.status().is_success() { + return Err(Error::SchemaFailed(response.status().as_u16())); + } + } + Ok(()) +} + +#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)] +pub struct NormalizedFieldDefinition { + pub name: &'static str, + pub clickhouse_column: &'static str, + pub clickhouse_type: &'static str, + pub meaning: &'static str, +} + +pub const NORMALIZED_FIELD_DEFINITIONS: [NormalizedFieldDefinition; 9] = [ + NormalizedFieldDefinition { + name: "observation_type", + clickhouse_column: "ObservationType", + clickhouse_type: "LowCardinality(String)", + meaning: "Agent, LLM, tool, chain, or framework span", + }, + NormalizedFieldDefinition { + name: "agent_name", + clickhouse_column: "AgentName", + clickhouse_type: "LowCardinality(String)", + meaning: "Agent associated with this span", + }, + NormalizedFieldDefinition { + name: "framework", + clickhouse_column: "Framework", + clickhouse_type: "LowCardinality(String)", + meaning: "Agent framework or SDK that emitted this span, e.g. claude-agent-sdk", + }, + NormalizedFieldDefinition { + name: "litellm_request_id", + clickhouse_column: "LiteLLMRequestId", + clickhouse_type: "String", + meaning: "LiteLLM response ID used to link a span to a spend log", + }, + NormalizedFieldDefinition { + name: "model", + clickhouse_column: "Model", + clickhouse_type: "LowCardinality(String)", + meaning: "Model used by this span", + }, + NormalizedFieldDefinition { + name: "input_tokens", + clickhouse_column: "InputTokens", + clickhouse_type: "UInt32", + meaning: "Input token count", + }, + NormalizedFieldDefinition { + name: "output_tokens", + clickhouse_column: "OutputTokens", + clickhouse_type: "UInt32", + meaning: "Output token count", + }, + NormalizedFieldDefinition { + name: "input", + clickhouse_column: "Input", + clickhouse_type: "String", + meaning: "Normalized input payload", + }, + NormalizedFieldDefinition { + name: "output", + clickhouse_column: "Output", + clickhouse_type: "String", + meaning: "Normalized output payload", + }, +]; diff --git a/litellm-rust/crates/traces-clickhouse/src/sql.rs b/litellm-rust/crates/traces-clickhouse/src/sql.rs new file mode 100644 index 00000000000..16a610ca13a --- /dev/null +++ b/litellm-rust/crates/traces-clickhouse/src/sql.rs @@ -0,0 +1,83 @@ +use std::collections::BTreeMap; + +use litellm_http::Client; +use litellm_traces::ReadQuery; + +use super::query::{lens::*, named::*}; +use super::{Connection, Error, Parameter}; +use litellm_storage_clickhouse::{Query, fetch_json}; + +pub async fn execute_named_read( + client: &Client, + connection: &Connection, + query: ReadQuery, + parameters: &BTreeMap, +) -> Result { + match query { + ReadQuery::ListTraces => named_json::(client, connection, parameters).await, + ReadQuery::TraceIdentity => { + named_json::(client, connection, parameters).await + } + ReadQuery::TraceSpans => named_json::(client, connection, parameters).await, + ReadQuery::SpanDetail => named_json::(client, connection, parameters).await, + ReadQuery::SpanError => named_json::(client, connection, parameters).await, + ReadQuery::SpendByResponseIds => { + named_json::(client, connection, parameters).await + } + ReadQuery::Availability => { + named_json::(client, connection, parameters).await + } + ReadQuery::Agents => named_json::(client, connection, parameters).await, + ReadQuery::Sample => named_json::(client, connection, parameters).await, + ReadQuery::Content => named_json::(client, connection, parameters).await, + ReadQuery::Evidence => named_json::(client, connection, parameters).await, + } +} + +async fn named_json( + client: &Client, + connection: &Connection, + parameters: &BTreeMap, +) -> Result +where + Q::Params: serde::de::DeserializeOwned, +{ + let value = serde_json::to_value(parameters).map_err(|_| Error::InvalidParameters)?; + let params = + serde_json::from_value::(value).map_err(|_| Error::InvalidParameters)?; + fetch_json::(client, connection, ¶ms) + .await + .map_err(Error::from) +} + +#[cfg(test)] +mod tests { + use super::*; + use rstest::rstest; + + #[rstest] + #[case::missing_span(serde_json::json!({}))] + #[case::negative_offset(serde_json::json!({"span_id": "span", "error_offset": -1, "error_version": ""}))] + #[case::overflow(serde_json::json!({"span_id": "span", "error_offset": "18446744073709551616", "error_version": ""}))] + #[tokio::test] + async fn named_read_rejects_invalid_parameters_before_transport( + #[case] specific: serde_json::Value, + ) { + let common = serde_json::json!({ + "all_teams": 1, "user_id": "", "team_ids": [], "trace_id": "trace", "trace_ref": "" + }); + let parameters: BTreeMap = common + .as_object() + .unwrap() + .iter() + .chain(specific.as_object().unwrap().iter()) + .map(|(name, value)| (name.clone(), serde_json::from_value(value.clone()).unwrap())) + .collect(); + let client = Client::no_redirect_for_test(); + let connection = Connection::parse("http://127.0.0.1:1").unwrap(); + assert!(matches!( + execute_named_read(&client, &connection, ReadQuery::SpanError, ¶meters).await, + Err(Error::InvalidParameters) + )); + } +} diff --git a/litellm-rust/crates/traces-clickhouse/src/table.rs b/litellm-rust/crates/traces-clickhouse/src/table.rs new file mode 100644 index 00000000000..4cbbf3cee89 --- /dev/null +++ b/litellm-rust/crates/traces-clickhouse/src/table.rs @@ -0,0 +1,17 @@ +#[derive( + Clone, + Copy, + Debug, + serde::Serialize, + strum::Display, + strum::AsRefStr, + strum::EnumIter, + strum::IntoStaticStr, +)] +#[serde(rename_all = "snake_case")] +#[strum(serialize_all = "snake_case")] +pub enum TraceTable { + OtelTraces, + AgentTracesByKey, + SpendLogs, +} diff --git a/litellm-rust/crates/traces/templates/query_help.jinja b/litellm-rust/crates/traces-clickhouse/templates/query_help.jinja similarity index 67% rename from litellm-rust/crates/traces/templates/query_help.jinja rename to litellm-rust/crates/traces-clickhouse/templates/query_help.jinja index 4bded74588c..3bea0ff4efe 100644 --- a/litellm-rust/crates/traces/templates/query_help.jinja +++ b/litellm-rust/crates/traces-clickhouse/templates/query_help.jinja @@ -11,18 +11,18 @@ Normalized span fields {% endfor %} Observed LLM call metadata {{ metadata.scope }} -Sampled rows: {{ metadata.sampled_rows }}; invalid JSON rows: {{ metadata.invalid_json_rows }}; truncated: {{ metadata.truncated }} -{% if let Some(error) = metadata.error %}Metadata discovery unavailable: {{ error }} -{% else if metadata.fields.is_empty() %}No metadata paths found in the sampled rows -{% else %}{% for field in metadata.fields %}{{ field.expression }}: {% for kind in field.types %}{{ kind }} {% endfor %} -{% endfor %}{% endif %} +{% match metadata.discovery %}{% when Discovery::Unavailable(error) %}Metadata discovery unavailable: {{ error }} +{% when Discovery::Observed(sample) %}Sampled rows: {{ sample.sampled_rows }}; invalid JSON rows: {{ sample.invalid_json_rows }}; truncated: {{ sample.truncated }} +{% if sample.fields.is_empty() %}No metadata paths found in the sampled rows +{% else %}{% for field in sample.fields %}{{ field.expression }}: {% for kind in field.types %}{{ kind }} {% endfor %} +{% endfor %}{% endif %}{% endmatch %} Observed span and resource attributes {% for catalog in attributes %}{{ catalog.table }}.{{ catalog.column }} {{ catalog.scope }} -{% if let Some(error) = catalog.error %}Attribute discovery unavailable: {{ error }} -{% else if catalog.fields.is_empty() %}No attribute keys found in the sampled spans -{% else %}{% for field in catalog.fields %}{{ field.expression }}: {{ field.kind }} -{% endfor %}{% endif %}{% endfor %} +{% match catalog.discovery %}{% when Discovery::Unavailable(error) %}Attribute discovery unavailable: {{ error }} +{% when Discovery::Observed(sample) %}{% if sample.fields.is_empty() %}No attribute keys found in the sampled spans +{% else %}{% for field in sample.fields %}{{ field.expression }}: {{ field.kind }} +{% endfor %}{% endif %}{% endmatch %}{% endfor %} Examples {% block recent_spans_name %}Recent normalized LLM spans{% endblock %} @@ -35,7 +35,7 @@ Examples {% block nested_metadata_sql %}SELECT request_id, JSONType(metadata, 'labels', 'priority') AS type, JSONExtractRaw(metadata, 'labels', 'priority') AS value FROM spend_logs FINAL WHERE start_time >= now() - INTERVAL 1 DAY AND JSONHas(metadata, 'labels', 'priority') LIMIT 100{% endblock %} {% block correlated_calls_name %}Traces correlated with LLM call metadata{% endblock %} -{% block correlated_calls_sql %}SELECT t.TraceId, t.SpanId, s.request_id, s.spend, s.metadata FROM otel_traces AS t INNER JOIN (SELECT * FROM spend_logs FINAL WHERE start_time >= now() - INTERVAL 1 DAY) AS s ON t.LiteLLMRequestId = s.response_id AND t.TeamId = s.team_id AND t.ApiKeyHash = s.api_key WHERE t.Timestamp >= now() - INTERVAL 1 DAY AND t.LiteLLMRequestId != '' AND JSONExtractString(s.metadata, 'project') = 'example' LIMIT 100{% endblock %} +{% block correlated_calls_sql %}SELECT t.TraceId, t.SpanId, s.request_id, s.spend, s.metadata FROM otel_traces AS t INNER JOIN (SELECT * FROM spend_logs FINAL WHERE start_time >= now() - INTERVAL 1 DAY) AS s ON t.LiteLLMRequestId = s.response_id AND t.TeamId = s.team_id AND (t.TeamId != '' OR (t.UserId != '' AND t.UserId = s.user) OR (t.ApiKeyHash != '' AND t.ApiKeyHash = s.api_key)) WHERE t.Timestamp >= now() - INTERVAL 1 DAY AND t.LiteLLMRequestId != '' AND JSONExtractString(s.metadata, 'project') = 'example' LIMIT 100{% endblock %} {% block discover_keys_name %}Discover metadata keys over a different window{% endblock %} {% block discover_keys_sql %}SELECT DISTINCT arrayJoin(JSONExtractKeys(metadata)) AS key FROM spend_logs FINAL WHERE start_time >= now() - INTERVAL 30 DAY ORDER BY key LIMIT 200{% endblock %} @@ -44,9 +44,9 @@ Gotchas {% block time_window %}Always bound Timestamp or start_time and use LIMIT; add TeamId/ApiKeyHash or team_id/api_key filters when investigating one tenant{% endblock %} -{% block reader_limits %}The reader enforces 1000 result rows, 4 MiB response bytes, 256 MiB memory and a 10 second query limit; exceeding limits fails instead of returning partial results{% endblock %} +{% block reader_limits %}The reader enforces {{ limits.result_rows }} result rows, {{ limits.result_mib() }} MiB response bytes, {{ limits.memory_mib() }} MiB memory and a {{ limits.execution_seconds }} second query limit; exceeding limits fails instead of returning partial results{% endblock %} -{% block reader_profile %}LiteLLM provisions SELECT-only readers from the configured ClickHouse connection and enforces authenticated team scope through row policies. Project-bound and teamless keys see only their own rows. Provisioning requires CREATE USER, ALTER USER, CREATE ROW POLICY, and GRANT SELECT permissions{% endblock %} +{% block reader_profile %}LiteLLM provisions SELECT-only readers from the configured ClickHouse connection and enforces request-log visibility through row policies. Callers see their own user rows and permitted teams. Provisioning requires CREATE USER, ALTER USER, CREATE ROW POLICY, and GRANT SELECT permissions{% endblock %} {% block output_format %}Do not add FORMAT clauses; the endpoint requires ClickHouse JSON output{% endblock %} @@ -58,7 +58,7 @@ Gotchas {% block time_units %}Duration is nanoseconds; Timestamp has nanosecond precision, spend start_time has millisecond precision{% endblock %} -{% block spend_totals %}Use spend_logs FINAL to collapse replacement rows before totals. Shared response IDs and multiple spans can multiply costs in joins; aggregate spend separately{% endblock %} +{% block spend_totals %}Use spend_logs FINAL to collapse replacement rows before totals. Shared response IDs and multiple spans can multiply costs in joins; require one spend match per response ID and ownership before aggregating. Missing IDs or costs leave totals unknown{% endblock %} {% block trace_rollups %}agent_traces_by_key uses SimpleAggregateFunction columns; group by TeamId, ApiKeyHash and TraceId, using min(StartTs), max(EndTs), sum(SpanCount) and groupUniqArrayArray(Models). Do not use Merge combinators{% endblock %} diff --git a/litellm-rust/crates/traces/tests/admin_sql.rs b/litellm-rust/crates/traces-clickhouse/tests/admin_sql.rs similarity index 84% rename from litellm-rust/crates/traces/tests/admin_sql.rs rename to litellm-rust/crates/traces-clickhouse/tests/admin_sql.rs index ab0eb873a28..49499ebfa39 100644 --- a/litellm-rust/crates/traces/tests/admin_sql.rs +++ b/litellm-rust/crates/traces-clickhouse/tests/admin_sql.rs @@ -1,18 +1,16 @@ use litellm_http::Client; -use litellm_traces::{Connection, Error, Parameter, execute_read}; +use litellm_traces_clickhouse::{ + Connection, Error, Parameter, QueryReaders, QueryScope, execute_read, +}; use rstest::{fixture, rstest}; use serde_json::Value; use std::collections::BTreeMap; -use testcontainers_modules::{ - clickhouse::ClickHouse, - testcontainers::{ContainerAsync, ImageExt, runners::AsyncRunner}, -}; +mod support; -const CLICKHOUSE_TAG: &str = - "26.9.6.6@sha256:eb4870e7ca7ed70c259eebfcfbee6cf797017f6b5436c2926bbbfe3d4d28486e"; +use support::{ClickHouseDatabase, database as start_database}; struct Database { - _container: ContainerAsync, + _database: ClickHouseDatabase, url: String, admin_url: String, client: Client, @@ -20,22 +18,9 @@ struct Database { #[fixture] async fn database() -> Result> { - let container = ClickHouse::default() - .with_tag(CLICKHOUSE_TAG) - .with_env_var("CLICKHOUSE_SKIP_USER_SETUP", "1") - .with_env_var("LITELLM_TRACES_READER_PASSWORD", "test_password") - .with_copy_to( - "/etc/clickhouse-server/users.d/litellm-traces-reader.xml", - include_bytes!("../config/reader.xml").to_vec(), - ) - .start() - .await?; - let admin_url = format!( - "http://{}:{}", - container.get_host().await?, - container.get_host_port_ipv4(8123).await?, - ); - let client = Client::no_redirect_for_test(); + let instance = start_database().await?; + let admin_url = instance.url.clone(); + let client = instance.client.clone(); for sql in [ "CREATE DATABASE litellm", "CREATE TABLE litellm.otel_traces (n UInt8) ENGINE = Memory", @@ -54,12 +39,13 @@ async fn database() -> Result> { .await? .error_for_status()?; } - let url = format!( - "{}?database=litellm", - admin_url.replacen("http://", "http://litellm_traces_reader:test_password@", 1) - ); + let readers = QueryReaders::new(Connection::writer(&admin_url)?, "litellm".into()); + let connection = readers + .connection(&client, &QueryScope::All, "test-secret") + .await?; + let url = connection.url().to_string(); Ok(Database { - _container: container, + _database: instance, url, admin_url, client, @@ -120,7 +106,15 @@ async fn reader_rejects_writes_and_privilege_escalation( let result = read(&database.client, &connection, sql).await; - assert!(matches!(result, Err(Error::QueryFailed(_))), "{result:?}"); + assert!( + matches!( + result, + Err(Error::Storage( + litellm_storage_clickhouse::Error::QueryFailed(_) + )) + ), + "{result:?}" + ); let rows = read(&database.client, &connection, "SELECT n FROM otel_traces").await?; let json: Value = serde_json::from_str(&rows)?; assert_eq!(json["data"], serde_json::json!([{ "n": 1 }])); @@ -147,7 +141,12 @@ async fn admin_sql_rejects_errors_after_output_starts( .await; assert!( - matches!(result, Err(Error::InvalidResponse)), + matches!( + result, + Err(Error::Storage( + litellm_storage_clickhouse::Error::InvalidResponse + )) + ), "expected an error embedded in a successful HTTP response: {result:?}" ); Ok(()) @@ -171,7 +170,15 @@ async fn admin_sql_enforces_result_row_limit( ) .await; - assert!(matches!(result, Err(Error::QueryFailed(_))), "{result:?}"); + assert!( + matches!( + result, + Err(Error::Storage( + litellm_storage_clickhouse::Error::QueryFailed(_) + )) + ), + "{result:?}" + ); Ok(()) } @@ -190,7 +197,15 @@ async fn admin_sql_enforces_response_byte_limit( ) .await; - assert!(matches!(result, Err(Error::ResponseTooLarge)), "{result:?}"); + assert!( + matches!( + result, + Err(Error::Storage( + litellm_storage_clickhouse::Error::ResponseTooLarge + )) + ), + "{result:?}" + ); Ok(()) } diff --git a/litellm-rust/crates/traces-clickhouse/tests/fixtures/README.md b/litellm-rust/crates/traces-clickhouse/tests/fixtures/README.md new file mode 100644 index 00000000000..560f919d867 --- /dev/null +++ b/litellm-rust/crates/traces-clickhouse/tests/fixtures/README.md @@ -0,0 +1,15 @@ +# ClickHouse query fixtures + +Run `cargo test -p litellm-traces-clickhouse --test queries --locked -- --test-threads=2` from `litellm-rust` with Docker running + +Raw OTLP exports live in `crates/traces/tests/fixtures/query_*.json`. The seeded fixture decodes and normalizes them through `litellm_traces::decode_otlp` at test startup, then projects the decoded fields into ClickHouse columns. Team and key identities come from fixture setup rather than exporter claims. Root and child exports are inserted separately through the public insert API so materialized views process multiple blocks + +`crates/traces/tests/fixtures/deeplite_auth_error.json` and `deeplite_swarm.json` were captured from Deeplite runs against the local proxy on 2026-10-02. The first contains a failed model call. The second contains successful model calls, searches, handoff attempts, and virtual filesystem writes. Credentials, workspace identifiers, and local user paths were redacted, and the protobuf exports were converted to OTLP JSON. Their round-trip tests check span identities, parent links, timestamps, durations, token counts, and statuses without pinning the provider's error wording + +The swarm capture has handoff spans marked ERROR with `ParentCommand` exception events and a root with UNSET status. These are exported diagnostic statuses, which do not establish a failed execution. The tests preserve incoming statuses and check root status separately from the count of error spans, deriving both from the decoded export. They do not infer an execution outcome from exception text, framework names, successful model calls, or output presence. Framework-specific interpretation of control-flow exceptions belongs in the instrumentation integration + +`spend_logs.jsonl` contains spend insert rows with millisecond timestamps, including two versions of one request. Replace this small placeholder dataset when the actual data is available. The query fixture applies production migrations, then removes TTL from its isolated database so fixed timestamps do not expire. Background merges are stopped so rollup aggregation and `FINAL` deduplication are exercised on unmerged data. Retention behavior stays covered by the migration tests + +Curated SQL lives in `tests/queries/*.sql`. Each query has a matching `.expected.json` containing ordered result rows for `admin`, `team`, `key`, and `other_team` readers. Update the exports and expected results together. Add a named case in `tests/queries.rs` for each new query. Assertions compare only result data, excluding server statistics and execution timing + +Typed query tests execute the production SQL through `litellm_storage_clickhouse::fetch` using contracts from `litellm-traces`. The fixture projection is test setup, so this suite covers the Rust decoder, normalization, inserts, schema, readers, and queries. Python ingress transformations, including payload truncation and exception-event fallback, remain covered by the Python tests diff --git a/litellm-rust/crates/traces-clickhouse/tests/fixtures/spend_logs.jsonl b/litellm-rust/crates/traces-clickhouse/tests/fixtures/spend_logs.jsonl new file mode 100644 index 00000000000..d798df19aa4 --- /dev/null +++ b/litellm-rust/crates/traces-clickhouse/tests/fixtures/spend_logs.jsonl @@ -0,0 +1,3 @@ +{"request_id":"request-a","response_id":"response-shared","team_id":"team-a","api_key":"key-a","user":"user-a","spend":0.125,"start_time":1735689600100,"end_time":1735689600500,"metadata":"{\"labels\":{\"priority\":\"obsolete\"}}"} +{"request_id":"request-a","response_id":"response-shared","team_id":"team-a","api_key":"key-a","user":"user-a","spend":0.5,"start_time":1735689600100,"end_time":1735689600600,"metadata":"{\"labels\":{\"priority\":\"high\"}}"} +{"request_id":"request-b","response_id":"response-shared","team_id":"team-b","api_key":"key-b","user":"user-b","spend":0.25,"start_time":1735689602000,"end_time":1735689602500,"metadata":"{\"labels\":{\"priority\":\"low\"}}"} diff --git a/litellm-rust/crates/traces/tests/insert.rs b/litellm-rust/crates/traces-clickhouse/tests/insert.rs similarity index 97% rename from litellm-rust/crates/traces/tests/insert.rs rename to litellm-rust/crates/traces-clickhouse/tests/insert.rs index 9dcb9cddf1f..2d1c98668e2 100644 --- a/litellm-rust/crates/traces/tests/insert.rs +++ b/litellm-rust/crates/traces-clickhouse/tests/insert.rs @@ -5,8 +5,9 @@ use std::{ use flate2::read::GzDecoder; use litellm_http::Client; -use litellm_traces::{ - Connection, Error, InsertRow, InsertTable, Shared, encode_rows, insert_shared_rows, +use litellm_traces::Shared; +use litellm_traces_clickhouse::{ + Connection, Error, InsertRow, InsertTable, encode_rows, insert_shared_rows, }; use rstest::{fixture, rstest}; use serde_json::{Value, json}; diff --git a/litellm-rust/crates/traces/tests/migrations.rs b/litellm-rust/crates/traces-clickhouse/tests/migrations.rs similarity index 69% rename from litellm-rust/crates/traces/tests/migrations.rs rename to litellm-rust/crates/traces-clickhouse/tests/migrations.rs index ccefa8b0b9f..2f9e88af3e7 100644 --- a/litellm-rust/crates/traces/tests/migrations.rs +++ b/litellm-rust/crates/traces-clickhouse/tests/migrations.rs @@ -1,45 +1,14 @@ use std::{collections::BTreeMap, time::Duration}; use litellm_http::Client; -use litellm_traces::{ +use litellm_traces_clickhouse::{ Connection, Error, InsertTable, NORMALIZED_FIELD_DEFINITIONS, Parameter, ReadQuery, encode_rows, ensure_schema, execute_named_read, execute_read, schema_statements, }; -use rstest::{fixture, rstest}; -use testcontainers_modules::{ - clickhouse::ClickHouse, - testcontainers::{ContainerAsync, ImageExt, runners::AsyncRunner}, -}; +use rstest::rstest; +mod support; -const CLICKHOUSE_TAG: &str = - "26.9.6.6@sha256:eb4870e7ca7ed70c259eebfcfbee6cf797017f6b5436c2926bbbfe3d4d28486e"; - -type TestResult = Result>; - -struct ClickHouseDatabase { - _container: ContainerAsync, - url: String, - client: Client, -} - -#[fixture] -async fn database() -> TestResult { - let container = ClickHouse::default() - .with_tag(CLICKHOUSE_TAG) - .with_env_var("CLICKHOUSE_SKIP_USER_SETUP", "1") - .start() - .await?; - let url = format!( - "http://{}:{}", - container.get_host().await?, - container.get_host_port_ipv4(8123).await? - ); - Ok(ClickHouseDatabase { - _container: container, - url, - client: Client::no_redirect_for_test(), - }) -} +use support::{ClickHouseDatabase, TestResult, database}; async fn insert_rows( database: &ClickHouseDatabase, @@ -115,7 +84,7 @@ async fn schema_supports_span_rollups_and_spend_joins( let span = serde_json::from_value(serde_json::json!({ "Timestamp": timestamp, "TraceId": "trace-1", "SpanId": "span-1", "ParentSpanId": "", "ServiceName": "proxy", "SpanName": "request", "Input": "hello world", - "ResourceAttributes": {"litellm.team_id": "team-1", "litellm.api_key_hash": "hash-1"}, + "ResourceAttributes": {"litellm.team_id": "team-1", "litellm.api_key_hash": "hash-1", "litellm.user_id": "exporter-claim"}, "SpanAttributes": {"gen_ai.response.id": "response-1", "gen_ai.usage.input_tokens": "12"} }))?; let spend = serde_json::from_value(serde_json::json!({ @@ -126,9 +95,29 @@ async fn schema_supports_span_rollups_and_spend_joins( insert_rows(&database, "otel_traces", vec![span]).await?; insert_rows(&database, "spend_logs", vec![spend]).await?; let reader = Connection::reader(&database.url, "trace_test")?; + let detail = + litellm_storage_clickhouse::fetch::( + &database.client, + &reader, + &litellm_traces_clickhouse::query::named::SpanDetailParams { + access: litellm_traces_clickhouse::query::named::ReadAccessParams { + all_teams: 0, + user_id: String::new(), + team_ids: vec!["team-1".into()], + }, + trace_id: "trace-1".into(), + trace_ref: String::new(), + span_id: "span-1".into(), + }, + ) + .await?; + assert_eq!(detail.len(), 1); + assert_eq!(detail[0].input, "hello world"); + assert_eq!(detail[0].attributes["gen_ai.response.id"], "response-1"); let list_parameters = BTreeMap::from([ + ("all_teams".into(), Parameter::Integer(0)), + ("user_id".into(), Parameter::Text(String::new())), ("team_ids".into(), Parameter::Strings(vec!["team-1".into()])), - ("api_key_hash".into(), Parameter::Text(String::new())), ( "start_ms".into(), Parameter::Integer(timestamp / 1_000_000 - 1000), @@ -159,8 +148,9 @@ async fn schema_supports_span_rollups_and_spend_joins( "response_ids".into(), Parameter::Strings(vec!["response-1".into()]), ), + ("all_teams".into(), Parameter::Integer(0)), + ("user_id".into(), Parameter::Text(String::new())), ("team_ids".into(), Parameter::Strings(vec!["team-1".into()])), - ("api_key_hash".into(), Parameter::Text(String::new())), ( "start_ms".into(), Parameter::Integer(timestamp / 1_000_000 - 1000), @@ -182,7 +172,7 @@ async fn schema_supports_span_rollups_and_spend_joins( assert_eq!(matched["data"][0]["spend"], 0.125); let body = read_json( &database, - "SELECT o.TeamId, o.ApiKeyHash, o.ObservationType, o.InputPreview, s.spend, \ + "SELECT o.TeamId, o.ApiKeyHash, o.UserId, o.ObservationType, o.InputPreview, s.spend, \ toString(toUnixTimestamp64Nano(o.Timestamp)) AS timestamp_ns, \ toString(toUnixTimestamp64Milli(s.start_time)) AS start_ms \ FROM trace_test.otel_traces o JOIN trace_test.spend_logs s \ @@ -192,7 +182,7 @@ async fn schema_supports_span_rollups_and_spend_joins( assert_eq!( body["data"], serde_json::json!([{ - "TeamId": "team-1", "ApiKeyHash": "hash-1", "ObservationType": "agent", + "TeamId": "team-1", "ApiKeyHash": "hash-1", "UserId": "", "ObservationType": "agent", "InputPreview": "hello world", "spend": 0.125, "timestamp_ns": timestamp.to_string(), "start_ms": (timestamp / 1_000_000).to_string() }]) @@ -269,7 +259,7 @@ async fn insert_rejects_unknown_columns_even_if_url_requests_skipping_them( ]); assert!(matches!( - litellm_traces::insert_rows( + litellm_traces_clickhouse::insert_rows( &database.client, &writer, "trace_test", @@ -277,7 +267,9 @@ async fn insert_rejects_unknown_columns_even_if_url_requests_skipping_them( vec![row] ) .await, - Err(Error::InsertFailed(_)) + Err(Error::Storage( + litellm_storage_clickhouse::Error::InsertFailed(_) + )) )); assert_eq!(table_rows(&database, "otel_traces").await?, 0); Ok(()) @@ -297,7 +289,7 @@ async fn retried_trace_insert_does_not_inflate_rollup( "TeamId": "team-1", "ApiKeyHash": "key-1", "SpanName": "root", "InputTokens": 7 }))?; for _ in 0..2 { - litellm_traces::insert_rows( + litellm_traces_clickhouse::insert_rows( &database.client, &writer, "trace_test", @@ -362,6 +354,194 @@ async fn keyed_rollup_keeps_same_trace_ids_separate_by_api_key( Ok(()) } +#[rstest] +#[tokio::test] +async fn listed_agent_names_preserve_scope_and_cursor( + #[future(awt)] database: TestResult, +) -> TestResult { + let database = database?; + let writer = Connection::writer(&database.url)?; + ensure_schema(&database.client, &writer, "trace_test", 7).await?; + let timestamp = time::OffsetDateTime::now_utc().unix_timestamp_nanos() as i64; + for (team, key, trace, agent, span, parent, framework) in [ + ( + "alpha", + "one", + "shared", + "research_agent", + "root", + "", + "claude-code", + ), + ( + "alpha", + "one", + "shared", + "reviewer", + "child", + "root", + "claude-agent-sdk", + ), + ( + "alpha", + "one", + "shared", + "reviewer", + "repeated", + "root", + "claude-agent-sdk", + ), + ("alpha", "one", "shared", "", "unnamed", "root", ""), + ("alpha", "one", "second", "support_agent", "root", "", ""), + ( + "alpha", + "two", + "shared", + "private_agent", + "root", + "", + "private-sdk", + ), + ( + "beta", + "other", + "shared", + "other_agent", + "root", + "", + "other-sdk", + ), + ] { + insert_rows( + &database, + "otel_traces", + vec![serde_json::from_value(serde_json::json!({ + "Timestamp": timestamp, "TraceId": trace, "SpanId": span, "ParentSpanId": parent, + "ServiceName": "shared-app", "SpanName": span, "AgentName": agent, + "UserId": if key == "one" { "owner" } else { "other" }, + "Framework": framework, "ObservationType": "agent", + "ResourceAttributes": {"litellm.team_id": team, "litellm.api_key_hash": key} + }))?], + ) + .await?; + } + let historical_rows = (0..5000) + .map(|index| { + serde_json::from_value(serde_json::json!({ + "Timestamp": timestamp - 86_400_000_000_000_i64, + "TraceId": "shared", "SpanId": format!("historical-{index}"), + "ParentSpanId": "", "SpanName": "historical", "AgentName": "private_agent", + "ObservationType": "agent", "ServiceName": "shared-app", + "ResourceAttributes": {"litellm.team_id": "alpha", "litellm.api_key_hash": "history"} + })) + }) + .collect::, _>>()?; + insert_rows(&database, "otel_traces", historical_rows).await?; + let connection = Connection::configured(&database.url, "trace_test", "default", "")?; + let parameters = BTreeMap::from([ + ("all_teams".into(), Parameter::Integer(0)), + ("user_id".into(), Parameter::Text("owner".into())), + ("team_ids".into(), Parameter::Strings(vec![])), + ( + "start_ms".into(), + Parameter::Integer(timestamp / 1_000_000 - 1000), + ), + ( + "end_ms".into(), + Parameter::Integer(timestamp / 1_000_000 + 1000), + ), + ("cursor_ms".into(), Parameter::Integer(0)), + ("cursor_trace_id".into(), Parameter::Text(String::new())), + ("limit".into(), Parameter::Integer(1)), + ]); + let first: serde_json::Value = serde_json::from_str( + &execute_named_read( + &database.client, + &connection, + ReadQuery::ListTraces, + ¶meters, + ) + .await?, + )?; + let cursor = first["data"][0]["trace_ref"] + .as_str() + .ok_or("missing cursor")?; + let next_parameters = parameters + .into_iter() + .chain([ + ( + "cursor_ms".into(), + Parameter::Integer(timestamp / 1_000_000), + ), + ("cursor_trace_id".into(), Parameter::Text(cursor.into())), + ]) + .collect(); + let second: serde_json::Value = serde_json::from_str( + &execute_named_read( + &database.client, + &connection, + ReadQuery::ListTraces, + &next_parameters, + ) + .await?, + )?; + assert_eq!( + first["data"].as_array().ok_or("missing first page")?.len(), + 1 + ); + assert_eq!( + second["data"] + .as_array() + .ok_or("missing second page")? + .len(), + 1 + ); + assert_ne!(first["data"][0]["trace_id"], second["data"][0]["trace_id"]); + let names = [&first["data"][0], &second["data"][0]] + .into_iter() + .map(|row| { + ( + row["trace_id"].as_str().unwrap(), + row["agent_names"].clone(), + ) + }) + .collect::>(); + assert_eq!( + names["shared"], + serde_json::json!(["research_agent", "reviewer"]) + ); + assert_eq!(names["second"], serde_json::json!(["support_agent"])); + let frameworks = [&first["data"][0], &second["data"][0]] + .into_iter() + .map(|row| (row["trace_id"].as_str().unwrap(), row["frameworks"].clone())) + .collect::>(); + assert_eq!( + frameworks["shared"], + serde_json::json!(["claude-agent-sdk", "claude-code"]) + ); + assert_eq!(frameworks["second"], serde_json::json!([])); + let counts = [&first["data"][0], &second["data"][0]] + .into_iter() + .map(|row| { + ( + row["trace_id"].as_str().unwrap(), + row["agent_count"].as_u64(), + ) + }) + .collect::>(); + assert_eq!(counts["shared"], Some(3)); + assert_eq!(counts["second"], Some(1)); + for page in [&first, &second] { + assert!( + page["statistics"]["rows_read"] + .as_u64() + .ok_or("missing read statistics")? + < 5000 + ); + } + Ok(()) +} + #[rstest] #[tokio::test] async fn rollup_merges_spans_across_days_without_losing_root_fields( @@ -376,6 +556,7 @@ async fn rollup_merges_spans_across_days_without_losing_root_fields( let root = serde_json::from_value(serde_json::json!({ "Timestamp": day_start - 1_000_000_000, "TraceId": "cross-day", "SpanId": "span-root", "ParentSpanId": "", "ServiceName": "proxy", "SpanName": "root", "Input": "root input", + "AgentName": "lead", "ObservationType": "agent", "StatusCode": "STATUS_CODE_ERROR", "ResourceAttributes": {"litellm.team_id": "team-1"} }))?; @@ -383,6 +564,7 @@ async fn rollup_merges_spans_across_days_without_losing_root_fields( let child = serde_json::from_value(serde_json::json!({ "Timestamp": day_start + 1_000_000_000, "TraceId": "cross-day", "SpanId": "span-child", "ParentSpanId": "span-root", "ServiceName": "proxy", "SpanName": "child", + "AgentName": "researcher", "ObservationType": "agent", "StatusCode": "STATUS_CODE_UNSET", "ResourceAttributes": {"litellm.team_id": "team-1"} }))?; @@ -406,6 +588,34 @@ async fn rollup_merges_spans_across_days_without_losing_root_fields( "RootStatus": "STATUS_CODE_ERROR", "SpanCount": 2 }]) ); + let connection = Connection::configured(&database.url, "trace_test", "default", "")?; + let parameters = BTreeMap::from([ + ("all_teams".into(), Parameter::Integer(0)), + ("user_id".into(), Parameter::Text(String::new())), + ("team_ids".into(), Parameter::Strings(vec!["team-1".into()])), + ( + "start_ms".into(), + Parameter::Integer(day_start / 1_000_000 - 2000), + ), + ("end_ms".into(), Parameter::Integer(day_start / 1_000_000)), + ("cursor_ms".into(), Parameter::Integer(0)), + ("cursor_trace_id".into(), Parameter::Text(String::new())), + ("limit".into(), Parameter::Integer(10)), + ]); + let listed: serde_json::Value = serde_json::from_str( + &execute_named_read( + &database.client, + &connection, + ReadQuery::ListTraces, + ¶meters, + ) + .await?, + )?; + assert_eq!( + listed["data"][0]["agent_names"], + serde_json::json!(["lead", "researcher"]) + ); + assert_eq!(listed["data"][0]["agent_count"], 2); Ok(()) } @@ -553,7 +763,10 @@ async fn schema_statement_timeout_maps_to_transport_error() -> TestResult { ) .await; server.abort(); - assert!(matches!(result, Ok(Err(Error::Transport))), "{result:?}"); + assert!( + matches!(result, Ok(Err(Error::SchemaTransport))), + "{result:?}" + ); Ok(()) } @@ -570,7 +783,7 @@ fn schema_rejects_invalid_configuration(#[case] database: &str, #[case] retentio async fn lens_filters_reads_and_evidence_keep_reused_trace_ids_separate( #[future(awt)] database: TestResult, ) -> TestResult { - use litellm_traces::{LensQuery, Parameter}; + use litellm_traces_clickhouse::{Parameter, ReadQuery}; let database = database?; let writer = Connection::writer(&database.url)?; ensure_schema(&database.client, &writer, "trace_test", 7).await?; @@ -578,7 +791,7 @@ async fn lens_filters_reads_and_evidence_keep_reused_trace_ids_separate( for (key, text) in [("one", "timeout"), ("two", "success")] { insert_rows(&database, "otel_traces", vec![serde_json::from_value(serde_json::json!({ "Timestamp": timestamp, "TraceId": "shared", "SpanId": "root", "ParentSpanId": "", - "ServiceName": "review", "SpanName": "release", "Input": text, + "ServiceName": "review", "SpanName": "release", "Input": text, "UserId": key, "ResourceAttributes": {"litellm.team_id": "team", "litellm.api_key_hash": key, "swarm": "release"} }))?]).await?; } @@ -616,10 +829,10 @@ async fn lens_filters_reads_and_evidence_keep_reused_trace_ids_separate( ("execution_ids".into(), Parameter::Strings(vec![])), ]); let sample: serde_json::Value = serde_json::from_str( - &execute_read( + &execute_named_read( &database.client, &connection, - LensQuery::Sample.sql(), + ReadQuery::Sample, ¶meters, ) .await?, @@ -627,6 +840,43 @@ async fn lens_filters_reads_and_evidence_keep_reused_trace_ids_separate( let rows = sample["data"].as_array().expect("sample rows"); assert_eq!(rows.len(), 2); assert_ne!(rows[0]["trace_ref"], rows[1]["trace_ref"]); + let identity_params = BTreeMap::from([ + ("trace_id".into(), Parameter::Text("shared".into())), + ("all_teams".into(), Parameter::Integer(0)), + ("user_id".into(), Parameter::Text(String::new())), + ("team_ids".into(), Parameter::Strings(vec!["team".into()])), + ]); + let identities: serde_json::Value = serde_json::from_str( + &execute_named_read( + &database.client, + &connection, + ReadQuery::TraceIdentity, + &identity_params, + ) + .await?, + )?; + assert_eq!(identities["data"].as_array().map(Vec::len), Some(2)); + let user_params = identity_params + .into_iter() + .chain([ + ("team_ids".into(), Parameter::Strings(vec![])), + ("user_id".into(), Parameter::Text("one".into())), + ]) + .collect(); + let identity: serde_json::Value = serde_json::from_str( + &execute_named_read( + &database.client, + &connection, + ReadQuery::TraceIdentity, + &user_params, + ) + .await?, + )?; + assert_eq!(identity["data"].as_array().map(Vec::len), Some(1)); + assert!( + rows.iter() + .any(|row| row["trace_ref"] == identity["data"][0]["trace_ref"]) + ); let first_ref = rows[0]["trace_ref"].as_str().expect("reference"); let read_parameters: BTreeMap<_, _> = parameters .into_iter() @@ -640,10 +890,10 @@ async fn lens_filters_reads_and_evidence_keep_reused_trace_ids_separate( ]) .collect(); let content: serde_json::Value = serde_json::from_str( - &execute_read( + &execute_named_read( &database.client, &connection, - LensQuery::Content.sql(), + ReadQuery::Content, &read_parameters, ) .await?, @@ -660,10 +910,10 @@ async fn lens_filters_reads_and_evidence_keep_reused_trace_ids_separate( .chain([("quote".into(), Parameter::Text(opposite.into()))]) .collect(); let evidence: serde_json::Value = serde_json::from_str( - &execute_read( + &execute_named_read( &database.client, &connection, - LensQuery::Evidence.sql(), + ReadQuery::Evidence, &evidence_parameters, ) .await?, @@ -677,7 +927,7 @@ async fn lens_filters_reads_and_evidence_keep_reused_trace_ids_separate( async fn lens_request_sample_does_not_trust_caller_tags( #[future(awt)] database: TestResult, ) -> TestResult { - use litellm_traces::{LensQuery, Parameter}; + use litellm_traces_clickhouse::{Parameter, ReadQuery}; let database = database?; let writer = Connection::writer(&database.url)?; ensure_schema(&database.client, &writer, "trace_test", 7).await?; @@ -712,10 +962,10 @@ async fn lens_request_sample_does_not_trust_caller_tags( ("execution_ids".into(), Parameter::Strings(vec![])), ]); let sample: serde_json::Value = serde_json::from_str( - &execute_read( + &execute_named_read( &database.client, &connection, - LensQuery::Sample.sql(), + ReadQuery::Sample, ¶meters, ) .await?, @@ -742,7 +992,7 @@ async fn lens_selection_pages_without_losing_or_repeating_runs( #[case] page_size: usize, #[case] changing: bool, ) -> TestResult { - use litellm_traces::LensQuery; + use litellm_traces_clickhouse::ReadQuery; let database = database?; ensure_schema( &database.client, @@ -781,10 +1031,10 @@ async fn lens_selection_pages_without_losing_or_repeating_runs( ("selected_team".into(), Parameter::Text(String::new())), ("execution_ids".into(), Parameter::Strings(vec![])), ]); - let body = execute_read( + let body = execute_named_read( &database.client, &connection, - LensQuery::Sample.sql(), + ReadQuery::Sample, ¶meters, ) .await?; @@ -822,7 +1072,7 @@ async fn lens_content_keeps_output_visible_after_long_input( #[future(awt)] database: TestResult, #[case] input_length: usize, ) -> TestResult { - use litellm_traces::LensQuery; + use litellm_traces_clickhouse::ReadQuery; let database = database?; ensure_schema( &database.client, @@ -846,10 +1096,10 @@ async fn lens_content_keeps_output_visible_after_long_input( ("cursor".into(), Parameter::Text(String::new())), ("offset".into(), Parameter::Integer(1)), ]); - let body = execute_read( + let body = execute_named_read( &database.client, &connection, - LensQuery::Content.sql(), + ReadQuery::Content, ¶meters, ) .await?; @@ -868,10 +1118,10 @@ async fn lens_content_keeps_output_visible_after_long_input( let mut recovered = String::new(); for offset in (2..original.len() + 2).step_by(8000) { parameters.insert("offset".into(), Parameter::Integer(offset as i64)); - let body = execute_read( + let body = execute_named_read( &database.client, &connection, - LensQuery::Content.sql(), + ReadQuery::Content, ¶meters, ) .await?; @@ -912,8 +1162,9 @@ async fn trace_error_previews_preserve_paginated_diagnostics( "trace_id".into(), Parameter::Text("diagnostic-trace".into()), ), + ("all_teams".into(), Parameter::Integer(1)), + ("user_id".into(), Parameter::Text(String::new())), ("team_ids".into(), Parameter::Strings(vec![])), - ("api_key_hash".into(), Parameter::Text(String::new())), ("trace_ref".into(), Parameter::Text(String::new())), ]); let body = execute_named_read( @@ -959,10 +1210,8 @@ async fn trace_error_previews_preserve_paginated_diagnostics( } } assert_eq!(recovered, message); - parameters.insert( - "api_key_hash".into(), - Parameter::Text("unrelated-key".into()), - ); + parameters.insert("all_teams".into(), Parameter::Integer(0)); + parameters.insert("user_id".into(), Parameter::Text("unrelated-user".into())); let denied = execute_named_read(&database.client, &reader, ReadQuery::SpanError, ¶meters).await?; assert_eq!( @@ -1004,8 +1253,9 @@ async fn duplicate_span_preview_matches_diagnostic( let parameters = BTreeMap::from([ ("trace_id".into(), Parameter::Text("duplicate-trace".into())), ("span_id".into(), Parameter::Text("duplicate-span".into())), + ("all_teams".into(), Parameter::Integer(1)), + ("user_id".into(), Parameter::Text(String::new())), ("team_ids".into(), Parameter::Strings(vec![])), - ("api_key_hash".into(), Parameter::Text(String::new())), ("trace_ref".into(), Parameter::Text(String::new())), ("error_version".into(), Parameter::Text(String::new())), ("error_offset".into(), Parameter::Integer(0)), @@ -1042,7 +1292,7 @@ fn schema_includes_every_migration_file() -> TestResult { async fn lens_agent_discovery_and_selection_preserve_scope( #[future] database: TestResult, ) -> TestResult { - use litellm_traces::LensQuery; + use litellm_traces_clickhouse::ReadQuery; let database = database.await?; let writer = Connection::writer(&database.url)?; ensure_schema(&database.client, &writer, "trace_test", 7).await?; @@ -1073,10 +1323,10 @@ async fn lens_agent_discovery_and_selection_preserve_scope( ("key_hash".into(), Parameter::Text("one".into())), ]); let agents: serde_json::Value = serde_json::from_str( - &execute_read( + &execute_named_read( &database.client, &connection, - LensQuery::Agents.sql(), + ReadQuery::Agents, &scope_parameters, ) .await?, @@ -1117,10 +1367,10 @@ async fn lens_agent_discovery_and_selection_preserve_scope( ]) .collect::>(); let sample: serde_json::Value = serde_json::from_str( - &execute_read( + &execute_named_read( &database.client, &connection, - LensQuery::Sample.sql(), + ReadQuery::Sample, ¶meters, ) .await?, @@ -1129,10 +1379,10 @@ async fn lens_agent_discovery_and_selection_preserve_scope( assert_eq!(sample["data"][0]["trace_id"], "research"); assert_eq!(sample["data"][0]["span_count"], 2); let available: serde_json::Value = serde_json::from_str( - &execute_read( + &execute_named_read( &database.client, &connection, - LensQuery::Availability.sql(), + ReadQuery::Availability, ¶meters, ) .await?, @@ -1207,8 +1457,9 @@ async fn query_help_discovers_live_schema_and_runs_its_examples( ) .await?; } - let help: serde_json::Value = - serde_json::from_str(&litellm_traces::query_help(&database.client, &reader).await?)?; + let help = serde_json::to_value( + litellm_traces_clickhouse::query_help(&database.client, &reader).await?, + )?; let keys: std::collections::BTreeSet<_> = help .as_object() .ok_or("missing help object")? @@ -1318,7 +1569,8 @@ async fn query_help_discovers_live_schema_and_runs_its_examples( "missing plain-text expression: {expression}" ); let sql = format!("SELECT {expression} AS value FROM spend_logs FINAL"); - let body = litellm_traces::query_sql(&database.client, &reader, &sql).await?; + let body = + litellm_traces_clickhouse::query_sql(&database.client, &reader, &sql).await?; let values: serde_json::Value = serde_json::from_str(&body)?; assert_ne!(values["data"][0]["value"], ""); } @@ -1336,7 +1588,7 @@ async fn query_help_discovers_live_schema_and_runs_its_examples( .collect::>(), std::collections::BTreeSet::from(["name", "sql"]) ); - let body = litellm_traces::query_sql(&database.client, &reader, sql).await?; + let body = litellm_traces_clickhouse::query_sql(&database.client, &reader, sql).await?; let values: serde_json::Value = serde_json::from_str(&body)?; assert_eq!( values["data"].as_array().ok_or("missing data")?.is_empty(), @@ -1392,8 +1644,9 @@ async fn query_help_preserves_schema_and_guide_when_discovery_hits_reader_limits }))).collect::, _>>()?; insert_rows(&database, "otel_traces", spans).await?; let reader = Connection::configured(&database.url, "trace_test", "help_reader", "")?; - let help: serde_json::Value = - serde_json::from_str(&litellm_traces::query_help(&database.client, &reader).await?)?; + let help = serde_json::to_value( + litellm_traces_clickhouse::query_help(&database.client, &reader).await?, + )?; assert_eq!(help["tables"].as_array().ok_or("tables")?.len(), 3); assert!(!help["examples"].as_array().ok_or("examples")?.is_empty()); assert_eq!( @@ -1427,3 +1680,274 @@ async fn query_help_preserves_schema_and_guide_when_discovery_hits_reader_limits } Ok(()) } + +#[rstest] +fn field_definitions_match_serialized_normalized_span() { + use litellm_traces::decode_otlp; + use std::collections::BTreeSet; + let spans = decode_otlp( + br#"{"resourceSpans":[{"scopeSpans":[{"spans":[{"traceId":"11111111111111111111111111111111","spanId":"2222222222222222","name":"root"}]}]}]}"#, + Some("application/json"), + ) + .expect("valid OTLP"); + let fields = &spans[0].normalized; + let serialized = serde_json::to_value(fields).expect("serializable fields"); + let keys: BTreeSet<_> = serialized + .as_object() + .expect("field object") + .keys() + .map(String::as_str) + .collect(); + let mapped: BTreeSet<_> = NORMALIZED_FIELD_DEFINITIONS + .iter() + .map(|field| field.name) + .collect(); + assert_eq!(keys, mapped); +} + +#[rstest] +#[case::own_user("owner", vec![], None, vec!["own"])] +#[case::own_user_and_permitted_team("owner", vec!["permitted"], None, vec!["own", "team"])] +#[case::no_identity("", vec![], None, vec![])] +#[case::legacy_key_without_identity("", vec![], Some("request-key"), vec![])] +#[tokio::test] +async fn named_and_sql_readers_share_request_log_visibility( + #[future(awt)] database: TestResult, + #[case] user: &str, + #[case] teams: Vec<&str>, + #[case] legacy_key: Option<&str>, + #[case] expected: Vec<&str>, +) -> TestResult { + use litellm_traces_clickhouse::query::named::{ + ReadAccessParams, SpendByResponseIds, SpendByResponseIdsParams, + }; + use litellm_traces_clickhouse::{QueryReaders, QueryScope}; + + let database = database?; + let writer = Connection::writer(&database.url)?; + ensure_schema(&database.client, &writer, "trace_test", 7).await?; + let timestamp = time::OffsetDateTime::now_utc().unix_timestamp_nanos() as i64; + let rows = [("own", "unpermitted", "owner", "request-key"), ("team", "permitted", "other", "other-key"), ("foreign", "foreign", "other", "foreign-key")] + .into_iter() + .map(|(id, team, owner, api_key)| serde_json::from_value(serde_json::json!({ + "request_id": id, "response_id": "shared-response", "team_id": team, "user": owner, + "api_key": api_key, "spend": 0.25, "start_time": timestamp / 1_000_000, "end_time": timestamp / 1_000_000, + }))) + .collect::>, _>>()?; + insert_rows(&database, "spend_logs", rows).await?; + let reader = Connection::reader(&database.url, "trace_test")?; + let params = + SpendByResponseIdsParams::from(litellm_traces::query::named::SpendByResponseIdsParams { + access: serde_json::from_value::(serde_json::json!({ + "all_teams": 0, "user_id": user, "team_ids": teams, + "api_key_hash": legacy_key.unwrap_or_default(), + }))?, + response_ids: vec!["shared-response".into()], + start_ms: timestamp / 1_000_000 - 1, + end_ms: timestamp / 1_000_000 + 1, + }); + let spend = + litellm_storage_clickhouse::fetch::(&database.client, &reader, ¶ms) + .await?; + let actual: std::collections::BTreeSet<_> = + spend.iter().map(|row| row.0.request_id.as_str()).collect(); + let expected: std::collections::BTreeSet<_> = expected.into_iter().collect(); + assert_eq!(actual, expected); + let scope = QueryScope::Owned { + user_id: user.into(), + team_ids: teams.into_iter().map(str::to_owned).collect(), + }; + if user.is_empty() && scope.validate().is_err() { + assert!( + QueryReaders::new(writer, "trace_test".into()) + .connection(&database.client, &scope, "secret") + .await + .is_err() + ); + return Ok(()); + } + let scoped = QueryReaders::new(writer, "trace_test".into()) + .connection(&database.client, &scope, "secret") + .await?; + let result: serde_json::Value = serde_json::from_str( + &litellm_traces_clickhouse::query_sql( + &database.client, + &scoped, + "SELECT request_id FROM spend_logs FINAL ORDER BY request_id", + ) + .await?, + )?; + assert_eq!( + result["data"], + serde_json::json!( + expected + .into_iter() + .map(|id| serde_json::json!({"request_id": id})) + .collect::>() + ) + ); + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn rollup_cost_completeness_preserves_missing_ids_and_fails_closed_for_historical_rows( + #[future(awt)] database: TestResult, +) -> TestResult { + let database = database?; + let writer = Connection::writer(&database.url)?; + ensure_schema(&database.client, &writer, "trace_test", 7).await?; + let initial_mutations = mutation_rows(&database).await?; + let timestamp = time::OffsetDateTime::now_utc().unix_timestamp_nanos() as i64; + let rows = [("complete", "llm", "response"), ("complete", "llm", "response"), ("complete", "agent", ""), ("missing", "llm", "response"), ("missing", "llm", ""), ("missing", "agent", "extra-id"), ("mixed", "llm", "mine"), ("mixed", "llm", "other")] + .into_iter().enumerate().map(|(index, (trace, kind, id))| serde_json::from_value(serde_json::json!({ + "Timestamp": timestamp, "TraceId": trace, "SpanId": index.to_string(), "TeamId": "team", "ApiKeyHash": "export", + "UserId": if id == "other" { "other" } else { "owner" }, "ObservationType": kind, "LiteLLMRequestId": id, + }))).collect::>, _>>()?; + insert_rows(&database, "otel_traces", rows).await?; + execute_write(&database, &format!( + "INSERT INTO trace_test.agent_traces_by_key (TeamId, ApiKeyHash, TraceId, StartTs, EndTs, LlmCount, RequestIds) \ + VALUES ('team', 'export', 'historical', fromUnixTimestamp64Nano({timestamp}), fromUnixTimestamp64Nano({timestamp}), 2, ['response', 'non-llm-id'])" + )).await?; + ensure_schema(&database.client, &writer, "trace_test", 7).await?; + let params = litellm_traces_clickhouse::query::named::ListTracesParams::from( + litellm_traces::query::named::ListTracesParams { + access: litellm_traces::query::named::ReadAccessParams { + all_teams: 0, + user_id: "".into(), + team_ids: vec!["team".into()], + }, + start_ms: timestamp / 1_000_000 - 1, + end_ms: timestamp / 1_000_000 + 1, + cursor_ms: 0, + cursor_trace_id: "".into(), + limit: 10, + }, + ); + let reader = Connection::reader(&database.url, "trace_test")?; + let listed = litellm_storage_clickhouse::fetch::< + litellm_traces_clickhouse::query::named::ListTraces, + >(&database.client, &reader, ¶ms) + .await?; + assert_eq!(listed.len(), 4); + let owned_params = litellm_traces_clickhouse::query::named::ListTracesParams::from( + litellm_traces::query::named::ListTracesParams { + access: litellm_traces::query::named::ReadAccessParams { + user_id: "owner".into(), + team_ids: vec![], + all_teams: 0, + }, + ..params.0 + }, + ); + let owned = litellm_storage_clickhouse::fetch::< + litellm_traces_clickhouse::query::named::ListTraces, + >(&database.client, &reader, &owned_params) + .await?; + assert_eq!(owned.len(), 2); + assert!( + owned + .iter() + .all(|row| ["complete", "missing"].contains(&row.0.trace_id.as_str())) + ); + for row in listed { + match row.0.trace_id.as_str() { + "complete" => { + assert_eq!(row.0.user_id, "owner"); + assert_eq!(row.0.request_ids, ["response"]); + assert_eq!(row.0.llm_calls, 2); + } + "missing" | "historical" => assert!(row.0.request_ids.iter().any(String::is_empty)), + "mixed" => assert!(row.0.user_id.is_empty()), + id => panic!("unexpected trace {id}"), + } + } + assert_eq!(mutation_rows(&database).await?, initial_mutations); + Ok(()) +} + +#[rstest] +#[case::admin(1, "", vec![], "own answer")] +#[case::user(0, "owner", vec![], "own answer")] +#[case::team(0, "", vec!["alpha"], "own answer")] +#[case::no_identity(0, "", vec![], "")] +#[tokio::test] +async fn agent_final_answer_preserves_visibility_and_trace_ownership( + #[future(awt)] database: TestResult, + #[case] all_teams: u8, + #[case] user: &str, + #[case] teams: Vec<&str>, + #[case] expected: &str, +) -> TestResult { + use litellm_traces_clickhouse::query::named::{ReadAccessParams, SpanDetail, SpanDetailParams}; + + let database = database?; + let writer = Connection::writer(&database.url)?; + ensure_schema(&database.client, &writer, "trace_test", 7).await?; + let timestamp = time::OffsetDateTime::now_utc().unix_timestamp_nanos() as i64; + let rows = [ + ("alpha", "one", "owner", "root", "", "agent", ""), + ( + "alpha", + "one", + "owner", + "child", + "root", + "llm", + "own answer", + ), + ( + "alpha", + "two", + "other", + "child", + "root", + "llm", + "other key answer", + ), + ( + "beta", + "one", + "other", + "child", + "root", + "llm", + "other team answer", + ), + ] + .into_iter() + .enumerate() + .map(|(index, (team, key, user, span, parent, kind, output))| { + serde_json::from_value(serde_json::json!({ + "Timestamp": timestamp + index as i64, "TraceId": "shared", "SpanId": span, + "ParentSpanId": parent, "TeamId": team, "ApiKeyHash": key, "UserId": user, + "ObservationType": kind, "Input": "prompt", "Output": output, + })) + }) + .collect::, _>>()?; + insert_rows(&database, "otel_traces", rows).await?; + let reader = Connection::reader(&database.url, "trace_test")?; + let details = litellm_storage_clickhouse::fetch::( + &database.client, + &reader, + &SpanDetailParams { + access: ReadAccessParams { + all_teams, + user_id: user.into(), + team_ids: teams.into_iter().map(str::to_owned).collect(), + }, + trace_id: "shared".into(), + trace_ref: String::new(), + span_id: "root".into(), + }, + ) + .await?; + if expected.is_empty() { + assert!(details.is_empty()); + } else { + assert_eq!(details.len(), 1); + assert_eq!(details[0].input, "prompt"); + assert_eq!(details[0].output, expected); + } + Ok(()) +} diff --git a/litellm-rust/crates/traces-clickhouse/tests/queries.rs b/litellm-rust/crates/traces-clickhouse/tests/queries.rs new file mode 100644 index 00000000000..c72255d5e0d --- /dev/null +++ b/litellm-rust/crates/traces-clickhouse/tests/queries.rs @@ -0,0 +1,252 @@ +use std::collections::BTreeMap; + +use litellm_storage_clickhouse::fetch; +use litellm_traces::query::named as contracts; +use litellm_traces_clickhouse::{ + QueryScope, + query::named::{ListTraces, ListTracesParams, TraceSpans, TraceSpansParams}, + query_sql, +}; +use rstest::{fixture, rstest}; +use serde::Deserialize; +use serde_json::Value; + +#[path = "queries/support.rs"] +mod fixtures; +mod support; + +use fixtures::{SeededDatabase, insert_export, migrated_database, seeded_database}; +use support::TestResult; + +#[derive(Clone, Copy, strum::AsRefStr)] +#[strum(serialize_all = "snake_case")] +enum ScopeCase { + Admin, + Team, + OtherTeam, +} + +impl ScopeCase { + fn scope(self) -> QueryScope { + match self { + Self::Admin => QueryScope::All, + Self::Team => QueryScope::Owned { + user_id: String::new(), + team_ids: vec!["team-a".into()], + }, + Self::OtherTeam => QueryScope::Owned { + user_id: String::new(), + team_ids: vec!["team-b".into()], + }, + } + } +} + +#[derive(Deserialize)] +struct QueryResult { + data: Vec, +} + +#[rstest] +#[case::rollups(include_str!("queries/rollups.sql"), include_str!("queries/rollups.expected.json"))] +#[case::costs(include_str!("queries/trace_costs.sql"), include_str!("queries/trace_costs.expected.json"))] +#[case::errors(include_str!("queries/failed_spans.sql"), include_str!("queries/failed_spans.expected.json"))] +#[case::metadata(include_str!("queries/metadata_filters.sql"), include_str!("queries/metadata_filters.expected.json"))] +#[tokio::test] +async fn curated_queries_return_expected_rows( + #[future(awt)] seeded_database: TestResult, + #[case] sql: &str, + #[case] expected_json: &str, + #[values(ScopeCase::Admin, ScopeCase::Team, ScopeCase::OtherTeam)] scope: ScopeCase, +) -> TestResult { + let fixture = seeded_database?; + let reader = fixture + .readers + .connection(&fixture.database.client, &scope.scope(), "fixture-secret") + .await?; + let result: QueryResult = + serde_json::from_str(&query_sql(&fixture.database.client, &reader, sql).await?)?; + let expected: BTreeMap> = serde_json::from_str(expected_json)?; + assert_eq!( + &result.data, + expected + .get(scope.as_ref()) + .ok_or("missing expected scope")?, + "{}: {sql}", + scope.as_ref() + ); + Ok(()) +} + +#[fixture] +fn admin_access() -> TestResult { + Ok(serde_json::from_str(include_str!( + "queries/read_access.json" + ))?) +} + +#[rstest] +#[tokio::test] +async fn typed_queries_read_normalized_spans_and_keep_trace_identities_separate( + #[future(awt)] seeded_database: TestResult, + admin_access: TestResult, +) -> TestResult { + let fixture = seeded_database?; + let reader = fixture + .readers + .connection(&fixture.database.client, &QueryScope::All, "fixture-secret") + .await?; + let params = ListTracesParams::from(contracts::ListTracesParams { + access: admin_access?, + start_ms: 0, + end_ms: i64::MAX / 1_000_000, + cursor_ms: 0, + cursor_trace_id: String::new(), + limit: 10, + }); + let traces = fetch::(&fixture.database.client, &reader, ¶ms).await?; + assert_eq!( + traces + .iter() + .map(|row| row.0.api_key_hash.as_str()) + .collect::>(), + ["key-b", "key-alt", "key-a"] + ); + let trace = &traces[2].0; + assert_eq!( + ( + trace.span_count, + trace.llm_calls, + trace.tool_calls, + trace.error_count + ), + (3, 1, 1, 1) + ); + assert_eq!((trace.input_tokens, trace.output_tokens), (12, 6)); + assert_eq!(trace.input_preview, "Review the change"); + let span_params = TraceSpansParams { + access: params.0.access, + trace_id: trace.trace_id.clone(), + trace_ref: trace.trace_ref.clone(), + }; + let spans = fetch::(&fixture.database.client, &reader, &span_params).await?; + assert_eq!( + spans + .iter() + .map(|row| row.0.name.as_str()) + .collect::>(), + ["review", "completion", "lookup"] + ); + assert!( + spans + .iter() + .all(|row| row.0.api_key_hash == trace.api_key_hash) + ); + assert_eq!( + ( + spans[1].0.kind.as_str(), + spans[1].0.input_tokens, + spans[1].0.output_tokens + ), + ("llm", 12, 6) + ); + assert_eq!(spans[2].0.status_message, "lookup timed out"); + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn typed_trace_cursor_returns_the_next_fixture_trace( + #[future(awt)] seeded_database: TestResult, + admin_access: TestResult, +) -> TestResult { + let fixture = seeded_database?; + let reader = fixture + .readers + .connection(&fixture.database.client, &QueryScope::All, "fixture-secret") + .await?; + let params = ListTracesParams::from(contracts::ListTracesParams { + access: admin_access?, + start_ms: 0, + end_ms: i64::MAX / 1_000_000, + cursor_ms: 0, + cursor_trace_id: String::new(), + limit: 1, + }); + let first = fetch::(&fixture.database.client, &reader, ¶ms).await?; + assert_eq!(first.len(), 1); + assert_eq!(first[0].0.api_key_hash, "key-b"); + let next_params = ListTracesParams::from(contracts::ListTracesParams { + cursor_ms: first[0].0.start_ms, + cursor_trace_id: first[0].0.trace_ref.clone(), + ..params.0 + }); + let next = fetch::(&fixture.database.client, &reader, &next_params).await?; + assert_eq!(next.len(), 1); + assert_eq!(next[0].0.api_key_hash, "key-alt"); + assert_ne!(first[0].0.trace_ref, next[0].0.trace_ref); + Ok(()) +} + +#[rstest] +#[case::authentication_error(include_bytes!("../../traces/tests/fixtures/deeplite_auth_error.json"))] +#[case::swarm(include_bytes!("../../traces/tests/fixtures/deeplite_swarm.json"))] +#[tokio::test] +async fn captured_deeplite_exports_round_trip_through_clickhouse( + #[future(awt)] migrated_database: TestResult, + admin_access: TestResult, + #[case] export: &[u8], +) -> TestResult { + let fixture = migrated_database?; + let decoded = insert_export(&fixture, export, "team-a", "key-a").await?; + let reader = fixture + .readers + .connection(&fixture.database.client, &QueryScope::All, "fixture-secret") + .await?; + let params = TraceSpansParams { + access: admin_access?, + trace_id: decoded[0].trace_id.clone(), + trace_ref: String::new(), + }; + let stored = fetch::(&fixture.database.client, &reader, ¶ms).await?; + assert_eq!(stored.len(), decoded.len()); + let list_params = ListTracesParams::from(contracts::ListTracesParams { + access: params.access, + start_ms: 0, + end_ms: i64::MAX / 1_000_000, + cursor_ms: 0, + cursor_trace_id: String::new(), + limit: 10, + }); + let traces = fetch::(&fixture.database.client, &reader, &list_params).await?; + assert_eq!(traces.len(), 1); + let roots = decoded + .iter() + .filter(|span| span.parent_span_id.is_empty()) + .collect::>(); + assert_eq!(roots.len(), 1); + assert_eq!(traces[0].0.status, roots[0].status_code); + assert_eq!( + traces[0].0.error_count, + decoded + .iter() + .filter(|span| span.status_code == "STATUS_CODE_ERROR") + .count() as u64 + ); + let by_id: BTreeMap<_, _> = stored + .iter() + .map(|row| (row.0.span_id.as_str(), &row.0)) + .collect(); + for span in &decoded { + let row = by_id + .get(span.span_id.as_str()) + .ok_or("missing captured span")?; + assert_eq!(row.parent_span_id, span.parent_span_id); + assert_eq!(row.start_ns as u64, span.start_ns); + assert_eq!(row.duration_ns, span.end_ns - span.start_ns); + assert_eq!(row.input_tokens, span.normalized.input_tokens); + assert_eq!(row.output_tokens, span.normalized.output_tokens); + assert_eq!(row.status, span.status_code); + } + Ok(()) +} diff --git a/litellm-rust/crates/traces-clickhouse/tests/queries/failed_spans.expected.json b/litellm-rust/crates/traces-clickhouse/tests/queries/failed_spans.expected.json new file mode 100644 index 00000000000..df5dfb1c254 --- /dev/null +++ b/litellm-rust/crates/traces-clickhouse/tests/queries/failed_spans.expected.json @@ -0,0 +1,30 @@ +{ + "admin": [ + { + "team": "team-a", + "api_key": "key-a", + "trace_id": "01010101010101010101010101010101", + "span_id": "0303030303030303", + "message": "lookup timed out" + } + ], + "team": [ + { + "team": "team-a", + "api_key": "key-a", + "trace_id": "01010101010101010101010101010101", + "span_id": "0303030303030303", + "message": "lookup timed out" + } + ], + "key": [ + { + "team": "team-a", + "api_key": "key-a", + "trace_id": "01010101010101010101010101010101", + "span_id": "0303030303030303", + "message": "lookup timed out" + } + ], + "other_team": [] +} diff --git a/litellm-rust/crates/traces-clickhouse/tests/queries/failed_spans.sql b/litellm-rust/crates/traces-clickhouse/tests/queries/failed_spans.sql new file mode 100644 index 00000000000..72db132e198 --- /dev/null +++ b/litellm-rust/crates/traces-clickhouse/tests/queries/failed_spans.sql @@ -0,0 +1,5 @@ +SELECT TeamId AS team, ApiKeyHash AS api_key, TraceId AS trace_id, + SpanId AS span_id, StatusMessage AS message +FROM otel_traces +WHERE StatusCode = 'STATUS_CODE_ERROR' +ORDER BY team, api_key, trace_id, span_id diff --git a/litellm-rust/crates/traces-clickhouse/tests/queries/metadata_filters.expected.json b/litellm-rust/crates/traces-clickhouse/tests/queries/metadata_filters.expected.json new file mode 100644 index 00000000000..791948f60a0 --- /dev/null +++ b/litellm-rust/crates/traces-clickhouse/tests/queries/metadata_filters.expected.json @@ -0,0 +1,30 @@ +{ + "admin": [ + { + "team": "team-a", + "api_key": "key-a", + "request_id": "request-a", + "spend": 0.5, + "priority": "high" + } + ], + "team": [ + { + "team": "team-a", + "api_key": "key-a", + "request_id": "request-a", + "spend": 0.5, + "priority": "high" + } + ], + "key": [ + { + "team": "team-a", + "api_key": "key-a", + "request_id": "request-a", + "spend": 0.5, + "priority": "high" + } + ], + "other_team": [] +} diff --git a/litellm-rust/crates/traces-clickhouse/tests/queries/metadata_filters.sql b/litellm-rust/crates/traces-clickhouse/tests/queries/metadata_filters.sql new file mode 100644 index 00000000000..a9c88b33c74 --- /dev/null +++ b/litellm-rust/crates/traces-clickhouse/tests/queries/metadata_filters.sql @@ -0,0 +1,5 @@ +SELECT team_id AS team, api_key, request_id, spend, + JSONExtractString(metadata, 'labels', 'priority') AS priority +FROM spend_logs FINAL +WHERE JSONExtractString(metadata, 'labels', 'priority') = 'high' +ORDER BY team, api_key, request_id diff --git a/litellm-rust/crates/traces-clickhouse/tests/queries/read_access.json b/litellm-rust/crates/traces-clickhouse/tests/queries/read_access.json new file mode 100644 index 00000000000..f0af446092e --- /dev/null +++ b/litellm-rust/crates/traces-clickhouse/tests/queries/read_access.json @@ -0,0 +1,8 @@ +{ + "all_teams": 1, + "user_id": "", + "team_ids": [ + "team-a", + "team-b" + ] +} diff --git a/litellm-rust/crates/traces-clickhouse/tests/queries/rollups.expected.json b/litellm-rust/crates/traces-clickhouse/tests/queries/rollups.expected.json new file mode 100644 index 00000000000..af24543bab3 --- /dev/null +++ b/litellm-rust/crates/traces-clickhouse/tests/queries/rollups.expected.json @@ -0,0 +1,87 @@ +{ + "admin": [ + { + "team": "team-a", + "api_key": "key-a", + "trace_id": "01010101010101010101010101010101", + "name": "review", + "spans": 3, + "llm_calls": 1, + "errors": 1, + "input_tokens": 12, + "output_tokens": 6 + }, + { + "team": "team-a", + "api_key": "key-alt", + "trace_id": "01010101010101010101010101010101", + "name": "alternate", + "spans": 1, + "llm_calls": 0, + "errors": 0, + "input_tokens": 0, + "output_tokens": 0 + }, + { + "team": "team-b", + "api_key": "key-b", + "trace_id": "01010101010101010101010101010101", + "name": "other-team", + "spans": 1, + "llm_calls": 0, + "errors": 0, + "input_tokens": 0, + "output_tokens": 0 + } + ], + "team": [ + { + "team": "team-a", + "api_key": "key-a", + "trace_id": "01010101010101010101010101010101", + "name": "review", + "spans": 3, + "llm_calls": 1, + "errors": 1, + "input_tokens": 12, + "output_tokens": 6 + }, + { + "team": "team-a", + "api_key": "key-alt", + "trace_id": "01010101010101010101010101010101", + "name": "alternate", + "spans": 1, + "llm_calls": 0, + "errors": 0, + "input_tokens": 0, + "output_tokens": 0 + } + ], + "key": [ + { + "team": "team-a", + "api_key": "key-a", + "trace_id": "01010101010101010101010101010101", + "name": "review", + "spans": 3, + "llm_calls": 1, + "errors": 1, + "input_tokens": 12, + "output_tokens": 6 + } + ], + "other_team": [ + { + "team": "team-b", + "api_key": "key-b", + "trace_id": "01010101010101010101010101010101", + "name": "other-team", + "spans": 1, + "llm_calls": 0, + "errors": 0, + "input_tokens": 0, + "output_tokens": 0 + } + ] +} diff --git a/litellm-rust/crates/traces-clickhouse/tests/queries/rollups.sql b/litellm-rust/crates/traces-clickhouse/tests/queries/rollups.sql new file mode 100644 index 00000000000..9c470f0d355 --- /dev/null +++ b/litellm-rust/crates/traces-clickhouse/tests/queries/rollups.sql @@ -0,0 +1,10 @@ +SELECT TeamId AS team, ApiKeyHash AS api_key, TraceId AS trace_id, + ifNull(any(RootName), '') AS name, + toUInt32(sum(SpanCount)) AS spans, + toUInt32(sum(LlmCount)) AS llm_calls, + toUInt32(sum(ErrorCount)) AS errors, + toUInt32(sum(InputTokens)) AS input_tokens, + toUInt32(sum(OutputTokens)) AS output_tokens +FROM agent_traces_by_key +GROUP BY TeamId, ApiKeyHash, TraceId +ORDER BY team, api_key, trace_id diff --git a/litellm-rust/crates/traces-clickhouse/tests/queries/support.rs b/litellm-rust/crates/traces-clickhouse/tests/queries/support.rs new file mode 100644 index 00000000000..1039d761e23 --- /dev/null +++ b/litellm-rust/crates/traces-clickhouse/tests/queries/support.rs @@ -0,0 +1,153 @@ +use std::collections::BTreeMap; + +use litellm_traces::{DecodedSpan, decode_otlp}; +use litellm_traces_clickhouse::{ + Connection, InsertTable, QueryReaders, ensure_schema, insert_rows, +}; +use rstest::fixture; +use serde_json::{Value, json}; + +use crate::support::{ClickHouseDatabase, TestResult, database}; + +pub const DATABASE: &str = "trace_test"; + +pub struct SeededDatabase { + pub database: ClickHouseDatabase, + pub readers: QueryReaders, +} + +#[fixture] +pub async fn migrated_database( + #[future(awt)] database: TestResult, +) -> TestResult { + let database = database?; + let writer = Connection::writer(&database.url)?; + ensure_schema(&database.client, &writer, DATABASE, 7).await?; + for table in ["otel_traces", "agent_traces_by_key", "spend_logs"] { + database + .client + .post(writer.url().clone()) + .body(format!("ALTER TABLE {DATABASE}.{table} REMOVE TTL")) + .send() + .await? + .error_for_status()?; + database + .client + .post(writer.url().clone()) + .body(format!("SYSTEM STOP MERGES {DATABASE}.{table}")) + .send() + .await? + .error_for_status()?; + } + let readers = QueryReaders::new(writer, DATABASE.to_owned()); + Ok(SeededDatabase { database, readers }) +} + +#[fixture] +pub async fn seeded_database( + #[future(awt)] migrated_database: TestResult, +) -> TestResult { + let fixture = migrated_database?; + let writer = Connection::writer(&fixture.database.url)?; + for (contents, team, key) in [ + ( + include_bytes!("../../../traces/tests/fixtures/query_root.json").as_slice(), + "team-a", + "key-a", + ), + ( + include_bytes!("../../../traces/tests/fixtures/query_children.json").as_slice(), + "team-a", + "key-a", + ), + ( + include_bytes!("../../../traces/tests/fixtures/query_alternate.json").as_slice(), + "team-a", + "key-alt", + ), + ( + include_bytes!("../../../traces/tests/fixtures/query_other_team.json").as_slice(), + "team-b", + "key-b", + ), + ] { + insert_export(&fixture, contents, team, key).await?; + } + let spend_rows = include_str!("../fixtures/spend_logs.jsonl") + .lines() + .map(serde_json::from_str::>) + .collect::, _>>()?; + insert_rows( + &fixture.database.client, + &writer, + DATABASE, + InsertTable::SpendLogs, + spend_rows, + ) + .await?; + Ok(fixture) +} + +pub async fn insert_export( + fixture: &SeededDatabase, + contents: &[u8], + team: &str, + key: &str, +) -> TestResult> { + let spans = decode_otlp(contents, Some("application/json"))?; + let writer = Connection::writer(&fixture.database.url)?; + let rows = spans.iter().map(|span| span_row(span, team, key)).collect(); + insert_rows( + &fixture.database.client, + &writer, + DATABASE, + InsertTable::OtelTraces, + rows, + ) + .await?; + Ok(spans) +} + +fn span_row(span: &DecodedSpan, team: &str, key: &str) -> BTreeMap { + BTreeMap::from([ + ("Timestamp".into(), json!(span.start_ns)), + ("TraceId".into(), json!(span.trace_id)), + ("SpanId".into(), json!(span.span_id)), + ("ParentSpanId".into(), json!(span.parent_span_id)), + ("TraceState".into(), json!(span.trace_state)), + ("SpanName".into(), json!(span.name)), + ("SpanKind".into(), json!(span.kind)), + ( + "ServiceName".into(), + json!( + span.resource_attributes + .get("service.name") + .map(String::as_str) + .unwrap_or_default() + ), + ), + ("ResourceAttributes".into(), json!(span.resource_attributes)), + ("ScopeName".into(), json!(span.scope_name)), + ("ScopeVersion".into(), json!(span.scope_version)), + ("SpanAttributes".into(), json!(span.attributes)), + ("Duration".into(), json!(span.end_ns - span.start_ns)), + ("StatusCode".into(), json!(span.status_code)), + ("StatusMessage".into(), json!(span.status_message)), + ("TeamId".into(), json!(team)), + ("ApiKeyHash".into(), json!(key)), + ( + "ObservationType".into(), + json!(span.normalized.observation_type), + ), + ("AgentName".into(), json!(span.normalized.agent_name)), + ("Model".into(), json!(span.normalized.model)), + ( + "LiteLLMRequestId".into(), + json!(span.normalized.litellm_request_id), + ), + ("InputTokens".into(), json!(span.normalized.input_tokens)), + ("OutputTokens".into(), json!(span.normalized.output_tokens)), + ("Input".into(), json!(span.normalized.input)), + ("Output".into(), json!(span.normalized.output)), + ]) +} diff --git a/litellm-rust/crates/traces-clickhouse/tests/queries/trace_costs.expected.json b/litellm-rust/crates/traces-clickhouse/tests/queries/trace_costs.expected.json new file mode 100644 index 00000000000..314981b76b8 --- /dev/null +++ b/litellm-rust/crates/traces-clickhouse/tests/queries/trace_costs.expected.json @@ -0,0 +1,40 @@ +{ + "admin": [ + { + "team": "team-a", + "api_key": "key-a", + "trace_id": "01010101010101010101010101010101", + "spend": 0.5 + }, + { + "team": "team-b", + "api_key": "key-b", + "trace_id": "01010101010101010101010101010101", + "spend": 0.25 + } + ], + "team": [ + { + "team": "team-a", + "api_key": "key-a", + "trace_id": "01010101010101010101010101010101", + "spend": 0.5 + } + ], + "key": [ + { + "team": "team-a", + "api_key": "key-a", + "trace_id": "01010101010101010101010101010101", + "spend": 0.5 + } + ], + "other_team": [ + { + "team": "team-b", + "api_key": "key-b", + "trace_id": "01010101010101010101010101010101", + "spend": 0.25 + } + ] +} diff --git a/litellm-rust/crates/traces-clickhouse/tests/queries/trace_costs.sql b/litellm-rust/crates/traces-clickhouse/tests/queries/trace_costs.sql new file mode 100644 index 00000000000..42ee655229e --- /dev/null +++ b/litellm-rust/crates/traces-clickhouse/tests/queries/trace_costs.sql @@ -0,0 +1,9 @@ +SELECT o.TeamId AS team, o.ApiKeyHash AS api_key, o.TraceId AS trace_id, + sum(s.spend) AS spend +FROM otel_traces AS o +INNER JOIN (SELECT * FROM spend_logs FINAL) AS s + ON o.TeamId = s.team_id + AND o.ApiKeyHash = s.api_key + AND o.LiteLLMRequestId = s.response_id +GROUP BY o.TeamId, o.ApiKeyHash, o.TraceId +ORDER BY team, api_key, trace_id diff --git a/litellm-rust/crates/traces-clickhouse/tests/query_access.rs b/litellm-rust/crates/traces-clickhouse/tests/query_access.rs new file mode 100644 index 00000000000..ef5b76c2097 --- /dev/null +++ b/litellm-rust/crates/traces-clickhouse/tests/query_access.rs @@ -0,0 +1,269 @@ +use std::collections::BTreeMap; + +use litellm_http::Client; +use litellm_traces_clickhouse::{ + Connection, Error, QueryReaders, QueryScope, ensure_schema, query_help, query_sql, +}; +use rstest::{fixture, rstest}; +use serde_json::{Value, json}; +mod support; + +use support::{ClickHouseDatabase, database as start_database}; + +struct Database { + _database: ClickHouseDatabase, + client: Client, + writer: Connection, + readers: QueryReaders, +} + +#[fixture] +async fn database() -> Result> { + let instance = start_database().await?; + let url = instance.url.clone(); + let client = instance.client.clone(); + let writer = Connection::parse(&url)?; + ensure_schema(&client, &writer, "trace_test", 7).await?; + for sql in [ + "INSERT INTO trace_test.otel_traces (TeamId, ApiKeyHash, TraceId, SpanId, Timestamp, SpanAttributes, UserId) VALUES ('team-a', 'key-a1', 'shared-trace', 'a1', now(), map('visible', 'a'), 'owner'), ('team-a', 'key-a2', 'shared-trace', 'a2', now(), map('visible', 'a'), 'other'), ('team-b', 'key-b', 'shared-trace', 'b', now(), map('secret-b', 'b'), 'owner'), ('team-c', 'key-a1', 'shared-trace', 'same-key-foreign', now(), map('visible', 'foreign'), 'other'), ('', 'key-teamless', 'shared-trace', 'teamless', now(), map('visible', 'teamless'), ''), ('', 'key-other', 'shared-trace', 'other-teamless', now(), map('visible', 'other'), '')", + "INSERT INTO trace_test.spend_logs (team_id, api_key, request_id, start_time, end_time, metadata, user) VALUES ('team-a', 'key-a1', 'a1', now(), now(), '{\"visible\":1}', 'owner'), ('team-a', 'key-a2', 'a2', now(), now(), '{\"visible\":1}', 'other'), ('team-b', 'key-b', 'b', now(), now(), '{\"secret_b\":1}', 'owner'), ('team-c', 'key-a1', 'same-key-foreign', now(), now(), '{}', 'other'), ('', 'key-teamless', 'teamless', now(), now(), '{}', ''), ('', 'key-other', 'other-teamless', now(), now(), '{}', '')", + "CREATE TABLE trace_test.private_data (secret String) ENGINE = Memory", + "INSERT INTO trace_test.private_data VALUES ('hidden')", + ] { + let response = client.post(writer.url().clone()).body(sql).send().await?; + assert!(response.status().is_success(), "{}", response.text().await?); + } + let readers = QueryReaders::new(writer.clone(), "trace_test".to_owned()); + Ok(Database { + _database: instance, + client, + writer, + readers, + }) +} + +#[rstest] +#[case::own_user(QueryScope::Owned { user_id: "owner".into(), team_ids: vec![] }, vec!["a1", "b"])] +#[case::own_user_and_permitted_team(QueryScope::Owned { user_id: "owner".into(), team_ids: vec!["team-a".into()] }, vec!["a1", "a2", "b"])] +#[case::quoted_user(QueryScope::Owned { user_id: "owner' OR 1=1 --".into(), team_ids: vec![] }, vec![])] +#[case::team(QueryScope::Owned { user_id: String::new(), team_ids: vec!["team-a".to_owned() ] }, vec!["a1", "a2"])] +#[case::admin(QueryScope::All, vec!["a1", "a2", "b", "other-teamless", "same-key-foreign", "teamless"])] +#[case::quoted_team(QueryScope::Owned { user_id: String::new(), team_ids: vec!["team-a' OR 1=1 --\\".to_owned() ] }, vec![])] +#[tokio::test] +async fn queries_and_help_are_scoped_by_the_database( + #[future(awt)] database: Result>, + #[case] scope: QueryScope, + #[case] expected: Vec<&str>, +) -> Result<(), Box> { + let database = database?; + let reader = database + .readers + .connection(&database.client, &scope, "test-master-secret") + .await?; + let queries = [ + "SELECT SpanId AS id FROM otel_traces ORDER BY id", + "SELECT SpanId AS id FROM trace_test.otel_traces WHERE 1 = 1 ORDER BY id", + "SELECT SpanId AS id FROM merge('trace_test', '^otel_traces$') ORDER BY id", + "WITH source AS (SELECT * FROM trace_test.otel_traces) SELECT SpanId AS id FROM source ORDER BY id", + "SELECT id FROM (SELECT SpanId AS id FROM otel_traces UNION DISTINCT SELECT SpanId AS id FROM trace_test.otel_traces) ORDER BY id", + "SELECT t.SpanId AS id FROM otel_traces t INNER JOIN spend_logs s ON t.SpanId = s.request_id ORDER BY id", + "SELECT request_id AS id FROM spend_logs FINAL ORDER BY id", + ]; + for sql in queries { + let body: Value = serde_json::from_str(&query_sql(&database.client, &reader, sql).await?)?; + assert_eq!( + body["data"], + json!( + expected + .iter() + .map(|id| json!({"id": id})) + .collect::>() + ), + "{sql}" + ); + } + let summary: Value = serde_json::from_str( + &query_sql( + &database.client, + &reader, + "SELECT sum(SpanCount) AS count FROM agent_traces_by_key", + ) + .await?, + )?; + assert_eq!(summary["data"][0]["count"], json!(expected.len())); + let help = serde_json::to_string(&query_help(&database.client, &reader).await?)?; + assert_eq!(help.contains("secret_b"), expected.contains(&"b")); + assert_eq!(help.contains("secret-b"), expected.contains(&"b")); + let recreated = QueryReaders::new(database.writer.clone(), "trace_test".to_owned()); + let repeated = recreated + .connection(&database.client, &scope, "test-master-secret") + .await?; + assert_eq!(reader.url(), repeated.url()); + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn rotating_master_secret_revokes_previous_reader_credentials( + #[future(awt)] database: Result>, +) -> Result<(), Box> { + let database = database?; + let scope = QueryScope::Owned { + user_id: String::new(), + team_ids: vec!["team-a".to_owned()], + }; + let old_reader = database + .readers + .connection(&database.client, &scope, "old-master-secret") + .await?; + let old_result = query_sql( + &database.client, + &old_reader, + "SELECT SpanId AS id FROM otel_traces ORDER BY id", + ) + .await?; + let old_rows: Value = serde_json::from_str(&old_result)?; + assert_eq!(old_rows["data"], json!([{ "id": "a1" }, { "id": "a2" }])); + + let rotated_readers = QueryReaders::new(database.writer.clone(), "trace_test".into()); + let new_reader = rotated_readers + .connection(&database.client, &scope, "new-master-secret") + .await?; + assert!( + query_sql( + &database.client, + &old_reader, + "SELECT SpanId AS id FROM otel_traces ORDER BY id", + ) + .await + .is_err() + ); + let new_result = query_sql( + &database.client, + &new_reader, + "SELECT SpanId AS id FROM otel_traces ORDER BY id", + ) + .await?; + let new_rows: Value = serde_json::from_str(&new_result)?; + assert_eq!(new_rows["data"], json!([{ "id": "a1" }, { "id": "a2" }])); + assert_eq!(old_reader.url().username(), new_reader.url().username()); + assert_ne!(old_reader.url().password(), new_reader.url().password()); + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn managed_reader_rejects_privilege_and_scope_bypasses( + #[future(awt)] database: Result>, +) -> Result<(), Box> { + let database = database?; + let scope = QueryScope::Owned { + user_id: String::new(), + team_ids: vec!["team-a".to_owned()], + }; + let reader = database + .readers + .connection(&database.client, &scope, "test-master-secret") + .await?; + for sql in [ + "INSERT INTO otel_traces (TraceId) VALUES ('injected')", + "DROP TABLE otel_traces", + "SELECT * FROM private_data", + "SELECT * FROM otel_traces SETTINGS readonly = 0", + "SELECT * FROM otel_traces SETTINGS max_memory_usage = 0", + "SELECT * FROM otel_traces SETTINGS max_execution_time = 0", + "CREATE USER scope_bypass", + "CREATE NAMED COLLECTION scope_bypass AS host = 'localhost'", + "BACKUP TABLE otel_traces TO Disk('default', 'scope-bypass')", + "SELECT * FROM url('http://127.0.0.1:1/', 'LineAsString', 'line String')", + "SELECT * FROM remote('127.0.0.1', 'trace_test', 'otel_traces')", + ] { + assert!( + matches!( + query_sql(&database.client, &reader, sql).await, + Err(Error::Storage( + litellm_storage_clickhouse::Error::QueryFailed(_) + )) + ), + "{sql}" + ); + } + let roles: Value = serde_json::from_str( + &query_sql(&database.client, &reader, "SELECT enabledRoles() AS roles").await?, + )?; + assert_eq!(roles["data"], json!([{ "roles": [] }])); + let rows: Value = serde_json::from_str( + &query_sql( + &database.client, + &reader, + "SELECT DISTINCT TeamId FROM otel_traces", + ) + .await?, + )?; + assert_eq!(rows["data"], json!([{ "TeamId": "team-a" }])); + Ok(()) +} + +#[rstest] +#[tokio::test] +async fn provisioning_failure_never_returns_a_writer_connection( + #[future(awt)] database: Result>, +) -> Result<(), Box> { + let database = database?; + let reader = database + .readers + .connection(&database.client, &QueryScope::All, "test-master-secret") + .await?; + let no_provision_privileges = QueryReaders::new(reader, "trace_test".to_owned()); + let result = no_provision_privileges + .connection( + &database.client, + &QueryScope::Owned { + user_id: String::new(), + team_ids: vec!["team-a".to_owned()], + }, + "other-secret", + ) + .await; + assert!(matches!( + result, + Err(Error::Cached(source)) if matches!(source.as_ref(), Error::ProvisionFailed(_)) + )); + assert!(matches!( + database + .readers + .connection(&database.client, &QueryScope::All, "") + .await, + Err(Error::MissingSecret) + )); + assert!(matches!( + database + .readers + .connection( + &database.client, + &QueryScope::Owned { + user_id: String::new(), + team_ids: vec![String::new()] + }, + "test-master-secret" + ) + .await, + Err(Error::InvalidScope) + )); + let permits = (0..8) + .map(|_| database.readers.acquire()) + .collect::, _>>()?; + assert!(matches!(database.readers.acquire(), Err(Error::Busy))); + drop(permits); + assert!(database.readers.acquire().is_ok()); + let rows = litellm_traces_clickhouse::execute_read( + &database.client, + &database.writer, + "SELECT count() AS count FROM trace_test.otel_traces", + &BTreeMap::new(), + ) + .await?; + let rows: Value = serde_json::from_str(&rows)?; + assert_eq!(rows["data"][0]["count"], 6); + Ok(()) +} diff --git a/litellm-rust/crates/traces-clickhouse/tests/support/mod.rs b/litellm-rust/crates/traces-clickhouse/tests/support/mod.rs new file mode 100644 index 00000000000..a27e30c6806 --- /dev/null +++ b/litellm-rust/crates/traces-clickhouse/tests/support/mod.rs @@ -0,0 +1,36 @@ +use litellm_http::Client; +use rstest::fixture; +use testcontainers_modules::{ + clickhouse::ClickHouse, + testcontainers::{ContainerAsync, ImageExt, runners::AsyncRunner}, +}; + +const CLICKHOUSE_TAG: &str = + "26.9.6.6@sha256:eb4870e7ca7ed70c259eebfcfbee6cf797017f6b5436c2926bbbfe3d4d28486e"; + +pub type TestResult = Result>; + +pub struct ClickHouseDatabase { + _container: ContainerAsync, + pub url: String, + pub client: Client, +} + +#[fixture] +pub async fn database() -> TestResult { + let container = ClickHouse::default() + .with_tag(CLICKHOUSE_TAG) + .with_env_var("CLICKHOUSE_SKIP_USER_SETUP", "1") + .start() + .await?; + let url = format!( + "http://{}:{}", + container.get_host().await?, + container.get_host_port_ipv4(8123).await? + ); + Ok(ClickHouseDatabase { + _container: container, + url, + client: Client::no_redirect_for_test(), + }) +} diff --git a/litellm-rust/crates/traces/AGENTS.md b/litellm-rust/crates/traces/AGENTS.md index d0181c53308..a9249f31aa8 100644 --- a/litellm-rust/crates/traces/AGENTS.md +++ b/litellm-rust/crates/traces/AGENTS.md @@ -1,7 +1,6 @@ -- Keep OTLP decoding, trace schema, row encoding and named query selection here. Generic ClickHouse connections and HTTP execution belong in `litellm-storage-clickhouse` -- Keep this crate independent of Python; PyO3 conversion and public Python exceptions belong in `python-bridge` -- Keep the SQL migrations here as the only ClickHouse schema definition, as `migrations/NNNN_description.sql` files embedded by `litellm_migrate::migrate!`; adding a file is the only step -- Use typed query parameters and a dedicated SELECT-only reader with server-side limits -- Keep `config/reader.xml` grants on the database the schema is created in (CLICKHOUSE_DATABASE, default `litellm`) -- Bound insert time and encoded bytes; make retry deduplication behavior explicit for supported ClickHouse versions -- Test storage behavior through the crate's public API against ClickHouse +- Own OTLP decoding, normalization, shared authorization and named query contracts; remain independent of storage and Python +- Never depend on `litellm-traces-clickhouse` or `litellm-storage-clickhouse` +- Preserve decoding limits, normalization precedence and shared resource identity +- Keep ClickHouse schema, row encoding and queries in `litellm-traces-clickhouse`; keep PyO3 conversion in `python-bridge` +- Test decoding and normalization through the public API +- Expose one top-level `Error` enum in `src/error.rs` for decoding and normalization failures diff --git a/litellm-rust/crates/traces/Cargo.toml b/litellm-rust/crates/traces/Cargo.toml index 5e3b41e719a..1949eb2a260 100644 --- a/litellm-rust/crates/traces/Cargo.toml +++ b/litellm-rust/crates/traces/Cargo.toml @@ -6,34 +6,17 @@ license.workspace = true repository.workspace = true [dependencies] -askama.workspace = true -base64.workspace = true -flate2.workspace = true -futures-util.workspace = true -hmac = "0.12.1" indexmap = { version = "2", features = ["serde"] } -moka.workspace = true opentelemetry-proto = { workspace = true, features = ["gen-tonic-messages", "trace", "with-serde"] } prost.workspace = true -time = { workspace = true, features = ["formatting"] } -litellm-http.workspace = true -litellm-migrate.workspace = true -litellm-storage-clickhouse.workspace = true -sha2.workspace = true serde = { workspace = true, features = ["rc"] } serde_json.workspace = true strum.workspace = true thiserror.workspace = true -tokio.workspace = true -url.workspace = true [dev-dependencies] criterion.workspace = true -litellm-http = { workspace = true, features = ["test-support"] } rstest.workspace = true -testcontainers-modules = { version = "0.15.0", features = ["clickhouse"] } -tokio.workspace = true -wiremock.workspace = true [[bench]] name = "resource-fanout" diff --git a/litellm-rust/crates/traces/config/reader.xml b/litellm-rust/crates/traces/config/reader.xml deleted file mode 100644 index 3ab337a13fc..00000000000 --- a/litellm-rust/crates/traces/config/reader.xml +++ /dev/null @@ -1,32 +0,0 @@ - - - - 1 - 10 - 1000 - 4194304 - throw - 268435456 - - - - - - - - - - - - - - ::/0 - litellm_traces_reader - - GRANT SELECT ON litellm.otel_traces - GRANT SELECT ON litellm.agent_traces_by_key - GRANT SELECT ON litellm.spend_logs - - - - diff --git a/litellm-rust/crates/traces/query/list_traces.sql b/litellm-rust/crates/traces/query/list_traces.sql deleted file mode 100644 index c0c1b28aa7f..00000000000 --- a/litellm-rust/crates/traces/query/list_traces.sql +++ /dev/null @@ -1,23 +0,0 @@ -SELECT TraceId AS trace_id, - hex(SHA256(concat(TeamId, char(0), ApiKeyHash, char(0), TraceId))) AS trace_ref, - TeamId AS team_id, ApiKeyHash AS api_key_hash, - ifNull(any(RootName), '') AS name, any(ServiceName) AS service, - ifNull(any(RootInput), '') AS input_preview, ifNull(any(RootStatus), '') AS status, - toUnixTimestamp64Milli(min(StartTs)) AS start_ms, - dateDiff('millisecond', min(StartTs), max(EndTs)) AS duration_ms, - sum(SpanCount) AS span_count, length(groupUniqArrayArray(AgentNames)) AS agent_count, - sum(AgentCount) AS agent_invocations, - sum(LlmCount) AS llm_calls, sum(ToolCount) AS tool_calls, - sum(InputTokens) AS input_tokens, sum(OutputTokens) AS output_tokens, - groupUniqArrayArray(Models) AS models, sum(ErrorCount) AS error_count, - arrayDistinct(groupArrayArray(RequestIds)) AS request_ids -FROM agent_traces_by_key -WHERE (empty({team_ids:Array(String)}) OR TeamId IN {team_ids:Array(String)}) - AND ({api_key_hash:String} = '' OR ApiKeyHash = {api_key_hash:String}) -GROUP BY TeamId, ApiKeyHash, TraceId -HAVING min(StartTs) >= fromUnixTimestamp64Milli({start_ms:Int64}) - AND min(StartTs) < fromUnixTimestamp64Milli({end_ms:Int64}) - AND ({cursor_ms:Int64} = 0 OR (toUnixTimestamp64Milli(min(StartTs)), trace_ref) - < ({cursor_ms:Int64}, {cursor_trace_id:String})) -ORDER BY start_ms DESC, trace_ref DESC -LIMIT {limit:UInt32} diff --git a/litellm-rust/crates/traces/query/span_detail.sql b/litellm-rust/crates/traces/query/span_detail.sql deleted file mode 100644 index 37bb4e8a87e..00000000000 --- a/litellm-rust/crates/traces/query/span_detail.sql +++ /dev/null @@ -1,8 +0,0 @@ -SELECT SpanId AS span_id, Input AS input, Output AS output, SpanAttributes AS attributes -FROM otel_traces -WHERE TraceId = {trace_id:String} AND SpanId = {span_id:String} - AND (empty({team_ids:Array(String)}) OR TeamId IN {team_ids:Array(String)}) - AND ({api_key_hash:String} = '' OR ApiKeyHash = {api_key_hash:String}) - AND ({trace_ref:String} = '' OR - hex(SHA256(concat(TeamId, char(0), ApiKeyHash, char(0), TraceId))) = {trace_ref:String}) -LIMIT 1 diff --git a/litellm-rust/crates/traces/src/error.rs b/litellm-rust/crates/traces/src/error.rs index c677e73e96f..b45ff21140c 100644 --- a/litellm-rust/crates/traces/src/error.rs +++ b/litellm-rust/crates/traces/src/error.rs @@ -1,5 +1,5 @@ #[derive(Debug, thiserror::Error)] -pub enum DecodeError { +pub enum Error { #[error("invalid OTLP trace payload")] InvalidPayload, #[error("OTLP trace payload exceeds the decoding budget")] @@ -9,21 +9,9 @@ pub enum DecodeError { } #[derive(Debug, thiserror::Error)] -pub enum QueryAccessError { - #[error("trace SQL queries require a configured proxy master key")] - MissingSecret, - #[error("invalid trace query scope")] - InvalidScope, - #[error("trace SQL query concurrency limit exceeded")] - Busy, - #[error( - "ClickHouse reader provisioning failed with HTTP status {0}; the configured connection must be allowed to manage users, row policies, and SELECT grants on the trace tables" - )] - ProvisionFailed(u16), - #[error("ClickHouse reader provisioning transport failed")] - ProvisionTransport, - #[error(transparent)] - Storage(#[from] litellm_storage_clickhouse::Error), - #[error(transparent)] - Cached(#[from] std::sync::Arc), -} +#[error("invalid trace query scope")] +pub struct InvalidScope; + +#[derive(Debug, thiserror::Error)] +#[error("unknown ClickHouse read query")] +pub struct InvalidQuery; diff --git a/litellm-rust/crates/traces/src/lib.rs b/litellm-rust/crates/traces/src/lib.rs index 05b56c7ea46..369a0adb403 100644 --- a/litellm-rust/crates/traces/src/lib.rs +++ b/litellm-rust/crates/traces/src/lib.rs @@ -1,25 +1,13 @@ -mod config; mod error; -mod insert; mod normalize; mod otlp; -mod query; +pub mod query; mod query_access; -mod schema; mod shared; -mod sql; -pub use config::Config; -pub use error::{DecodeError, QueryAccessError}; -pub use insert::{InsertRow, InsertTable, encode_rows, insert_rows, insert_shared_rows}; -pub use litellm_storage_clickhouse::{Connection, Error, Parameter, execute_read}; -pub use normalize::{ - NORMALIZED_FIELD_DEFINITIONS, NormalizedFieldDefinition, NormalizedSpan, ObservationType, -}; +pub use error::{Error, InvalidQuery, InvalidScope}; +pub use normalize::{NormalizedSpan, ObservationType}; pub use otlp::{DecodedSpan, decode_otlp}; -pub use query_access::{QueryReaders, QueryScope}; -pub use schema::{ensure_schema, schema_statements}; +pub use query::ReadQuery; +pub use query_access::QueryScope; pub use shared::{Shared, SharedIdentity}; -pub use sql::{LensQuery, ReadQuery, execute_named_read}; - -pub use query::{query_help, query_sql}; diff --git a/litellm-rust/crates/traces/src/normalize/claude_code.rs b/litellm-rust/crates/traces/src/normalize/claude_code.rs new file mode 100644 index 00000000000..513702d2f34 --- /dev/null +++ b/litellm-rust/crates/traces/src/normalize/claude_code.rs @@ -0,0 +1,367 @@ +use std::collections::BTreeMap; + +use serde_json::{Map, Value, json}; + +use super::{NormalizedSpan, ObservationType, SpanNormalizer, attr, first, tokens}; +use crate::{Error, otlp::DecodedEvent}; + +pub(crate) const CLAUDE_CODE_SCOPE: &str = "com.anthropic.claude_code.tracing"; +pub(crate) const CLAUDE_CODE_AGENT: &str = "claude-code"; +const AGENT_SDK_FRAMEWORK: &str = "claude-agent-sdk"; + +pub(super) struct ClaudeCodeNormalizer; + +enum SpanType { + Interaction, + LlmRequest, + Tool, + Other, +} + +fn span_type(name: &str, attributes: &BTreeMap) -> SpanType { + let kind = attr(attributes, "span.type"); + let kind = if kind.is_empty() { + name.strip_prefix("claude_code.").unwrap_or(name) + } else { + kind + }; + match kind { + "interaction" => SpanType::Interaction, + "llm_request" => SpanType::LlmRequest, + "tool" => SpanType::Tool, + _ => SpanType::Other, + } +} + +fn framework(attributes: &BTreeMap) -> &'static str { + if attr(attributes, "query_source_safe") == "sdk" + || attr(attributes, "system_prompt_preview").contains("cc_entrypoint=sdk") + { + AGENT_SDK_FRAMEWORK + } else { + CLAUDE_CODE_AGENT + } +} + +fn split_header(text: &str) -> Option<(&str, &str)> { + let (header, body) = text.strip_prefix('[')?.split_once("]\n")?; + Some((header, body)) +} + +fn without_header<'a>(text: &'a str, prefix: &str) -> &'a str { + split_header(text) + .filter(|(header, _)| header.starts_with(prefix)) + .map_or(text, |(_, body)| body) +} + +fn tool_arguments(attributes: &BTreeMap) -> Option<&str> { + let arguments = without_header(attr(attributes, "tool_input"), "TOOL INPUT"); + serde_json::from_str::>(arguments) + .is_ok() + .then_some(arguments) +} + +fn tool_input(attributes: &BTreeMap) -> String { + if let Some(arguments) = tool_arguments(attributes) { + return arguments.to_owned(); + } + let fields: Map = [ + ("command", "full_command"), + ("file_path", "file_path"), + ("bash_argv0", "bash_argv0"), + ] + .into_iter() + .filter_map(|(key, source)| { + let value = attr(attributes, source); + (!value.is_empty()).then(|| (key.to_owned(), Value::String(value.to_owned()))) + }) + .collect(); + if fields.is_empty() { + String::new() + } else { + Value::Object(fields).to_string() + } +} + +fn tool_output(attributes: &BTreeMap, events: &[DecodedEvent]) -> String { + events + .iter() + .filter(|event| event.name == "tool.output") + .flat_map(|event| { + ["output", "content", "diff"] + .into_iter() + .map(|key| attr(&event.attributes, key)) + }) + .find(|value| !value.is_empty()) + .unwrap_or_else(|| without_header(attr(attributes, "new_context"), "TOOL RESULT")) + .to_owned() +} + +fn context_message(context: &str) -> Value { + let (role, content) = match split_header(context) { + Some(("USER" | "USER PROMPT", body)) => ("user", body), + Some(("ASSISTANT", body)) => ("assistant", body), + Some((header, body)) if header.starts_with("TOOL RESULT") => ("tool", body), + _ => ("user", context), + }; + json!({"role": role, "content": content}) +} + +fn user_prompt(attributes: &BTreeMap) -> String { + let prompt = attr(attributes, "user_prompt"); + if prompt.is_empty() { + String::new() + } else { + json!([{"role": "user", "content": prompt}]).to_string() + } +} + +fn llm_input(attributes: &BTreeMap) -> String { + let messages: Vec = [ + Some(attr(attributes, "system_prompt_preview")) + .filter(|system| !system.is_empty()) + .map(|system| json!({"role": "system", "content": system})), + Some(attr(attributes, "new_context")) + .filter(|context| !context.is_empty()) + .map(context_message), + ] + .into_iter() + .flatten() + .collect(); + if messages.is_empty() { + String::new() + } else { + Value::Array(messages).to_string() + } +} + +fn llm_output(attributes: &BTreeMap) -> String { + let output = attr(attributes, "response.model_output"); + if output.is_empty() { + String::new() + } else { + json!({"role": "assistant", "content": output}).to_string() + } +} + +fn input_tokens(attributes: &BTreeMap) -> Result { + ["input_tokens", "cache_read_tokens", "cache_creation_tokens"] + .into_iter() + .try_fold(0u32, |total, key| { + total + .checked_add(tokens(attributes, key)?) + .ok_or(Error::TokenCountOutOfRange) + }) +} + +impl SpanNormalizer for ClaudeCodeNormalizer { + fn matches(&self, scope_name: &str, _attributes: &BTreeMap) -> bool { + scope_name == CLAUDE_CODE_SCOPE + } + + fn consumed_attributes(&self, attributes: &BTreeMap) -> [&'static str; 2] { + match span_type("", attributes) { + SpanType::Interaction => ["user_prompt", ""], + SpanType::LlmRequest => ["new_context", "response.model_output"], + SpanType::Tool if tool_arguments(attributes).is_some() => ["tool_input", ""], + SpanType::Tool | SpanType::Other => ["", ""], + } + } + + fn display_name(&self, attributes: &BTreeMap) -> Option { + let tool_name = attr(attributes, "tool_name"); + (matches!(span_type("", attributes), SpanType::Tool) && !tool_name.is_empty()) + .then(|| tool_name.to_owned()) + } + + fn normalize( + &self, + name: &str, + _parent_span_id: &str, + attributes: &BTreeMap, + events: &[DecodedEvent], + ) -> Result { + let base = NormalizedSpan { + observation_type: ObservationType::Framework, + agent_name: CLAUDE_CODE_AGENT.to_owned(), + framework: framework(attributes).to_owned(), + litellm_request_id: String::new(), + model: String::new(), + input_tokens: 0, + output_tokens: 0, + input: String::new(), + output: String::new(), + }; + Ok(match span_type(name, attributes) { + SpanType::Interaction => NormalizedSpan { + observation_type: ObservationType::Agent, + input: user_prompt(attributes), + ..base + }, + SpanType::LlmRequest => NormalizedSpan { + observation_type: ObservationType::Llm, + litellm_request_id: first(attributes, "gen_ai.response.id", "request_id") + .to_owned(), + model: first(attributes, "model", "gen_ai.request.model").to_owned(), + input_tokens: input_tokens(attributes)?, + output_tokens: tokens(attributes, "output_tokens")?, + input: llm_input(attributes), + output: llm_output(attributes), + ..base + }, + SpanType::Tool => NormalizedSpan { + observation_type: ObservationType::Tool, + input: tool_input(attributes), + output: tool_output(attributes, events), + ..base + }, + SpanType::Other => base, + }) + } +} + +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use rstest::rstest; + use serde_json::Value; + + use super::{CLAUDE_CODE_SCOPE, ClaudeCodeNormalizer, SpanNormalizer}; + use crate::{Error, normalize::ObservationType, otlp::DecodedEvent}; + + fn attributes(pairs: &[(&str, &str)]) -> BTreeMap { + pairs + .iter() + .map(|(key, value)| ((*key).to_owned(), (*value).to_owned())) + .collect() + } + + #[rstest] + fn tool_without_detailed_input_lists_known_arguments() { + let span = ClaudeCodeNormalizer + .normalize( + "claude_code.tool", + "parent", + &attributes(&[ + ("span.type", "tool"), + ("tool_name", "Bash"), + ("full_command", "git status"), + ("bash_argv0", "git"), + ]), + &[], + ) + .expect("valid span"); + let input: Value = serde_json::from_str(&span.input).expect("argument object"); + assert_eq!(input["command"], "git status"); + assert_eq!(input["bash_argv0"], "git"); + assert!(input.get("file_path").is_none()); + assert!(input.get("role").is_none()); + } + + #[rstest] + fn malformed_tool_input_falls_back_and_stays_in_attributes() { + let attrs = attributes(&[ + ("span.type", "tool"), + ("tool_input", "[TOOL INPUT: Read]\nnot json"), + ("file_path", "/workspace/a.py"), + ]); + let span = ClaudeCodeNormalizer + .normalize("claude_code.tool", "parent", &attrs, &[]) + .expect("valid span"); + let input: Value = serde_json::from_str(&span.input).expect("argument object"); + assert_eq!(input["file_path"], "/workspace/a.py"); + assert!( + !ClaudeCodeNormalizer + .consumed_attributes(&attrs) + .contains(&"tool_input") + ); + } + + #[rstest] + #[case::event_output( + vec![DecodedEvent { name: "tool.output".to_owned(), attributes: attributes(&[("output", "stdout text")]) }], + "stdout text" + )] + #[case::event_diff( + vec![DecodedEvent { name: "tool.output".to_owned(), attributes: attributes(&[("diff", "+line")]) }], + "+line" + )] + #[case::other_event_ignored( + vec![DecodedEvent { name: "other".to_owned(), attributes: attributes(&[("output", "nope")]) }], + "{\"stdout\":\"ctx\"}" + )] + fn tool_output_prefers_event_then_context( + #[case] events: Vec, + #[case] expected: &str, + ) { + let span = ClaudeCodeNormalizer + .normalize( + "claude_code.tool", + "parent", + &attributes(&[ + ("span.type", "tool"), + ("new_context", "[TOOL RESULT: Bash]\n{\"stdout\":\"ctx\"}"), + ]), + &events, + ) + .expect("valid span"); + assert_eq!(span.output, expected); + } + + #[rstest] + fn llm_tool_result_context_becomes_tool_message() { + let span = ClaudeCodeNormalizer + .normalize( + "claude_code.llm_request", + "parent", + &attributes(&[ + ("span.type", "llm_request"), + ("new_context", "[TOOL RESULT: toolu_1]\n1\timport os"), + ]), + &[], + ) + .expect("valid span"); + let input: Value = serde_json::from_str(&span.input).expect("messages"); + assert_eq!(input[0]["role"], "tool"); + assert_eq!(input[0]["content"], "1\timport os"); + assert_eq!(span.output, ""); + assert_eq!(span.framework, "claude-code"); + } + + #[rstest] + fn llm_token_sum_overflow_is_rejected() { + let result = ClaudeCodeNormalizer.normalize( + "claude_code.llm_request", + "parent", + &attributes(&[ + ("span.type", "llm_request"), + ("input_tokens", "4294967295"), + ("cache_read_tokens", "1"), + ]), + &[], + ); + assert!(matches!(result, Err(Error::TokenCountOutOfRange))); + } + + #[rstest] + #[case::span_type_wins("claude_code.tool", "hook", ObservationType::Framework)] + #[case::name_fallback("claude_code.interaction", "", ObservationType::Agent)] + #[case::unknown("claude_code.something_new", "", ObservationType::Framework)] + fn span_type_attribute_then_name_select_the_observation( + #[case] name: &str, + #[case] kind: &str, + #[case] expected: ObservationType, + ) { + let attrs = if kind.is_empty() { + BTreeMap::new() + } else { + attributes(&[("span.type", kind)]) + }; + let span = ClaudeCodeNormalizer + .normalize(name, "parent", &attrs, &[]) + .expect("valid span"); + assert_eq!(span.observation_type, expected); + assert!(ClaudeCodeNormalizer.matches(CLAUDE_CODE_SCOPE, &attrs)); + } +} diff --git a/litellm-rust/crates/traces/src/normalize/genai.rs b/litellm-rust/crates/traces/src/normalize/genai.rs index 15a334ab0fd..f5460a5a014 100644 --- a/litellm-rust/crates/traces/src/normalize/genai.rs +++ b/litellm-rust/crates/traces/src/normalize/genai.rs @@ -1,7 +1,7 @@ use std::collections::BTreeMap; use super::{NormalizedSpan, ObservationType, SpanNormalizer, attr, first, usage_tokens}; -use crate::DecodeError; +use crate::{Error, otlp::DecodedEvent}; pub(super) struct GenAiNormalizer; @@ -30,7 +30,8 @@ impl SpanNormalizer for GenAiNormalizer { _name: &str, parent_span_id: &str, attributes: &BTreeMap, - ) -> Result { + _events: &[DecodedEvent], + ) -> Result { let (input_tokens, output_tokens) = usage_tokens(attributes)?; let observation_type = match attr(attributes, "gen_ai.operation.name") { "invoke_agent" => ObservationType::Agent, @@ -42,6 +43,7 @@ impl SpanNormalizer for GenAiNormalizer { Ok(NormalizedSpan { observation_type, agent_name: attr(attributes, "gen_ai.agent.name").to_owned(), + framework: String::new(), litellm_request_id: attr(attributes, "gen_ai.response.id").to_owned(), model: first(attributes, "gen_ai.request.model", "gen_ai.response.model").to_owned(), input_tokens, diff --git a/litellm-rust/crates/traces/src/normalize/langsmith.rs b/litellm-rust/crates/traces/src/normalize/langsmith.rs index bfe7a2c1796..7e5d84362f0 100644 --- a/litellm-rust/crates/traces/src/normalize/langsmith.rs +++ b/litellm-rust/crates/traces/src/normalize/langsmith.rs @@ -5,7 +5,7 @@ use serde::{Deserialize, Deserializer, Serialize, de::DeserializeOwned}; use serde_json::{Value, ser::Formatter}; use super::{NormalizedSpan, ObservationType, SpanNormalizer, attr, usage_tokens}; -use crate::DecodeError; +use crate::{Error, otlp::DecodedEvent}; pub(super) struct LangSmithNormalizer; @@ -404,13 +404,15 @@ impl SpanNormalizer for LangSmithNormalizer { name: &str, parent_span_id: &str, attributes: &BTreeMap, - ) -> Result { + _events: &[DecodedEvent], + ) -> Result { let (input_tokens, output_tokens) = usage_tokens(attributes)?; let observation_type = span_type(name, parent_span_id, attributes); let io = span_io(observation_type, attributes); Ok(NormalizedSpan { observation_type, agent_name: attr(attributes, "langsmith.metadata.lc_agent_name").to_owned(), + framework: String::new(), litellm_request_id: io.request_id, model: attr(attributes, "gen_ai.request.model").to_owned(), input_tokens, diff --git a/litellm-rust/crates/traces/src/normalize/mod.rs b/litellm-rust/crates/traces/src/normalize/mod.rs index b4d5a4a9d06..8b6cc5fa372 100644 --- a/litellm-rust/crates/traces/src/normalize/mod.rs +++ b/litellm-rust/crates/traces/src/normalize/mod.rs @@ -1,7 +1,7 @@ use std::collections::BTreeMap; -use crate::DecodeError; -use serde::Serialize; +use crate::{Error, otlp::DecodedEvent}; +use serde::{Deserialize, Serialize}; #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)] #[serde(rename_all = "lowercase")] @@ -17,6 +17,7 @@ pub enum ObservationType { pub struct NormalizedSpan { pub observation_type: ObservationType, pub agent_name: String, + pub framework: String, pub litellm_request_id: String, pub model: String, pub input_tokens: u32, @@ -27,68 +28,10 @@ pub struct NormalizedSpan { pub(crate) struct Normalization { pub span: NormalizedSpan, + pub display_name: Option, pub consumed_attributes: [&'static str; 2], } -#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize)] -pub struct NormalizedFieldDefinition { - pub name: &'static str, - pub clickhouse_column: &'static str, - pub clickhouse_type: &'static str, - pub meaning: &'static str, -} - -pub const NORMALIZED_FIELD_DEFINITIONS: [NormalizedFieldDefinition; 8] = [ - NormalizedFieldDefinition { - name: "observation_type", - clickhouse_column: "ObservationType", - clickhouse_type: "LowCardinality(String)", - meaning: "Agent, LLM, tool, chain, or framework span", - }, - NormalizedFieldDefinition { - name: "agent_name", - clickhouse_column: "AgentName", - clickhouse_type: "LowCardinality(String)", - meaning: "Agent associated with this span", - }, - NormalizedFieldDefinition { - name: "litellm_request_id", - clickhouse_column: "LiteLLMRequestId", - clickhouse_type: "String", - meaning: "LiteLLM response ID used to link a span to a spend log", - }, - NormalizedFieldDefinition { - name: "model", - clickhouse_column: "Model", - clickhouse_type: "LowCardinality(String)", - meaning: "Model used by this span", - }, - NormalizedFieldDefinition { - name: "input_tokens", - clickhouse_column: "InputTokens", - clickhouse_type: "UInt32", - meaning: "Input token count", - }, - NormalizedFieldDefinition { - name: "output_tokens", - clickhouse_column: "OutputTokens", - clickhouse_type: "UInt32", - meaning: "Output token count", - }, - NormalizedFieldDefinition { - name: "input", - clickhouse_column: "Input", - clickhouse_type: "String", - meaning: "Normalized input payload", - }, - NormalizedFieldDefinition { - name: "output", - clickhouse_column: "Output", - clickhouse_type: "String", - meaning: "Normalized output payload", - }, -]; - trait SpanNormalizer { fn matches(&self, scope_name: &str, attributes: &BTreeMap) -> bool; fn consumed_attributes(&self, attributes: &BTreeMap) -> [&'static str; 2]; @@ -97,13 +40,20 @@ trait SpanNormalizer { name: &str, parent_span_id: &str, attributes: &BTreeMap, - ) -> Result; + events: &[DecodedEvent], + ) -> Result; + fn display_name(&self, _attributes: &BTreeMap) -> Option { + None + } } +mod claude_code; mod genai; mod langsmith; mod openinference; +use claude_code::ClaudeCodeNormalizer; +pub(crate) use claude_code::{CLAUDE_CODE_AGENT, CLAUDE_CODE_SCOPE}; use genai::GenAiNormalizer; use langsmith::LangSmithNormalizer; use openinference::OpenInferenceNormalizer; @@ -121,40 +71,96 @@ fn first<'a>(attributes: &'a BTreeMap, left: &str, right: &str) } } -fn tokens(attributes: &BTreeMap, key: &str) -> Result { +fn tokens(attributes: &BTreeMap, key: &str) -> Result { let value = attr(attributes, key).trim(); if value.is_empty() { return Ok(0); } match value.parse::() { Ok(number) if (0..=u32::MAX as i128).contains(&number) => Ok(number as u32), - Ok(_) => Err(DecodeError::TokenCountOutOfRange), + Ok(_) => Err(Error::TokenCountOutOfRange), Err(_) if value .trim_start_matches(['+', '-']) .bytes() .all(|byte| byte.is_ascii_digit()) => { - Err(DecodeError::TokenCountOutOfRange) + Err(Error::TokenCountOutOfRange) } Err(_) => Ok(0), } } -fn usage_tokens(attributes: &BTreeMap) -> Result<(u32, u32), DecodeError> { +fn usage_tokens(attributes: &BTreeMap) -> Result<(u32, u32), Error> { Ok(( tokens(attributes, "gen_ai.usage.input_tokens")?, tokens(attributes, "gen_ai.usage.output_tokens")?, )) } +#[derive(Default, Deserialize)] +struct AgentMetadata { + #[serde(default)] + lc_agent_name: String, + #[serde(default)] + ls_integration: String, +} + +fn recorded_agent_name( + name: &str, + attributes: &BTreeMap, + span: &NormalizedSpan, +) -> String { + let explicit = [ + span.agent_name.as_str(), + attr(attributes, "gen_ai.agent.name"), + attr(attributes, "agent.name"), + attr(attributes, "openclaw.agent"), + ] + .into_iter() + .find(|value| !value.is_empty()); + if let Some(value) = explicit { + return value.to_owned(); + } + let metadata = + serde_json::from_str::(attr(attributes, "metadata")).unwrap_or_default(); + if !metadata.lc_agent_name.is_empty() { + return metadata.lc_agent_name; + } + if span.observation_type == ObservationType::Agent { + let node = attr(attributes, "graph.node.id"); + if !node.is_empty() { + return node.to_owned(); + } + if metadata.ls_integration == "langgraph" && name != "LangGraph" && !is_middleware(name) { + return name.to_owned(); + } + } + String::new() +} + +fn is_middleware(name: &str) -> bool { + [ + ".wrap_model_call", + ".wrap_tool_call", + ".before_agent", + ".after_agent", + ".before_model", + ".after_model", + ] + .iter() + .any(|suffix| name.ends_with(suffix)) +} + pub fn normalize( scope_name: &str, name: &str, parent_span_id: &str, attributes: &BTreeMap, -) -> Result { - let normalizers: [&dyn SpanNormalizer; 3] = [ + events: &[DecodedEvent], +) -> Result { + let normalizers: [&dyn SpanNormalizer; 4] = [ + &ClaudeCodeNormalizer, &LangSmithNormalizer, &OpenInferenceNormalizer, &GenAiNormalizer, @@ -163,24 +169,40 @@ pub fn normalize( .into_iter() .find(|normalizer| normalizer.matches(scope_name, attributes)) .expect("GenAI fallback always matches"); + let span = normalizer.normalize(name, parent_span_id, attributes, events)?; + let agent_name = recorded_agent_name(name, attributes, &span); + let observation_type = if !parent_span_id.is_empty() + && scope_name == "openinference.instrumentation.langchain" + && is_middleware(name) + { + ObservationType::Framework + } else { + span.observation_type + }; Ok(Normalization { - span: normalizer.normalize(name, parent_span_id, attributes)?, + span: NormalizedSpan { + agent_name, + observation_type, + ..span + }, + display_name: normalizer.display_name(attributes), consumed_attributes: normalizer.consumed_attributes(attributes), }) } #[cfg(test)] mod tests { - use std::collections::{BTreeMap, BTreeSet}; + use std::collections::BTreeMap; use rstest::rstest; - use super::{NORMALIZED_FIELD_DEFINITIONS, ObservationType, normalize}; + use super::{ObservationType, normalize}; #[rstest] #[case::langsmith("langsmith", [("langsmith.span.kind", "llm"), ("openinference.span.kind", "TOOL")], ObservationType::Llm)] #[case::openinference("other", [("openinference.span.kind", "LLM"), ("gen_ai.operation.name", "execute_tool")], ObservationType::Llm)] #[case::genai("other", [("gen_ai.operation.name", "execute_tool"), ("gen_ai.usage.input_tokens", "7")], ObservationType::Tool)] + #[case::claude_code("com.anthropic.claude_code.tracing", [("span.type", "llm_request"), ("openinference.span.kind", "TOOL")], ObservationType::Llm)] fn convention_dispatch_preserves_precedence( #[case] scope: &str, #[case] attributes: [(&str, &str); 2], @@ -190,7 +212,7 @@ mod tests { .into_iter() .map(|(key, value)| (key.to_owned(), value.to_owned())) .collect(); - let fields = normalize(scope, "step", "parent", &attributes) + let fields = normalize(scope, "step", "parent", &attributes, &[]) .expect("valid tokens") .span; assert_eq!(fields.observation_type, expected); @@ -199,30 +221,11 @@ mod tests { } } - #[rstest] - fn field_definitions_match_serialized_normalized_span() { - let fields = normalize("", "root", "", &BTreeMap::new()) - .expect("valid tokens") - .span; - let serialized = serde_json::to_value(fields).expect("serializable fields"); - let keys: BTreeSet<_> = serialized - .as_object() - .expect("field object") - .keys() - .map(String::as_str) - .collect(); - let mapped: BTreeSet<_> = NORMALIZED_FIELD_DEFINITIONS - .iter() - .map(|field| field.name) - .collect(); - assert_eq!(keys, mapped); - } - #[rstest] fn token_counts_accept_surrounding_whitespace() { let attributes = BTreeMap::from([("gen_ai.usage.input_tokens".to_owned(), " 7 ".to_owned())]); - let fields = normalize("", "root", "", &attributes) + let fields = normalize("", "root", "", &attributes, &[]) .expect("valid tokens") .span; assert_eq!(fields.input_tokens, 7); @@ -234,6 +237,6 @@ mod tests { fn token_counts_outside_storage_range_are_rejected(#[case] value: &str) { let attributes = BTreeMap::from([("gen_ai.usage.input_tokens".to_owned(), value.to_owned())]); - assert!(normalize("", "root", "", &attributes).is_err()); + assert!(normalize("", "root", "", &attributes, &[]).is_err()); } } diff --git a/litellm-rust/crates/traces/src/normalize/openinference.rs b/litellm-rust/crates/traces/src/normalize/openinference.rs index b6156d8f24f..e8c222020a1 100644 --- a/litellm-rust/crates/traces/src/normalize/openinference.rs +++ b/litellm-rust/crates/traces/src/normalize/openinference.rs @@ -1,7 +1,7 @@ use std::collections::BTreeMap; use super::{NormalizedSpan, ObservationType, SpanNormalizer, attr, tokens, usage_tokens}; -use crate::DecodeError; +use crate::{Error, otlp::DecodedEvent}; pub(super) struct OpenInferenceNormalizer; @@ -19,7 +19,8 @@ impl SpanNormalizer for OpenInferenceNormalizer { _name: &str, parent_span_id: &str, attributes: &BTreeMap, - ) -> Result { + _events: &[DecodedEvent], + ) -> Result { let (usage_input, usage_output) = usage_tokens(attributes)?; let observation_type = match attr(attributes, "openinference.span.kind") .to_ascii_uppercase() @@ -34,6 +35,7 @@ impl SpanNormalizer for OpenInferenceNormalizer { Ok(NormalizedSpan { observation_type, agent_name: attr(attributes, "agent.name").to_owned(), + framework: String::new(), litellm_request_id: String::new(), model: attr(attributes, "llm.model_name").to_owned(), input_tokens: if attributes.contains_key("llm.token_count.prompt") { diff --git a/litellm-rust/crates/traces/src/otlp/attributes.rs b/litellm-rust/crates/traces/src/otlp/attributes.rs index 50e063e4582..fb3f627697a 100644 --- a/litellm-rust/crates/traces/src/otlp/attributes.rs +++ b/litellm-rust/crates/traces/src/otlp/attributes.rs @@ -9,7 +9,7 @@ use serde::{ }; use super::limits::{Budget, MAX_ATTRIBUTES}; -use crate::DecodeError; +use crate::Error; struct AttributeWriter<'a> { body: Vec, @@ -32,9 +32,9 @@ impl Write for AttributeWriter<'_> { pub(super) fn attributes( values: Vec, budget: &mut Budget, -) -> Result, DecodeError> { +) -> Result, Error> { if values.len() > MAX_ATTRIBUTES { - return Err(DecodeError::TooLarge); + return Err(Error::TooLarge); } values .into_iter() @@ -59,8 +59,8 @@ pub(super) fn attributes( budget, }; serde_json::to_writer(&mut writer, &AttributeJson(value.as_ref())) - .map_err(|_| DecodeError::TooLarge)?; - String::from_utf8(writer.body).map_err(|_| DecodeError::InvalidPayload)? + .map_err(|_| Error::TooLarge)?; + String::from_utf8(writer.body).map_err(|_| Error::InvalidPayload)? } }; Ok((entry.key, text)) diff --git a/litellm-rust/crates/traces/src/otlp/limits.rs b/litellm-rust/crates/traces/src/otlp/limits.rs index f6b56ccf12d..ca1ac144346 100644 --- a/litellm-rust/crates/traces/src/otlp/limits.rs +++ b/litellm-rust/crates/traces/src/otlp/limits.rs @@ -3,7 +3,7 @@ use std::fmt; use prost::encoding::{DecodeContext, WireType, decode_key, decode_varint, skip_field}; use serde::de::{DeserializeSeed, MapAccess, SeqAccess, Visitor}; -use crate::{DecodeError, Shared}; +use crate::{Error, Shared}; pub(super) const MAX_DEPTH: usize = 32; pub(super) const MAX_NODES: usize = 65_536; @@ -12,7 +12,7 @@ pub(super) const MAX_ATTRIBUTES: usize = 256; pub(super) const MAX_EVENTS: usize = 256; pub(super) const MAX_DECODED_SPAN_BYTES: usize = 16 * 1024 * 1024; -pub(super) fn json_preflight(payload: &[u8]) -> Result<(), DecodeError> { +pub(super) fn json_preflight(payload: &[u8]) -> Result<(), Error> { let mut nodes = 0; let mut exceeded = false; let mut decoder = serde_json::Deserializer::from_slice(payload); @@ -24,9 +24,9 @@ pub(super) fn json_preflight(payload: &[u8]) -> Result<(), DecodeError> { .deserialize(&mut decoder) .and_then(|()| decoder.end()); if exceeded { - return Err(DecodeError::TooLarge); + return Err(Error::TooLarge); } - result.map_err(|_| DecodeError::InvalidPayload) + result.map_err(|_| Error::InvalidPayload) } struct JsonBudget<'a> { @@ -146,7 +146,7 @@ impl MessageKind { } } -pub(super) fn protobuf_preflight(payload: &[u8]) -> Result<(), DecodeError> { +pub(super) fn protobuf_preflight(payload: &[u8]) -> Result<(), Error> { scan_message(payload, MessageKind::Export, 0, &mut 0) } @@ -155,27 +155,27 @@ fn scan_message( kind: MessageKind, depth: usize, nodes: &mut usize, -) -> Result<(), DecodeError> { +) -> Result<(), Error> { if depth > MAX_DEPTH { - return Err(DecodeError::TooLarge); + return Err(Error::TooLarge); } while !payload.is_empty() { *nodes += 1; if *nodes > MAX_NODES { - return Err(DecodeError::TooLarge); + return Err(Error::TooLarge); } - let (tag, wire) = decode_key(&mut payload).map_err(|_| DecodeError::InvalidPayload)?; + let (tag, wire) = decode_key(&mut payload).map_err(|_| Error::InvalidPayload)?; if let (WireType::LengthDelimited, Some(child)) = (wire, kind.child(tag)) { - let length = decode_varint(&mut payload).map_err(|_| DecodeError::InvalidPayload)?; - let length = usize::try_from(length).map_err(|_| DecodeError::InvalidPayload)?; + let length = decode_varint(&mut payload).map_err(|_| Error::InvalidPayload)?; + let length = usize::try_from(length).map_err(|_| Error::InvalidPayload)?; let (message, rest) = payload .split_at_checked(length) - .ok_or(DecodeError::InvalidPayload)?; + .ok_or(Error::InvalidPayload)?; scan_message(message, child, depth + 1, nodes)?; payload = rest; } else { skip_field(wire, tag, &mut payload, DecodeContext::default()) - .map_err(|_| DecodeError::InvalidPayload)?; + .map_err(|_| Error::InvalidPayload)?; } } Ok(()) @@ -194,7 +194,7 @@ impl Budget { &mut self, value: &Shared, allocated_bytes: impl FnOnce(&T) -> usize, - ) -> Result, DecodeError> { + ) -> Result, Error> { let cloned = value.clone(); if !value.shares_storage_with(&cloned) { self.consume(allocated_bytes(value))?; @@ -202,11 +202,8 @@ impl Budget { Ok(cloned) } - pub(super) fn consume(&mut self, bytes: usize) -> Result<(), DecodeError> { - self.remaining = self - .remaining - .checked_sub(bytes) - .ok_or(DecodeError::TooLarge)?; + pub(super) fn consume(&mut self, bytes: usize) -> Result<(), Error> { + self.remaining = self.remaining.checked_sub(bytes).ok_or(Error::TooLarge)?; Ok(()) } } diff --git a/litellm-rust/crates/traces/src/otlp/mod.rs b/litellm-rust/crates/traces/src/otlp/mod.rs index 1beef48fe2c..e11047fe2ca 100644 --- a/litellm-rust/crates/traces/src/otlp/mod.rs +++ b/litellm-rust/crates/traces/src/otlp/mod.rs @@ -6,7 +6,7 @@ mod wire; use serde::Serialize; use std::collections::BTreeMap; -use crate::{DecodeError, NormalizedSpan, Shared}; +use crate::{Error, NormalizedSpan, Shared}; #[derive(Serialize)] pub struct DecodedEvent { @@ -35,10 +35,7 @@ pub struct DecodedSpan { pub consumed_attributes: [&'static str; 2], } -pub fn decode_otlp( - body: &[u8], - content_type: Option<&str>, -) -> Result, DecodeError> { +pub fn decode_otlp(body: &[u8], content_type: Option<&str>) -> Result, Error> { let request = wire::decode(body, content_type)?; span::flatten(request) } diff --git a/litellm-rust/crates/traces/src/otlp/span.rs b/litellm-rust/crates/traces/src/otlp/span.rs index 1c1e53e756c..003ffeb9edc 100644 --- a/litellm-rust/crates/traces/src/otlp/span.rs +++ b/litellm-rust/crates/traces/src/otlp/span.rs @@ -10,9 +10,12 @@ use super::{ attributes::attributes, limits::{Budget, MAX_ATTRIBUTES, MAX_DECODED_SPAN_BYTES, MAX_EVENTS, MAX_SPANS}, }; -use crate::{DecodeError, Shared, normalize::normalize}; +use crate::{ + Error, Shared, + normalize::{CLAUDE_CODE_AGENT, CLAUDE_CODE_SCOPE, normalize}, +}; -pub(super) fn flatten(request: ExportTraceServiceRequest) -> Result, DecodeError> { +pub(super) fn flatten(request: ExportTraceServiceRequest) -> Result, Error> { let mut budget = Budget::new(MAX_DECODED_SPAN_BYTES); let mut spans = Vec::new(); for resource in request.resource_spans { @@ -25,7 +28,7 @@ fn append_resource( resource: ResourceSpans, budget: &mut Budget, spans: &mut Vec, -) -> Result<(), DecodeError> { +) -> Result<(), Error> { let attributes = Shared::new(attributes( resource .resource @@ -44,17 +47,17 @@ fn append_scope( resource: &Shared>, budget: &mut Budget, spans: &mut Vec, -) -> Result<(), DecodeError> { +) -> Result<(), Error> { let scope = scope_spans.scope.unwrap_or_default(); if scope.attributes.len() > MAX_ATTRIBUTES { - return Err(DecodeError::TooLarge); + return Err(Error::TooLarge); } budget.consume(scope.name.len() + scope.version.len())?; let scope_name: Shared = scope.name.into(); let scope_version: Shared = scope.version.into(); for span in scope_spans.spans { if spans.len() >= MAX_SPANS { - return Err(DecodeError::TooLarge); + return Err(Error::TooLarge); } validate_span(&span)?; budget.consume( @@ -82,7 +85,7 @@ fn valid_id(value: &[u8], length: usize) -> bool { value.len() == length && value.iter().any(|byte| *byte != 0) } -fn validate_span(span: &Span) -> Result<(), DecodeError> { +fn validate_span(span: &Span) -> Result<(), Error> { if !valid_id(&span.trace_id, 16) || !valid_id(&span.span_id, 8) || (!span.parent_span_id.is_empty() && !valid_id(&span.parent_span_id, 8)) @@ -94,7 +97,7 @@ fn validate_span(span: &Span) -> Result<(), DecodeError> { .iter() .any(|link| !valid_id(&link.trace_id, 16) || !valid_id(&link.span_id, 8)) { - return Err(DecodeError::InvalidPayload); + return Err(Error::InvalidPayload); } if span.events.len() > MAX_EVENTS || span.links.len() > MAX_EVENTS @@ -108,7 +111,7 @@ fn validate_span(span: &Span) -> Result<(), DecodeError> { .iter() .any(|event| event.attributes.len() > MAX_ATTRIBUTES) { - return Err(DecodeError::TooLarge); + return Err(Error::TooLarge); } Ok(()) } @@ -123,30 +126,62 @@ fn decoded_span( scope_name: &Shared, scope_version: &Shared, budget: &mut Budget, -) -> Result { +) -> Result { let status = span.status.unwrap_or_default(); let parent_span_id = hex_bytes(&span.parent_span_id); let span_attributes = attributes(span.attributes, budget)?; + let events = span + .events + .into_iter() + .map(|event| { + budget.consume(event.name.len() + 96)?; + Ok(DecodedEvent { + name: event.name, + attributes: attributes(event.attributes, budget)?, + }) + }) + .collect::, Error>>()?; let normalization = normalize( scope_name.as_ref(), &span.name, &parent_span_id, &span_attributes, + &events, )?; - let normalized = normalization.span; + let resource_agent_name = resource_attributes + .get("gen_ai.agent.name") + .filter(|name| !name.is_empty()); + let agent_name = match (resource_agent_name, normalization.span.agent_name.as_str()) { + (Some(name), "") => name.clone(), + (Some(name), "hermes-agent") if scope_name.as_ref() == "hermes-otel-plugin" => name.clone(), + (Some(name), CLAUDE_CODE_AGENT) if scope_name.as_ref() == CLAUDE_CODE_SCOPE => name.clone(), + (None, CLAUDE_CODE_AGENT) if scope_name.as_ref() == CLAUDE_CODE_SCOPE => { + resource_attributes + .get("service.name") + .filter(|name| !name.is_empty()) + .map_or_else(|| CLAUDE_CODE_AGENT.to_owned(), Clone::clone) + } + (_, name) => name.to_owned(), + }; + let normalized = crate::normalize::NormalizedSpan { + agent_name, + ..normalization.span + }; budget.consume( normalized.input.len() + normalized.output.len() + normalized.agent_name.len() + + normalized.framework.len() + normalized.litellm_request_id.len() - + normalized.model.len(), + + normalized.model.len() + + normalization.display_name.as_ref().map_or(0, String::len), )?; Ok(DecodedSpan { trace_id: hex_bytes(&span.trace_id), span_id: hex_bytes(&span.span_id), parent_span_id, trace_state: span.trace_state, - name: span.name, + name: normalization.display_name.unwrap_or(span.name), kind: SpanKind::try_from(span.kind) .unwrap_or(SpanKind::Unspecified) .as_str_name() @@ -167,17 +202,7 @@ fn decoded_span( .as_str_name() .to_owned(), status_message: status.message, - events: span - .events - .into_iter() - .map(|event| { - budget.consume(event.name.len() + 96)?; - Ok(DecodedEvent { - name: event.name, - attributes: attributes(event.attributes, budget)?, - }) - }) - .collect::, DecodeError>>()?, + events, normalized, consumed_attributes: normalization.consumed_attributes, }) diff --git a/litellm-rust/crates/traces/src/otlp/wire.rs b/litellm-rust/crates/traces/src/otlp/wire.rs index bac29ba49e4..2330ac12be3 100644 --- a/litellm-rust/crates/traces/src/otlp/wire.rs +++ b/litellm-rust/crates/traces/src/otlp/wire.rs @@ -2,7 +2,7 @@ use opentelemetry_proto::tonic::collector::trace::v1::ExportTraceServiceRequest; use prost::Message; use super::limits::{json_preflight, protobuf_preflight}; -use crate::DecodeError; +use crate::Error; #[derive(strum::EnumString)] #[strum(ascii_case_insensitive)] @@ -19,7 +19,7 @@ enum OtlpMediaType { pub(super) fn decode( body: &[u8], content_type: Option<&str>, -) -> Result { +) -> Result { let media_type = content_type .unwrap_or("application/x-protobuf") .split(';') @@ -27,16 +27,16 @@ pub(super) fn decode( .unwrap_or_default() .trim() .parse::() - .map_err(|_| DecodeError::InvalidPayload)?; + .map_err(|_| Error::InvalidPayload)?; let request = match media_type { OtlpMediaType::Json => { json_preflight(body)?; - serde_json::from_slice(body).map_err(|_| DecodeError::InvalidPayload)? + serde_json::from_slice(body).map_err(|_| Error::InvalidPayload)? } OtlpMediaType::Protobuf => { protobuf_preflight(body)?; - ExportTraceServiceRequest::decode(body).map_err(|_| DecodeError::InvalidPayload)? + ExportTraceServiceRequest::decode(body).map_err(|_| Error::InvalidPayload)? } }; Ok(request) diff --git a/litellm-rust/crates/traces/src/query.rs b/litellm-rust/crates/traces/src/query.rs index b2e8ddd8702..825878159fc 100644 --- a/litellm-rust/crates/traces/src/query.rs +++ b/litellm-rust/crates/traces/src/query.rs @@ -1,346 +1,23 @@ -use std::collections::{BTreeMap, BTreeSet}; +pub mod named; -use futures_util::{ - StreamExt, - stream::{self, TryStreamExt}, -}; -use litellm_http::Client; -use serde::{Deserialize, Serialize}; -use serde_json::{Value, json}; - -use crate::{Connection, Error, NORMALIZED_FIELD_DEFINITIONS, execute_read}; - -mod guide; - -const SAMPLE_ROWS: usize = 200; -const MAX_FIELDS: usize = 200; -const MAX_DEPTH: usize = 16; -const METADATA_SQL: &str = "SELECT metadata FROM spend_logs FINAL \ - WHERE start_time >= now() - INTERVAL 7 DAY AND length(metadata) <= 8192 \ - LIMIT 201"; -const METADATA_SCOPE: &str = "Up to 200 unordered rows from the last 7 days, excluding metadata larger than 8192 bytes; up to 200 paths and 16 levels. Missing paths may exist outside this sample. Array indexes are 1-based and describe sampled positions, not a fixed schema"; -const ATTRIBUTE_SCOPE: &str = "Distinct keys from up to 200 unordered spans in the last 7 days; up to 200 keys per map. Missing keys may exist outside this sample"; - -#[derive(Deserialize)] -struct Rows { - data: Vec, +#[derive(Clone, Copy, Debug, Eq, PartialEq, strum::EnumString, strum::Display, strum::AsRefStr)] +#[strum(serialize_all = "snake_case")] +pub enum ReadQuery { + ListTraces, + TraceSpans, + TraceIdentity, + SpanDetail, + SpanError, + SpendByResponseIds, + Availability, + Agents, + Sample, + Content, + Evidence, } -#[derive(Deserialize)] -struct MetadataRow { - metadata: String, -} - -#[derive(Deserialize)] -struct AttributeRow { - key: String, -} - -#[derive(Clone, Debug, Eq, Ord, PartialEq, PartialOrd, Serialize)] -#[serde(untagged)] -enum PathPart { - Key(String), - Index(usize), -} - -#[derive(Serialize)] -struct MetadataField { - path: Vec, - types: BTreeSet<&'static str>, - expression: String, -} - -#[derive(Deserialize, Serialize)] -struct ColumnSchema { - name: String, - #[serde(rename = "type")] - kind: String, - #[serde(flatten)] - details: BTreeMap, -} - -#[derive(Serialize)] -struct TableSchema { - name: &'static str, - columns: Vec, -} - -#[derive(Serialize)] -struct MetadataCatalog { - table: &'static str, - column: &'static str, - fields: Vec, - sampled_rows: usize, - invalid_json_rows: usize, - truncated: bool, - sample_sql: &'static str, - scope: &'static str, - #[serde(skip_serializing_if = "Option::is_none")] - error: Option, -} - -#[derive(Serialize)] -struct AttributeField { - key: String, - #[serde(rename = "type")] - kind: &'static str, - expression: String, -} - -#[derive(Serialize)] -struct AttributeCatalog { - table: &'static str, - column: &'static str, - fields: Vec, - truncated: bool, - discovery_sql: String, - scope: &'static str, - #[serde(skip_serializing_if = "Option::is_none")] - error: Option, -} - -pub async fn query_sql( - client: &Client, - connection: &Connection, - sql: &str, -) -> Result { - execute_read(client, connection, sql, &BTreeMap::new()).await -} - -async fn rows( - client: &Client, - connection: &Connection, - sql: &str, -) -> Result, Error> { - let body = query_sql(client, connection, sql).await?; - serde_json::from_str::>(&body) - .map(|result| result.data) - .map_err(|_| Error::InvalidResponse) -} - -fn literal(value: &str) -> String { - format!("'{}'", value.replace('\\', "\\\\").replace('\'', "\\'")) -} - -fn metadata_expression(path: &[PathPart]) -> String { - let arguments = path - .iter() - .map(|part| match part { - PathPart::Key(key) => literal(key), - PathPart::Index(index) => index.to_string(), - }) - .collect::>() - .join(", "); - format!("JSONExtractRaw(metadata, {arguments})") -} - -fn discover( - value: &Value, - path: Vec, - fields: &mut BTreeMap, BTreeSet<&'static str>>, -) -> bool { - if path.len() > MAX_DEPTH || (fields.len() >= MAX_FIELDS && !fields.contains_key(&path)) { - return true; - } - if !path.is_empty() { - let kind = match value { - Value::Null => "null", - Value::Bool(_) => "boolean", - Value::Number(number) if number.is_i64() || number.is_u64() => "integer", - Value::Number(_) => "number", - Value::String(_) => "string", - Value::Array(_) => "array", - Value::Object(_) => "object", - }; - fields.entry(path.clone()).or_default().insert(kind); - } - match value { - Value::Object(object) => object.iter().fold(false, |limited, (key, value)| { - let child = path - .iter() - .cloned() - .chain([PathPart::Key(key.clone())]) - .collect(); - discover(value, child, fields) | limited - }), - Value::Array(array) => array - .iter() - .enumerate() - .fold(false, |limited, (index, value)| { - let child = path - .iter() - .cloned() - .chain([PathPart::Index(index + 1)]) - .collect(); - discover(value, child, fields) | limited - }), - _ => false, - } -} - -fn metadata_catalog(sample: &[MetadataRow]) -> MetadataCatalog { - let (fields, limited, invalid_rows) = sample.iter().take(SAMPLE_ROWS).fold( - (BTreeMap::new(), sample.len() > SAMPLE_ROWS, 0), - |(fields, limited, invalid_rows), row| match serde_json::from_str::(&row.metadata) { - Ok(value) => { - let mut fields = fields; - let limited = limited | discover(&value, Vec::new(), &mut fields); - (fields, limited, invalid_rows) - } - Err(_) => (fields, limited, invalid_rows + 1), - }, - ); - let fields: Vec<_> = fields - .into_iter() - .map(|(path, types)| MetadataField { - expression: metadata_expression(&path), - path, - types, - }) - .collect(); - MetadataCatalog { - table: "spend_logs", - column: "metadata", - fields, - sampled_rows: sample.len().min(SAMPLE_ROWS), - invalid_json_rows: invalid_rows, - truncated: limited, - sample_sql: METADATA_SQL, - error: None, - scope: METADATA_SCOPE, - } -} - -pub async fn query_help(client: &Client, connection: &Connection) -> Result { - let tables = stream::iter(["otel_traces", "agent_traces_by_key", "spend_logs"]) - .then(|table| async move { - Ok::<_, Error>(TableSchema { - name: table, - columns: rows::( - client, - connection, - &format!("DESCRIBE TABLE {table}"), - ) - .await?, - }) - }) - .try_collect::>() - .await?; - let metadata = match rows::(client, connection, METADATA_SQL).await { - Ok(sample) => metadata_catalog(&sample), - Err(error) => MetadataCatalog { - error: Some(error.to_string()), - truncated: true, - ..metadata_catalog(&[]) - }, - }; - let attributes = stream::iter(["SpanAttributes", "ResourceAttributes"]) - .then(|column| async move { - let sql = format!( - "SELECT DISTINCT arrayJoin(mapKeys({column})) AS key FROM \ - (SELECT {column} FROM otel_traces WHERE Timestamp >= now() - INTERVAL 7 DAY \ - LIMIT 200) ORDER BY key LIMIT 201" - ); - let (keys, error) = match rows::(client, connection, &sql).await { - Ok(keys) => (keys, None), - Err(error) => (Vec::new(), Some(error.to_string())), - }; - let fields = keys - .iter() - .take(MAX_FIELDS) - .map(|row| AttributeField { - key: row.key.clone(), - kind: "String", - expression: format!("{column}[{}]", literal(&row.key)), - }) - .collect(); - AttributeCatalog { - table: "otel_traces", - column, - fields, - truncated: error.is_some() || keys.len() > MAX_FIELDS, - discovery_sql: sql, - scope: ATTRIBUTE_SCOPE, - error, - } - }) - .collect::>() - .await; - let guide = guide::QueryGuide { - tables: &tables, - normalized_fields: &NORMALIZED_FIELD_DEFINITIONS, - metadata: &metadata, - attributes: &attributes, - }; - Ok(json!({ - "dialect": "ClickHouse SQL", - "access": "Authenticated team scope enforced by ClickHouse row policies; proxy admins can read all teams, while project-bound and teamless keys can read only their own rows", - "response": "ClickHouse JSON envelope: meta, data, rows, statistics; 64-bit integers may be strings", - "tables": tables, - "normalized_fields": NORMALIZED_FIELD_DEFINITIONS.iter().map(|field| json!({ - "table": "otel_traces", "name": field.name, "column": field.clickhouse_column, - "type": field.clickhouse_type, "meaning": field.meaning - })).collect::>(), - "metadata": metadata, - "attributes": attributes, - "relationships": [{ - "left": "otel_traces.LiteLLMRequestId", "right": "spend_logs.response_id", - "additional_predicates": "otel_traces.TeamId = spend_logs.team_id AND otel_traces.ApiKeyHash = spend_logs.api_key", - "meaning": "The normalized ID is the response ID, not request_id. Cached requests can share response_id; joins may return multiple spend rows" - }], - "examples": guide.examples()?, - "gotchas": guide.gotchas()?, - "guide": guide::render(&guide)?, - }).to_string()) -} - -#[cfg(test)] -mod tests { - use super::*; - use rstest::rstest; - - #[rstest] - fn metadata_discovery_preserves_mixed_types_and_reports_invalid_rows() { - let sample = [ - MetadataRow { - metadata: r#"{"x": 1}"#.into(), - }, - MetadataRow { - metadata: r#"{"x": "one"}"#.into(), - }, - MetadataRow { - metadata: "invalid".into(), - }, - ]; - let catalog = json!(metadata_catalog(&sample)); - assert_eq!( - catalog["fields"], - json!([{ - "path": ["x"], "types": ["integer", "string"], "expression": "JSONExtractRaw(metadata, 'x')" - }]) - ); - assert_eq!(catalog["invalid_json_rows"], 1); - assert_eq!(catalog["sampled_rows"], sample.len()); - } - - #[rstest] - #[case::rows(SAMPLE_ROWS + 1, 1)] - #[case::paths(1, MAX_FIELDS + 1)] - fn metadata_discovery_reports_truncation(#[case] row_count: usize, #[case] field_count: usize) { - let metadata: BTreeMap<_, _> = (0..field_count) - .map(|index| (format!("field{index}"), index)) - .collect(); - let sample: Vec<_> = (0..row_count) - .map(|_| MetadataRow { - metadata: json!(metadata).to_string(), - }) - .collect(); - let catalog = json!(metadata_catalog(&sample)); - assert_eq!(catalog["truncated"], true); - assert_eq!(catalog["sampled_rows"], row_count.min(SAMPLE_ROWS)); - assert_eq!( - catalog["fields"].as_array().unwrap().len(), - field_count.min(MAX_FIELDS) - ); +impl ReadQuery { + pub fn parse(value: &str) -> Result { + value.parse().map_err(|_| crate::InvalidQuery) } } diff --git a/litellm-rust/crates/traces/src/query/named.rs b/litellm-rust/crates/traces/src/query/named.rs new file mode 100644 index 00000000000..b45c076c3ba --- /dev/null +++ b/litellm-rust/crates/traces/src/query/named.rs @@ -0,0 +1,151 @@ +use serde::{Deserialize, Serialize}; +use std::collections::BTreeMap; + +#[derive(Debug, Deserialize, Serialize)] +pub struct ReadAccessParams { + pub all_teams: u8, + pub user_id: String, + pub team_ids: Vec, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct ListTracesParams { + #[serde(flatten)] + pub access: ReadAccessParams, + pub start_ms: i64, + pub end_ms: i64, + pub cursor_ms: i64, + pub cursor_trace_id: String, + pub limit: u32, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct ListTracesRow { + pub trace_id: String, + pub trace_ref: String, + pub team_id: String, + pub api_key_hash: String, + pub user_id: String, + pub name: String, + pub service: String, + pub input_preview: String, + pub status: String, + pub start_ms: i64, + pub duration_ms: i64, + pub span_count: u64, + pub agent_count: u64, + pub agent_invocations: u64, + #[serde(default)] + pub agent_names: Vec, + #[serde(default)] + pub frameworks: Vec, + pub llm_calls: u64, + pub tool_calls: u64, + pub input_tokens: u64, + pub output_tokens: u64, + pub models: Vec, + pub error_count: u64, + pub request_ids: Vec, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct TraceSpansParams { + #[serde(flatten)] + pub access: ReadAccessParams, + pub trace_id: String, + pub trace_ref: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct TraceSpansRow { + pub span_id: String, + pub parent_span_id: String, + pub name: String, + #[serde(rename = "type")] + pub kind: String, + pub agent: String, + #[serde(default)] + pub framework: String, + pub status: String, + pub status_message: String, + pub error_truncated: u8, + pub start_ns: i64, + pub duration_ns: u64, + pub service: String, + pub input_preview: String, + pub model: String, + pub input_tokens: u32, + pub output_tokens: u32, + pub litellm_request_id: String, + pub team_id: String, + pub api_key_hash: String, + pub user_id: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct SpanDetailParams { + #[serde(flatten)] + pub access: ReadAccessParams, + pub trace_id: String, + pub trace_ref: String, + pub span_id: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct SpanDetailRow { + pub span_id: String, + pub input: String, + pub output: String, + pub attributes: BTreeMap, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct SpanErrorParams { + #[serde(flatten)] + pub access: ReadAccessParams, + pub trace_id: String, + pub trace_ref: String, + pub span_id: String, + pub error_offset: u64, + pub error_version: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct SpanErrorRow { + pub span_id: String, + pub message: String, + pub total_chars: u64, + pub version: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct SpendByResponseIdsParams { + #[serde(flatten)] + pub access: ReadAccessParams, + pub response_ids: Vec, + pub start_ms: i64, + pub end_ms: i64, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct SpendByResponseIdsRow { + pub request_id: String, + pub response_id: String, + pub team_id: String, + pub api_key: String, + pub user: String, + pub spend: f64, + pub start_ms: i64, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct TraceIdentityParams { + #[serde(flatten)] + pub access: ReadAccessParams, + pub trace_id: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct TraceIdentityRow { + pub trace_ref: String, +} diff --git a/litellm-rust/crates/traces/src/query_access.rs b/litellm-rust/crates/traces/src/query_access.rs index 881b609177d..2f57bd4c0ce 100644 --- a/litellm-rust/crates/traces/src/query_access.rs +++ b/litellm-rust/crates/traces/src/query_access.rs @@ -1,200 +1,28 @@ -use std::{sync::Arc, time::Duration}; - -use hmac::{Hmac, Mac}; -use litellm_http::Client; -use moka::future::Cache; use serde::{Deserialize, Serialize}; -use sha2::{Digest, Sha256}; -use tokio::sync::{OwnedSemaphorePermit, Semaphore}; -use crate::{Connection, QueryAccessError}; - -const TABLES: [&str; 3] = ["otel_traces", "agent_traces_by_key", "spend_logs"]; +use crate::InvalidScope; #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(tag = "kind", rename_all = "snake_case", deny_unknown_fields)] pub enum QueryScope { - Admin, - Team { - team_id: String, - }, - Key { - team_id: String, - api_key_hash: String, + All, + Owned { + user_id: String, + team_ids: Vec, }, } impl QueryScope { - fn validate(&self) -> Result<(), QueryAccessError> { + pub fn validate(&self) -> Result<(), InvalidScope> { match self { - Self::Admin => Ok(()), - Self::Team { team_id } if !team_id.is_empty() => Ok(()), - Self::Key { api_key_hash, .. } if !api_key_hash.is_empty() => Ok(()), - _ => Err(QueryAccessError::InvalidScope), - } - } - - fn predicate(&self, table: &str) -> String { - let (team, key) = if table == "spend_logs" { - ("team_id", "api_key") - } else { - ("TeamId", "ApiKeyHash") - }; - match self { - Self::Admin => "1".to_owned(), - Self::Team { team_id } => format!("{team} = {}", literal(team_id)), - Self::Key { - team_id, - api_key_hash, - } => format!( - "{team} = {} AND {key} = {}", - literal(team_id), - literal(api_key_hash) - ), + Self::All => Ok(()), + Self::Owned { user_id, team_ids } + if (!user_id.is_empty() || !team_ids.is_empty()) + && team_ids.iter().all(|team| !team.is_empty()) => + { + Ok(()) + } + _ => Err(InvalidScope), } } } - -#[derive(Clone)] -pub struct QueryReaders { - writer: Connection, - database: String, - readers: Cache, - slots: Arc, -} - -impl QueryReaders { - pub fn new(writer: Connection, database: String) -> Self { - Self { - writer, - database, - readers: Cache::builder().max_capacity(1024).build(), - slots: Arc::new(Semaphore::new(8)), - } - } - - pub fn acquire(&self) -> Result { - self.slots - .clone() - .try_acquire_owned() - .map_err(|_| QueryAccessError::Busy) - } - - pub async fn connection( - &self, - client: &Client, - scope: &QueryScope, - secret: &str, - ) -> Result { - scope.validate()?; - if secret.is_empty() { - return Err(QueryAccessError::MissingSecret); - } - let identity = serde_json::to_vec(&("litellm_trace_reader_v1", &self.database, scope)) - .map_err(|_| QueryAccessError::InvalidScope)?; - let user = format!("litellm_traces_{:x}", Sha256::digest(&identity)); - let password = credential(secret, b"password", &identity)?; - self.readers - .try_get_with( - user.clone(), - self.provision(client, scope, &user, &password), - ) - .await - .map_err(QueryAccessError::Cached) - } - - async fn provision( - &self, - client: &Client, - scope: &QueryScope, - user: &str, - password: &str, - ) -> Result { - let database = &self.database; - if database.is_empty() - || !database - .bytes() - .all(|c| c.is_ascii_alphanumeric() || c == b'_') - { - return Err(QueryAccessError::InvalidScope); - } - let password_hash = format!("{:x}", Sha256::digest(password)); - self.execute( - client, - format!( - "CREATE USER IF NOT EXISTS {user} IDENTIFIED WITH sha256_hash BY '{password_hash}' \ - SETTINGS readonly = 1 CONST, max_execution_time = 10 CONST, \ - max_result_rows = 1000 CONST, max_result_bytes = 4194304 CONST, \ - result_overflow_mode = 'throw' CONST, max_memory_usage = 268435456 CONST, \ - max_threads = 2 CONST, max_concurrent_queries_for_user = 8 CONST" - ), - ) - .await?; - self.execute( - client, - format!("ALTER USER {user} IDENTIFIED WITH sha256_hash BY '{password_hash}'"), - ) - .await?; - for table in TABLES { - let predicate = scope.predicate(table); - self.execute( - client, - format!( - "CREATE ROW POLICY IF NOT EXISTS {user}_allow ON `{database}`.{table} \ - USING 1 TO {user}" - ), - ) - .await?; - self.execute( - client, - format!( - "CREATE ROW POLICY IF NOT EXISTS {user}_scope ON `{database}`.{table} \ - AS RESTRICTIVE USING {predicate} TO {user}" - ), - ) - .await?; - } - for table in TABLES { - self.execute( - client, - format!("GRANT SELECT ON `{database}`.{table} TO {user}"), - ) - .await?; - } - Connection::configured( - &self.writer.url()[..url::Position::AfterPath], - database, - user, - password, - ) - .map_err(QueryAccessError::Storage) - } - - async fn execute(&self, client: &Client, sql: String) -> Result<(), QueryAccessError> { - let response = client - .post(self.writer.url().clone()) - .timeout(Duration::from_secs(15)) - .body(sql) - .send() - .await - .map_err(|_| QueryAccessError::ProvisionTransport)?; - if !response.status().is_success() { - return Err(QueryAccessError::ProvisionFailed( - response.status().as_u16(), - )); - } - Ok(()) - } -} - -fn credential(secret: &str, purpose: &[u8], identity: &[u8]) -> Result { - let mut mac = Hmac::::new_from_slice(secret.as_bytes()) - .map_err(|_| QueryAccessError::MissingSecret)?; - mac.update(purpose); - mac.update(identity); - Ok(format!("{:x}", mac.finalize().into_bytes())) -} - -fn literal(value: &str) -> String { - format!("'{}'", value.replace('\\', "\\\\").replace('\'', "\\'")) -} diff --git a/litellm-rust/crates/traces/src/schema.rs b/litellm-rust/crates/traces/src/schema.rs deleted file mode 100644 index c52db1523cc..00000000000 --- a/litellm-rust/crates/traces/src/schema.rs +++ /dev/null @@ -1,70 +0,0 @@ -use litellm_http::Client; -use litellm_migrate::Migration; -use std::time::Duration; - -use crate::Connection; -use crate::Error; - -const SCHEMA_REQUEST_TIMEOUT: Duration = Duration::from_secs(30); - -const MIGRATIONS: &[Migration] = litellm_migrate::migrate!("migrations"); - -pub fn schema_statements(database: &str, retention_days: u32) -> Result, Error> { - if database.is_empty() - || !database - .bytes() - .all(|c| c.is_ascii_alphanumeric() || c == b'_') - || retention_days == 0 - { - return Err(Error::InvalidSchema); - } - let database = format!("`{database}`"); - Ok( - std::iter::once(format!("CREATE DATABASE IF NOT EXISTS {database}")) - .chain(MIGRATIONS.iter().map(|migration| { - migration - .sql - .replace("{database}", &database) - .replace("{retention_days}", &retention_days.to_string()) - })) - .collect(), - ) -} - -pub async fn ensure_schema( - client: &Client, - connection: &Connection, - database: &str, - retention_days: u32, -) -> Result<(), Error> { - ensure_schema_with_timeout( - client, - connection, - database, - retention_days, - SCHEMA_REQUEST_TIMEOUT, - ) - .await -} - -async fn ensure_schema_with_timeout( - client: &Client, - connection: &Connection, - database: &str, - retention_days: u32, - request_timeout: Duration, -) -> Result<(), Error> { - for statement in schema_statements(database, retention_days)? { - let response = client - .post(connection.url().clone()) - .timeout(request_timeout) - .body(statement) - .send() - .await - .map_err(|_| Error::Transport)?; - if !response.status().is_success() { - return Err(Error::SchemaFailed(response.status().as_u16())); - } - } - Ok(()) -} diff --git a/litellm-rust/crates/traces/src/sql.rs b/litellm-rust/crates/traces/src/sql.rs deleted file mode 100644 index 51632066fdd..00000000000 --- a/litellm-rust/crates/traces/src/sql.rs +++ /dev/null @@ -1,76 +0,0 @@ -use std::collections::BTreeMap; - -use litellm_http::Client; - -use crate::{Connection, Error, Parameter, execute_read}; - -pub enum ReadQuery { - ListTraces, - TraceSpans, - SpanDetail, - SpanError, - SpendByResponseIds, -} - -impl ReadQuery { - pub fn parse(value: &str) -> Result { - match value { - "list_traces" => Ok(Self::ListTraces), - "trace_spans" => Ok(Self::TraceSpans), - "span_detail" => Ok(Self::SpanDetail), - "span_error" => Ok(Self::SpanError), - "spend_by_response_ids" => Ok(Self::SpendByResponseIds), - _ => Err(Error::InvalidQuery), - } - } - - fn sql(&self) -> &'static str { - match self { - Self::ListTraces => include_str!("../query/list_traces.sql"), - Self::TraceSpans => include_str!("../query/trace_spans.sql"), - Self::SpanDetail => include_str!("../query/span_detail.sql"), - Self::SpanError => include_str!("../query/span_error.sql"), - Self::SpendByResponseIds => include_str!("../query/spend_by_response_ids.sql"), - } - } -} - -#[derive(Clone, Copy)] -pub enum LensQuery { - Availability, - Agents, - Sample, - Content, - Evidence, -} - -impl LensQuery { - pub fn parse(name: &str) -> Result { - match name { - "availability" => Ok(Self::Availability), - "agents" => Ok(Self::Agents), - "sample" => Ok(Self::Sample), - "content" => Ok(Self::Content), - "evidence" => Ok(Self::Evidence), - _ => Err(Error::InvalidQuery), - } - } - pub fn sql(self) -> &'static str { - match self { - Self::Availability => include_str!("../query/lens_availability.sql"), - Self::Agents => include_str!("../query/lens_agents.sql"), - Self::Sample => include_str!("../query/lens_sample.sql"), - Self::Content => include_str!("../query/lens_content.sql"), - Self::Evidence => include_str!("../query/lens_evidence.sql"), - } - } -} - -pub async fn execute_named_read( - client: &Client, - connection: &Connection, - query: ReadQuery, - parameters: &BTreeMap, -) -> Result { - execute_read(client, connection, query.sql(), parameters).await -} diff --git a/litellm-rust/crates/traces/tests/fixtures/deeplite_auth_error.json b/litellm-rust/crates/traces/tests/fixtures/deeplite_auth_error.json new file mode 100644 index 00000000000..ef207be9358 --- /dev/null +++ b/litellm-rust/crates/traces/tests/fixtures/deeplite_auth_error.json @@ -0,0 +1,1614 @@ +{ + "resourceSpans": [ + { + "resource": { + "attributes": [ + { + "key": "telemetry.sdk.language", + "value": { + "stringValue": "python" + } + }, + { + "key": "telemetry.sdk.name", + "value": { + "stringValue": "opentelemetry" + } + }, + { + "key": "telemetry.sdk.version", + "value": { + "stringValue": "1.45.0" + } + }, + { + "key": "service.instance.id", + "value": { + "stringValue": "cedd2594-abc5-48f2-a9d1-b892921fadfd" + } + }, + { + "key": "service.name", + "value": { + "stringValue": "deeplite" + } + } + ] + }, + "scopeSpans": [ + { + "scope": { + "name": "langsmith" + }, + "spans": [ + { + "traceId": "7b787d5c997d768a725a3a2f0278e857", + "spanId": "93886a81b22f3dab", + "parentSpanId": "11f6195c25249ab1", + "name": "__start__", + "kind": 1, + "startTimeUnixNano": "1790967512073597952", + "endTimeUnixNano": "1790967512075591936", + "attributes": [ + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "__start__" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langgraph" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "0" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "__start__" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"__start__\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"__start__\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "__start__:af3b0b0a-9397-3b9d-0717-b11174443ca9" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "graph:step:0, langsmith:hidden" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W3sicm9sZSI6InVzZXIiLCJjb250ZW50IjoiRm9yIGEgc3ludGhldGljIHRyYWNpbmcgZml4dHVyZSwgZXhwbGFpbiBQeXRob24gdHVwbGUgdmVyc3VzIGxpc3QgaW1tdXRhYmlsaXR5IGluIGF0IG1vc3QgMTIwIHdvcmRzLiBVc2Ugb25seSBvZmZpY2lhbCBQeXRob24gZG9jdW1lbnRhdGlvbiBpZiB5b3Ugc2VhcmNoLiBIYXZlIHRoZSBza2VwdGljIGFuZCB2ZXJpZmllciBjaGVjayBpdCwgdGhlbiBoYXZlIHRoZSBlZGl0b3Igd3JpdGUgL2Fuc3dlci5tZC4gS2VlcCB0aGUgcnVuIHNob3J0IGFuZCBkbyBub3QgYXNrIGZvbGxvdy11cCBxdWVzdGlvbnMuIn1dfQ==" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W3sicm9sZSI6InVzZXIiLCJjb250ZW50IjoiRm9yIGEgc3ludGhldGljIHRyYWNpbmcgZml4dHVyZSwgZXhwbGFpbiBQeXRob24gdHVwbGUgdmVyc3VzIGxpc3QgaW1tdXRhYmlsaXR5IGluIGF0IG1vc3QgMTIwIHdvcmRzLiBVc2Ugb25seSBvZmZpY2lhbCBQeXRob24gZG9jdW1lbnRhdGlvbiBpZiB5b3Ugc2VhcmNoLiBIYXZlIHRoZSBza2VwdGljIGFuZCB2ZXJpZmllciBjaGVjayBpdCwgdGhlbiBoYXZlIHRoZSBlZGl0b3Igd3JpdGUgL2Fuc3dlci5tZC4gS2VlcCB0aGUgcnVuIHNob3J0IGFuZCBkbyBub3QgYXNrIGZvbGxvdy11cCBxdWVzdGlvbnMuIn1dfQ==" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "7b787d5c997d768a725a3a2f0278e857", + "spanId": "aa1eaf69717f1df1", + "parentSpanId": "93886a81b22f3dab", + "name": "route_to_active_agent", + "kind": 1, + "startTimeUnixNano": "1790967512074637056", + "endTimeUnixNano": "1790967512075297024", + "attributes": [ + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "route_to_active_agent" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langgraph" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "0" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "__start__" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"__start__\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"__start__\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "__start__:af3b0b0a-9397-3b9d-0717-b11174443ca9" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "langsmith:hidden, seq:step:2" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W3siY29udGVudCI6IkZvciBhIHN5bnRoZXRpYyB0cmFjaW5nIGZpeHR1cmUsIGV4cGxhaW4gUHl0aG9uIHR1cGxlIHZlcnN1cyBsaXN0IGltbXV0YWJpbGl0eSBpbiBhdCBtb3N0IDEyMCB3b3Jkcy4gVXNlIG9ubHkgb2ZmaWNpYWwgUHl0aG9uIGRvY3VtZW50YXRpb24gaWYgeW91IHNlYXJjaC4gSGF2ZSB0aGUgc2tlcHRpYyBhbmQgdmVyaWZpZXIgY2hlY2sgaXQsIHRoZW4gaGF2ZSB0aGUgZWRpdG9yIHdyaXRlIC9hbnN3ZXIubWQuIEtlZXAgdGhlIHJ1biBzaG9ydCBhbmQgZG8gbm90IGFzayBmb2xsb3ctdXAgcXVlc3Rpb25zLiIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoiaHVtYW4iLCJpZCI6Ijk0ZDk3NzJjLWI2NTYtNGUyNS05YmRiLTliZWQxZTRkNzIxZiJ9XX0=" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJvdXRwdXQiOiJyZXNlYXJjaGVyIn0=" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "7b787d5c997d768a725a3a2f0278e857", + "spanId": "88d2e1bce2887a89", + "parentSpanId": "b580832fc04b0921", + "name": "ChatAnthropic", + "kind": 1, + "startTimeUnixNano": "1790967512090679808", + "endTimeUnixNano": "1790967512605561856", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chat" + } + }, + { + "key": "gen_ai.serialized.name", + "value": { + "stringValue": "ChatAnthropic" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "llm" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "ChatAnthropic" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "anthropic" + } + }, + { + "key": "gen_ai.request.model", + "value": { + "stringValue": "claude-sonnet" + } + }, + { + "key": "gen_ai.tool.definitions", + "value": { + "stringValue": "[{\"name\":\"ls\",\"input_schema\":{\"properties\":{\"path\":{\"description\":\"Absolute path to the directory to list. Must be absolute, not relative.\",\"type\":\"string\"}},\"required\":[\"path\"],\"type\":\"object\"},\"description\":\"Lists all files in a directory.\\n\\nThis is useful for exploring the filesystem and finding the right file to read or edit.\\nYou should almost ALWAYS use this tool before using the read_file or edit_file tools.\"},{\"name\":\"read_file\",\"input_schema\":{\"properties\":{\"file_path\":{\"description\":\"Absolute path to the file to read. Must be absolute, not relative.\",\"type\":\"string\"},\"offset\":{\"default\":0,\"description\":\"Line number to start reading from (0-indexed). Use for pagination of large files.\",\"type\":\"integer\"},\"limit\":{\"default\":100,\"description\":\"Maximum number of lines to read. Use for pagination of large files.\",\"type\":\"integer\"}},\"required\":[\"file_path\"],\"type\":\"object\"},\"description\":\"Reads a file from the filesystem. Assume any path the user provides is valid; reading a missing file returns an error.\\n\\nUsage:\\n- By default, it reads up to 100 lines starting from the beginning of the file. Use `offset`/`limit` to page through large files instead of reading them whole.\\n- A status header, `@@ field | field | ... @@`, sits above the file content, and every line after it is verbatim file content. When content is truncated, there may be an explanation before the header. Never include the header when editing.\\n- Speculatively batch multiple `read_file` calls in one response when several files may be useful.\\n- An empty file returns a system-reminder warning in place of contents.\\n- Large tool results may be offloaded to a file; the tool message gives the path. Read that path here, paging with `offset`/`limit`.\\n- Images (`.png`, `.jpg`, etc.), audio, video, and PDFs return multimodal content blocks (https://docs.langchain.com/oss/python/langchain/messages#multimodal).\\n- For images and PDFs, pagination via `offset`/`limit` is text-only - supply `file_path` only\\n- Always read a file before editing it.\"},{\"name\":\"glob\",\"input_schema\":{\"properties\":{\"pattern\":{\"description\":\"Glob pattern to match files (e.g., '*.py', '**/*.py', '/subdir/**/*.md'). A pattern without '/' matches the file name at any depth; a pattern containing '/' matches the search-root-relative path; a leading '/' anchors to the search root ('/*.py' matches only top-level files). Leading-dot names are excluded unless the pattern segment starts with '.', so prefer the bare form '*.py' over '**/*.py' -- '**' will not descend into dot-directories like '.github'.\",\"type\":\"string\"},\"path\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"null\"}],\"default\":null,\"description\":\"Base directory to search from. Defaults to the backend's default root.\"}},\"required\":[\"pattern\"],\"type\":\"object\"},\"description\":\"Find files matching a glob pattern, returning absolute paths.\\n\\nSupports `*` (any characters within a path segment), `**` (any directories), `?` (single character), `[abc]` (one character from a set), and `{a,b}` (alternatives), e.g. `*.py`, `src/**/*.py`, `*.{yml,yaml}`.\\n\\nA pattern without `/` matches the file name at any depth under the search root (`*.py` matches `src/app/main.py`). A pattern containing `/` matches the search-root-relative path (`src/**/*.py`). A leading `/` anchors to the search root (`/*.py` matches only top-level Python files).\\n\\nLeading-dot names are only matched when the pattern segment itself starts with `.` (use `.env`, or `.github/**/*.yml`). Because `**` will not descend into dot-directories, the bare form `*.yml` is *broader* than `**/*.yml` and is usually what you want.\"},{\"name\":\"grep\",\"input_schema\":{\"properties\":{\"pattern\":{\"description\":\"Text pattern to search for (literal string, not regex).\",\"type\":\"string\"},\"path\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"null\"}],\"default\":null,\"description\":\"Directory to search in. Defaults to current working directory.\"},\"glob\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"null\"}],\"default\":null,\"description\":\"Glob pattern (NOT regex) limiting which files are searched (e.g. '*.py', '*.ts'). A pattern without '/' matches the file name at any depth; a pattern containing '/' matches the search-root-relative path (e.g. 'src/**/*.py'). This is an in-tool file filter, not a call to the separate glob tool. Brace expansion (e.g. '*.{ts,tsx}') is not supported on all backends; run a separate search per extension for reliable results.\"},\"output_mode\":{\"default\":\"files_with_matches\",\"description\":\"Shape of the returned text. 'files_with_matches' (default): newline-separated matching file paths. 'content': matching lines grouped by file under a ':' header, each line indented and formatted ': ' (only the matched line, no surrounding context). 'count': one ': ' line per file.\",\"enum\":[\"files_with_matches\",\"content\",\"count\"],\"type\":\"string\"},\"max_count\":{\"anyOf\":[{\"exclusiveMinimum\":0,\"type\":\"integer\"},{\"type\":\"null\"}],\"default\":null,\"description\":\"Optional cap on the total number of matches returned across all files. Leave unset to use the configured default. When the cap is hit, results are truncated and a note says so; narrow the pattern or path to see the rest.\"}},\"required\":[\"pattern\"],\"type\":\"object\"},\"description\":\"Search for a LITERAL text pattern across files (NOT regex).\\n\\nThe pattern is matched verbatim: regex metacharacters are ordinary characters, not operators. To match any of several strings, run a separate grep for each; `grep(pattern=\\\"foo|bar\\\")` searches for the literal text \\\"foo|bar\\\", and `.*` or `\\\\.` match those characters literally.\\n\\nReturns matching files or content per `output_mode`. Offloaded large tool results live under the artifacts root (`/large_tool_results/` by default); grep that directory to search them when you do not know the exact path.\"},{\"name\":\"web_search\",\"input_schema\":{\"properties\":{\"query\":{\"type\":\"string\"}},\"required\":[\"query\"],\"type\":\"object\"},\"description\":\"Search the web with Exa for current facts and return excerpts with source URLs\"},{\"name\":\"transfer_to_skeptic\",\"input_schema\":{\"properties\":{},\"type\":\"object\"},\"description\":\"Send findings to the skeptic for critique\"},{\"name\":\"transfer_to_verifier\",\"input_schema\":{\"properties\":{},\"type\":\"object\"},\"description\":\"Send revised findings for source verification\"}]" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_chat_model" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "1" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "researcher:816a3f65-9150-0b6d-54aa-920fdcfe25cd|model:2e4e0d8e-70a5-697c-c7f4-e1f685fb1a31" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "researcher" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "researcher:816a3f65-9150-0b6d-54aa-920fdcfe25cd" + } + }, + { + "key": "langsmith.metadata.ls_provider", + "value": { + "stringValue": "anthropic" + } + }, + { + "key": "langsmith.metadata.ls_model_name", + "value": { + "stringValue": "claude-sonnet" + } + }, + { + "key": "langsmith.metadata.ls_model_type", + "value": { + "stringValue": "chat" + } + }, + { + "key": "langsmith.metadata.ls_max_tokens", + "value": { + "intValue": "4096" + } + }, + { + "key": "langsmith.metadata.lc_versions", + "value": { + "stringValue": "{\"langchain-core\":\"1.6.6\",\"langchain\":\"1.4.3\",\"langchain-anthropic\":\"1.7.5\"}" + } + }, + { + "key": "langsmith.metadata.ls_method", + "value": { + "stringValue": "traceable" + } + }, + { + "key": "langsmith.metadata.model", + "value": { + "stringValue": "claude-sonnet" + } + }, + { + "key": "langsmith.metadata.max_tokens", + "value": { + "intValue": "4096" + } + }, + { + "key": "langsmith.metadata.model_kwargs", + "value": { + "stringValue": "{}" + } + }, + { + "key": "langsmith.metadata.streaming", + "value": { + "boolValue": false + } + }, + { + "key": "langsmith.metadata.max_retries", + "value": { + "intValue": "2" + } + }, + { + "key": "langsmith.metadata._type", + "value": { + "stringValue": "anthropic-chat" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "seq:step:1" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W1t7ImxjIjoxLCJ0eXBlIjoiY29uc3RydWN0b3IiLCJpZCI6WyJsYW5nY2hhaW4iLCJzY2hlbWEiLCJtZXNzYWdlcyIsIlN5c3RlbU1lc3NhZ2UiXSwia3dhcmdzIjp7ImNvbnRlbnQiOiJZb3UgYXJlIHRoZSByZXNlYXJjaGVyLiBTZWFyY2ggZm9yIGV2aWRlbmNlIGFuZCBzaGFyZSBzb3VyY2UgVVJMcy4gWW91IG1heSByZWFkIHNoYXJlZCB2aXJ0dWFsIGZpbGVzLCBidXQgb25seSB0aGUgZWRpdG9yIHdyaXRlcyB0aGVtLiBTZW5kIGluaXRpYWwgZmluZGluZ3MgdG8gdGhlIHNrZXB0aWMuIElmIGFub3RoZXIgYWdlbnQgcmV0dXJucyB3aXRoIGNvcnJlY3Rpb25zLCByZXZpc2UgeW91ciBmaW5kaW5ncyBhbmQgc2VuZCB0aGVtIHRvIHRoZSB2ZXJpZmllci4gRG8gbm90IGdpdmUgdGhlIGZpbmFsIGFuc3dlci4iLCJ0eXBlIjoic3lzdGVtIn19LHsibGMiOjEsInR5cGUiOiJjb25zdHJ1Y3RvciIsImlkIjpbImxhbmdjaGFpbiIsInNjaGVtYSIsIm1lc3NhZ2VzIiwiSHVtYW5NZXNzYWdlIl0sImt3YXJncyI6eyJjb250ZW50IjoiRm9yIGEgc3ludGhldGljIHRyYWNpbmcgZml4dHVyZSwgZXhwbGFpbiBQeXRob24gdHVwbGUgdmVyc3VzIGxpc3QgaW1tdXRhYmlsaXR5IGluIGF0IG1vc3QgMTIwIHdvcmRzLiBVc2Ugb25seSBvZmZpY2lhbCBQeXRob24gZG9jdW1lbnRhdGlvbiBpZiB5b3Ugc2VhcmNoLiBIYXZlIHRoZSBza2VwdGljIGFuZCB2ZXJpZmllciBjaGVjayBpdCwgdGhlbiBoYXZlIHRoZSBlZGl0b3Igd3JpdGUgL2Fuc3dlci5tZC4gS2VlcCB0aGUgcnVuIHNob3J0IGFuZCBkbyBub3QgYXNrIGZvbGxvdy11cCBxdWVzdGlvbnMuIiwidHlwZSI6Imh1bWFuIiwiaWQiOiJlOGZkNzFmYi1hZjE3LTRiZTgtYjU5OS0xOTc3ODUwNTFlZTYifX1dXX0=" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJnZW5lcmF0aW9ucyI6W1t7InRleHQiOiIiLCJnZW5lcmF0aW9uX2luZm8iOm51bGwsInR5cGUiOiJDaGF0R2VuZXJhdGlvbiIsIm1lc3NhZ2UiOnsibGMiOjEsInR5cGUiOiJjb25zdHJ1Y3RvciIsImlkIjpbImxhbmdjaGFpbiIsInNjaGVtYSIsIm1lc3NhZ2VzIiwiQUlNZXNzYWdlIl0sImt3YXJncyI6eyJjb250ZW50IjoiIiwicmVzcG9uc2VfbWV0YWRhdGEiOnsiYm9keSI6eyJ0eXBlIjoiZXJyb3IiLCJlcnJvciI6eyJ0eXBlIjoiYXV0aGVudGljYXRpb25fZXJyb3IiLCJtZXNzYWdlIjoibGl0ZWxsbS5BdXRoZW50aWNhdGlvbkVycm9yOiBBbnRocm9waWNFeGNlcHRpb24gLSB7XCJ0eXBlXCI6XCJlcnJvclwiLFwiZXJyb3JcIjp7XCJ0eXBlXCI6XCJhdXRoZW50aWNhdGlvbl9lcnJvclwiLFwibWVzc2FnZVwiOlwiQVBJIGtleSBpcyBpbnZhbGlkLlwifSxcInJlcXVlc3RfaWRcIjpudWxsfVxuXG5MaXRlTExNOiBtb2RlbCBncm91cCAnY2xhdWRlLXNvbm5ldCcgZmFpbGVkIHdpdGggdGhlIGVycm9yIGFib3ZlLiBObyBmYWxsYmFjayB3YXMgYXR0ZW1wdGVkLiJ9fSwiaGVhZGVycyI6eyJkYXRlIjoiRnJpLCAwMiBPY3QgMjAyNiAxODo1ODozMSBHTVQiLCJ4LWxpdGVsbG0tY2FsbC1pZCI6IjQ4OTRkMzhhLTVkYjctNGViYS1iZjFiLTE1ODM2NjZkNTc5NSIsIngtbGl0ZWxsbS1tb2RlbC1pZCI6ImNjMTViNDA2Y2QxNDZkM2YyMzNiMTQ0MGNkZThhYjZjMjExMTNjZGZjZDk3YmI0NTkyNzBkZjQ3NGNjZDY3MGMiLCJ4LWxpdGVsbG0tdmVyc2lvbiI6IjEuMTA1LjAiLCJ4LWxpdGVsbG0tcmVzcG9uc2UtY29zdCI6IjAiLCJ4LWxpdGVsbG0ta2V5LXNwZW5kIjoiMC4wIiwieC1saXRlbGxtLXRpbWVvdXQiOiI2MDAuMCIsImNvbnRlbnQtbGVuZ3RoIjoiMzM3IiwiY29udGVudC10eXBlIjoiYXBwbGljYXRpb24vanNvbiIsIngtZnJhbWUtb3B0aW9ucyI6IkRFTlkiLCJjb250ZW50LXNlY3VyaXR5LXBvbGljeSI6ImZyYW1lLWFuY2VzdG9ycyAnbm9uZSciLCJ4LWNvbnRlbnQtdHlwZS1vcHRpb25zIjoibm9zbmlmZiJ9LCJzdGF0dXNfY29kZSI6NDAxLCJyZXF1ZXN0X2lkIjpudWxsfSwidHlwZSI6ImFpIiwidG9vbF9jYWxscyI6W10sImludmFsaWRfdG9vbF9jYWxscyI6W119fX1dXSwibGxtX291dHB1dCI6bnVsbCwicnVuIjpudWxsLCJ0eXBlIjoiTExNUmVzdWx0In0=" + } + } + ], + "events": [ + { + "timeUnixNano": "1790967512686323000", + "name": "exception", + "attributes": [ + { + "key": "exception.type", + "value": { + "stringValue": "Exception" + } + }, + { + "key": "exception.message", + "value": { + "stringValue": "AnthropicAuthenticationError('Error code: 401 - {\\'type\\': \\'error\\', \\'error\\': {\\'type\\': \\'authentication_error\\', \\'message\\': \\'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\\\n\\\\nLiteLLM: model group \\\\\\'claude-sonnet\\\\\\' failed with the error above. No fallback was attempted.\\'}}')Traceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2597, in _generate\n raw_response = self._create(payload)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2176, in _create\n return self._client.messages.with_raw_response.create(**payload)\n ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_response.py\", line 797, in wrapped\n return cast(APIResponse[R], func(*args, **kwargs))\n ~~~~^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_utils/_utils.py\", line 294, in wrapper\n return func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/resources/messages/messages.py\", line 1027, in create\n return self._post(\n ~~~~~~~~~~^\n \"/v1/messages\",\n ^^^^^^^^^^^^^^^\n ...<23 lines>...\n stream_cls=Stream[RawMessageStreamEvent],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_base_client.py\", line 1543, in post\n return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls))\n ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_base_client.py\", line 1208, in request\n raise self._make_status_error_from_response(response) from None\n\n\nanthropic.AuthenticationError: Error code: 401 - {'type': 'error', 'error': {'type': 'authentication_error', 'message': 'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\n\\nLiteLLM: model group \\'claude-sonnet\\' failed with the error above. No fallback was attempted.'}}\n\n\n\nThe above exception was the direct cause of the following exception:\n\n\n\nTraceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 1684, in generate\n self._generate_with_cache(\n ~~~~~~~~~~~~~~~~~~~~~~~~~^\n m,\n ^^\n ...<2 lines>...\n **kwargs,\n ^^^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 2022, in _generate_with_cache\n result = self._generate(\n messages, stop=stop, run_manager=run_manager, **kwargs\n )\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2601, in _generate\n _handle_anthropic_api_error(e)\n ~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 1295, in _handle_anthropic_api_error\n raise AnthropicAuthenticationError(\n message=e.message, response=e.response, body=e.body\n ) from e\n\n\nlangchain_anthropic.chat_models.AnthropicAuthenticationError: Error code: 401 - {'type': 'error', 'error': {'type': 'authentication_error', 'message': 'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\n\\nLiteLLM: model group \\'claude-sonnet\\' failed with the error above. No fallback was attempted.'}}" + } + }, + { + "key": "exception.stacktrace", + "value": { + "stringValue": "Exception: AnthropicAuthenticationError('Error code: 401 - {\\'type\\': \\'error\\', \\'error\\': {\\'type\\': \\'authentication_error\\', \\'message\\': \\'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\\\n\\\\nLiteLLM: model group \\\\\\'claude-sonnet\\\\\\' failed with the error above. No fallback was attempted.\\'}}')Traceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2597, in _generate\n raw_response = self._create(payload)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2176, in _create\n return self._client.messages.with_raw_response.create(**payload)\n ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_response.py\", line 797, in wrapped\n return cast(APIResponse[R], func(*args, **kwargs))\n ~~~~^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_utils/_utils.py\", line 294, in wrapper\n return func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/resources/messages/messages.py\", line 1027, in create\n return self._post(\n ~~~~~~~~~~^\n \"/v1/messages\",\n ^^^^^^^^^^^^^^^\n ...<23 lines>...\n stream_cls=Stream[RawMessageStreamEvent],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_base_client.py\", line 1543, in post\n return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls))\n ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_base_client.py\", line 1208, in request\n raise self._make_status_error_from_response(response) from None\n\n\nanthropic.AuthenticationError: Error code: 401 - {'type': 'error', 'error': {'type': 'authentication_error', 'message': 'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\n\\nLiteLLM: model group \\'claude-sonnet\\' failed with the error above. No fallback was attempted.'}}\n\n\n\nThe above exception was the direct cause of the following exception:\n\n\n\nTraceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 1684, in generate\n self._generate_with_cache(\n ~~~~~~~~~~~~~~~~~~~~~~~~~^\n m,\n ^^\n ...<2 lines>...\n **kwargs,\n ^^^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 2022, in _generate_with_cache\n result = self._generate(\n messages, stop=stop, run_manager=run_manager, **kwargs\n )\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2601, in _generate\n _handle_anthropic_api_error(e)\n ~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 1295, in _handle_anthropic_api_error\n raise AnthropicAuthenticationError(\n message=e.message, response=e.response, body=e.body\n ) from e\n\n\nlangchain_anthropic.chat_models.AnthropicAuthenticationError: Error code: 401 - {'type': 'error', 'error': {'type': 'authentication_error', 'message': 'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\n\\nLiteLLM: model group \\'claude-sonnet\\' failed with the error above. No fallback was attempted.'}}\n" + } + }, + { + "key": "exception.escaped", + "value": { + "stringValue": "False" + } + } + ] + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "7b787d5c997d768a725a3a2f0278e857", + "spanId": "b580832fc04b0921", + "parentSpanId": "382ca31426f8f943", + "name": "UnsupportedContentMiddleware.wrap_model_call", + "kind": 1, + "startTimeUnixNano": "1790967512081928960", + "endTimeUnixNano": "1790967512610889984", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "e30=" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "UnsupportedContentMiddleware.wrap_model_call" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "1" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "researcher:816a3f65-9150-0b6d-54aa-920fdcfe25cd|model:2e4e0d8e-70a5-697c-c7f4-e1f685fb1a31" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "researcher" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "researcher:816a3f65-9150-0b6d-54aa-920fdcfe25cd" + } + }, + { + "key": "langsmith.metadata.ls_method", + "value": { + "stringValue": "traceable" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJvdXRwdXQiOm51bGx9" + } + } + ], + "events": [ + { + "timeUnixNano": "1790967512686465000", + "name": "exception", + "attributes": [ + { + "key": "exception.type", + "value": { + "stringValue": "Exception" + } + }, + { + "key": "exception.message", + "value": { + "stringValue": "AnthropicAuthenticationError('Error code: 401 - {\\'type\\': \\'error\\', \\'error\\': {\\'type\\': \\'authentication_error\\', \\'message\\': \\'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\\\n\\\\nLiteLLM: model group \\\\\\'claude-sonnet\\\\\\' failed with the error above. No fallback was attempted.\\'}}')\n\nTraceback (most recent call last):\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2597, in _generate\n raw_response = self._create(payload)\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2176, in _create\n return self._client.messages.with_raw_response.create(**payload)\n ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_response.py\", line 797, in wrapped\n return cast(APIResponse[R], func(*args, **kwargs))\n ~~~~^^^^^^^^^^^^^^^^^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_utils/_utils.py\", line 294, in wrapper\n return func(*args, **kwargs)\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/resources/messages/messages.py\", line 1027, in create\n return self._post(\n ~~~~~~~~~~^\n \"/v1/messages\",\n ^^^^^^^^^^^^^^^\n ...<23 lines>...\n stream_cls=Stream[RawMessageStreamEvent],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n )\n ^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_base_client.py\", line 1543, in post\n return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls))\n ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_base_client.py\", line 1208, in request\n raise self._make_status_error_from_response(response) from None\nanthropic.AuthenticationError: Error code: 401 - {'type': 'error', 'error': {'type': 'authentication_error', 'message': 'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\n\\nLiteLLM: model group \\'claude-sonnet\\' failed with the error above. No fallback was attempted.'}}\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/deepagents/middleware/unsupported_content.py\", line 191, in wrap_model_call\n return handler(self._filter_request(request))\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 1506, in _execute_model_sync\n output = model_.invoke(messages)\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/runnables/base.py\", line 6014, in invoke\n return self.bound.invoke(\n ~~~~~~~~~~~~~~~~~^\n input,\n ^^^^^^\n self._merge_configs(config),\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n **{**self.kwargs, **kwargs},\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n )\n ^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 488, in invoke\n self.generate_prompt(\n ~~~~~~~~~~~~~~~~~~~~^\n [self._convert_input(input)],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<6 lines>...\n **kwargs,\n ^^^^^^^^^\n ).generations[0][0],\n ^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 1877, in generate_prompt\n return self.generate(prompt_messages, stop=stop, callbacks=callbacks, **kwargs)\n ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 1684, in generate\n self._generate_with_cache(\n ~~~~~~~~~~~~~~~~~~~~~~~~~^\n m,\n ^^\n ...<2 lines>...\n **kwargs,\n ^^^^^^^^^\n )\n ^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 2022, in _generate_with_cache\n result = self._generate(\n messages, stop=stop, run_manager=run_manager, **kwargs\n )\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2601, in _generate\n _handle_anthropic_api_error(e)\n ~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 1295, in _handle_anthropic_api_error\n raise AnthropicAuthenticationError(\n message=e.message, response=e.response, body=e.body\n ) from e\nlangchain_anthropic.chat_models.AnthropicAuthenticationError: Error code: 401 - {'type': 'error', 'error': {'type': 'authentication_error', 'message': 'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\n\\nLiteLLM: model group \\'claude-sonnet\\' failed with the error above. No fallback was attempted.'}}\n" + } + }, + { + "key": "exception.stacktrace", + "value": { + "stringValue": "Exception: AnthropicAuthenticationError('Error code: 401 - {\\'type\\': \\'error\\', \\'error\\': {\\'type\\': \\'authentication_error\\', \\'message\\': \\'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\\\n\\\\nLiteLLM: model group \\\\\\'claude-sonnet\\\\\\' failed with the error above. No fallback was attempted.\\'}}')\n\nTraceback (most recent call last):\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2597, in _generate\n raw_response = self._create(payload)\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2176, in _create\n return self._client.messages.with_raw_response.create(**payload)\n ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_response.py\", line 797, in wrapped\n return cast(APIResponse[R], func(*args, **kwargs))\n ~~~~^^^^^^^^^^^^^^^^^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_utils/_utils.py\", line 294, in wrapper\n return func(*args, **kwargs)\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/resources/messages/messages.py\", line 1027, in create\n return self._post(\n ~~~~~~~~~~^\n \"/v1/messages\",\n ^^^^^^^^^^^^^^^\n ...<23 lines>...\n stream_cls=Stream[RawMessageStreamEvent],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n )\n ^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_base_client.py\", line 1543, in post\n return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls))\n ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_base_client.py\", line 1208, in request\n raise self._make_status_error_from_response(response) from None\nanthropic.AuthenticationError: Error code: 401 - {'type': 'error', 'error': {'type': 'authentication_error', 'message': 'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\n\\nLiteLLM: model group \\'claude-sonnet\\' failed with the error above. No fallback was attempted.'}}\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/deepagents/middleware/unsupported_content.py\", line 191, in wrap_model_call\n return handler(self._filter_request(request))\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 1506, in _execute_model_sync\n output = model_.invoke(messages)\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/runnables/base.py\", line 6014, in invoke\n return self.bound.invoke(\n ~~~~~~~~~~~~~~~~~^\n input,\n ^^^^^^\n self._merge_configs(config),\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n **{**self.kwargs, **kwargs},\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n )\n ^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 488, in invoke\n self.generate_prompt(\n ~~~~~~~~~~~~~~~~~~~~^\n [self._convert_input(input)],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<6 lines>...\n **kwargs,\n ^^^^^^^^^\n ).generations[0][0],\n ^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 1877, in generate_prompt\n return self.generate(prompt_messages, stop=stop, callbacks=callbacks, **kwargs)\n ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 1684, in generate\n self._generate_with_cache(\n ~~~~~~~~~~~~~~~~~~~~~~~~~^\n m,\n ^^\n ...<2 lines>...\n **kwargs,\n ^^^^^^^^^\n )\n ^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 2022, in _generate_with_cache\n result = self._generate(\n messages, stop=stop, run_manager=run_manager, **kwargs\n )\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2601, in _generate\n _handle_anthropic_api_error(e)\n ~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 1295, in _handle_anthropic_api_error\n raise AnthropicAuthenticationError(\n message=e.message, response=e.response, body=e.body\n ) from e\nlangchain_anthropic.chat_models.AnthropicAuthenticationError: Error code: 401 - {'type': 'error', 'error': {'type': 'authentication_error', 'message': 'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\n\\nLiteLLM: model group \\'claude-sonnet\\' failed with the error above. No fallback was attempted.'}}\n\n" + } + }, + { + "key": "exception.escaped", + "value": { + "stringValue": "False" + } + } + ] + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "7b787d5c997d768a725a3a2f0278e857", + "spanId": "382ca31426f8f943", + "parentSpanId": "e247cc846cdbc99e", + "name": "FilesystemMiddleware.wrap_model_call", + "kind": 1, + "startTimeUnixNano": "1790967512080943104", + "endTimeUnixNano": "1790967512614221056", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "e30=" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "FilesystemMiddleware.wrap_model_call" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "1" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "researcher:816a3f65-9150-0b6d-54aa-920fdcfe25cd|model:2e4e0d8e-70a5-697c-c7f4-e1f685fb1a31" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "researcher" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "researcher:816a3f65-9150-0b6d-54aa-920fdcfe25cd" + } + }, + { + "key": "langsmith.metadata.ls_method", + "value": { + "stringValue": "traceable" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJvdXRwdXQiOm51bGx9" + } + } + ], + "events": [ + { + "timeUnixNano": "1790967512686562000", + "name": "exception", + "attributes": [ + { + "key": "exception.type", + "value": { + "stringValue": "Exception" + } + }, + { + "key": "exception.message", + "value": { + "stringValue": "AnthropicAuthenticationError('Error code: 401 - {\\'type\\': \\'error\\', \\'error\\': {\\'type\\': \\'authentication_error\\', \\'message\\': \\'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\\\n\\\\nLiteLLM: model group \\\\\\'claude-sonnet\\\\\\' failed with the error above. No fallback was attempted.\\'}}')\n\nTraceback (most recent call last):\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2597, in _generate\n raw_response = self._create(payload)\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2176, in _create\n return self._client.messages.with_raw_response.create(**payload)\n ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_response.py\", line 797, in wrapped\n return cast(APIResponse[R], func(*args, **kwargs))\n ~~~~^^^^^^^^^^^^^^^^^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_utils/_utils.py\", line 294, in wrapper\n return func(*args, **kwargs)\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/resources/messages/messages.py\", line 1027, in create\n return self._post(\n ~~~~~~~~~~^\n \"/v1/messages\",\n ^^^^^^^^^^^^^^^\n ...<23 lines>...\n stream_cls=Stream[RawMessageStreamEvent],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n )\n ^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_base_client.py\", line 1543, in post\n return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls))\n ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_base_client.py\", line 1208, in request\n raise self._make_status_error_from_response(response) from None\nanthropic.AuthenticationError: Error code: 401 - {'type': 'error', 'error': {'type': 'authentication_error', 'message': 'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\n\\nLiteLLM: model group \\'claude-sonnet\\' failed with the error above. No fallback was attempted.'}}\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/deepagents/middleware/filesystem.py\", line 3218, in wrap_model_call\n response = handler(request)\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 347, in inner_handler\n inner_result = inner(req, handler)\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/deepagents/middleware/unsupported_content.py\", line 191, in wrap_model_call\n return handler(self._filter_request(request))\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 1506, in _execute_model_sync\n output = model_.invoke(messages)\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/runnables/base.py\", line 6014, in invoke\n return self.bound.invoke(\n ~~~~~~~~~~~~~~~~~^\n input,\n ^^^^^^\n self._merge_configs(config),\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n **{**self.kwargs, **kwargs},\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n )\n ^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 488, in invoke\n self.generate_prompt(\n ~~~~~~~~~~~~~~~~~~~~^\n [self._convert_input(input)],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<6 lines>...\n **kwargs,\n ^^^^^^^^^\n ).generations[0][0],\n ^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 1877, in generate_prompt\n return self.generate(prompt_messages, stop=stop, callbacks=callbacks, **kwargs)\n ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 1684, in generate\n self._generate_with_cache(\n ~~~~~~~~~~~~~~~~~~~~~~~~~^\n m,\n ^^\n ...<2 lines>...\n **kwargs,\n ^^^^^^^^^\n )\n ^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 2022, in _generate_with_cache\n result = self._generate(\n messages, stop=stop, run_manager=run_manager, **kwargs\n )\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2601, in _generate\n _handle_anthropic_api_error(e)\n ~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 1295, in _handle_anthropic_api_error\n raise AnthropicAuthenticationError(\n message=e.message, response=e.response, body=e.body\n ) from e\nlangchain_anthropic.chat_models.AnthropicAuthenticationError: Error code: 401 - {'type': 'error', 'error': {'type': 'authentication_error', 'message': 'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\n\\nLiteLLM: model group \\'claude-sonnet\\' failed with the error above. No fallback was attempted.'}}\n" + } + }, + { + "key": "exception.stacktrace", + "value": { + "stringValue": "Exception: AnthropicAuthenticationError('Error code: 401 - {\\'type\\': \\'error\\', \\'error\\': {\\'type\\': \\'authentication_error\\', \\'message\\': \\'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\\\n\\\\nLiteLLM: model group \\\\\\'claude-sonnet\\\\\\' failed with the error above. No fallback was attempted.\\'}}')\n\nTraceback (most recent call last):\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2597, in _generate\n raw_response = self._create(payload)\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2176, in _create\n return self._client.messages.with_raw_response.create(**payload)\n ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_response.py\", line 797, in wrapped\n return cast(APIResponse[R], func(*args, **kwargs))\n ~~~~^^^^^^^^^^^^^^^^^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_utils/_utils.py\", line 294, in wrapper\n return func(*args, **kwargs)\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/resources/messages/messages.py\", line 1027, in create\n return self._post(\n ~~~~~~~~~~^\n \"/v1/messages\",\n ^^^^^^^^^^^^^^^\n ...<23 lines>...\n stream_cls=Stream[RawMessageStreamEvent],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n )\n ^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_base_client.py\", line 1543, in post\n return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls))\n ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_base_client.py\", line 1208, in request\n raise self._make_status_error_from_response(response) from None\nanthropic.AuthenticationError: Error code: 401 - {'type': 'error', 'error': {'type': 'authentication_error', 'message': 'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\n\\nLiteLLM: model group \\'claude-sonnet\\' failed with the error above. No fallback was attempted.'}}\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/deepagents/middleware/filesystem.py\", line 3218, in wrap_model_call\n response = handler(request)\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 347, in inner_handler\n inner_result = inner(req, handler)\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/deepagents/middleware/unsupported_content.py\", line 191, in wrap_model_call\n return handler(self._filter_request(request))\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 1506, in _execute_model_sync\n output = model_.invoke(messages)\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/runnables/base.py\", line 6014, in invoke\n return self.bound.invoke(\n ~~~~~~~~~~~~~~~~~^\n input,\n ^^^^^^\n self._merge_configs(config),\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n **{**self.kwargs, **kwargs},\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n )\n ^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 488, in invoke\n self.generate_prompt(\n ~~~~~~~~~~~~~~~~~~~~^\n [self._convert_input(input)],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<6 lines>...\n **kwargs,\n ^^^^^^^^^\n ).generations[0][0],\n ^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 1877, in generate_prompt\n return self.generate(prompt_messages, stop=stop, callbacks=callbacks, **kwargs)\n ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 1684, in generate\n self._generate_with_cache(\n ~~~~~~~~~~~~~~~~~~~~~~~~~^\n m,\n ^^\n ...<2 lines>...\n **kwargs,\n ^^^^^^^^^\n )\n ^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 2022, in _generate_with_cache\n result = self._generate(\n messages, stop=stop, run_manager=run_manager, **kwargs\n )\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2601, in _generate\n _handle_anthropic_api_error(e)\n ~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 1295, in _handle_anthropic_api_error\n raise AnthropicAuthenticationError(\n message=e.message, response=e.response, body=e.body\n ) from e\nlangchain_anthropic.chat_models.AnthropicAuthenticationError: Error code: 401 - {'type': 'error', 'error': {'type': 'authentication_error', 'message': 'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\n\\nLiteLLM: model group \\'claude-sonnet\\' failed with the error above. No fallback was attempted.'}}\n\n" + } + }, + { + "key": "exception.escaped", + "value": { + "stringValue": "False" + } + } + ] + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "7b787d5c997d768a725a3a2f0278e857", + "spanId": "e247cc846cdbc99e", + "parentSpanId": "77a5ba07fbe8fb8b", + "name": "model", + "kind": 1, + "startTimeUnixNano": "1790967512077792000", + "endTimeUnixNano": "1790967512617573888", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "e30=" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "1" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "researcher:816a3f65-9150-0b6d-54aa-920fdcfe25cd|model:2e4e0d8e-70a5-697c-c7f4-e1f685fb1a31" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "researcher" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "researcher:816a3f65-9150-0b6d-54aa-920fdcfe25cd" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "graph:step:1" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W3siY29udGVudCI6IkZvciBhIHN5bnRoZXRpYyB0cmFjaW5nIGZpeHR1cmUsIGV4cGxhaW4gUHl0aG9uIHR1cGxlIHZlcnN1cyBsaXN0IGltbXV0YWJpbGl0eSBpbiBhdCBtb3N0IDEyMCB3b3Jkcy4gVXNlIG9ubHkgb2ZmaWNpYWwgUHl0aG9uIGRvY3VtZW50YXRpb24gaWYgeW91IHNlYXJjaC4gSGF2ZSB0aGUgc2tlcHRpYyBhbmQgdmVyaWZpZXIgY2hlY2sgaXQsIHRoZW4gaGF2ZSB0aGUgZWRpdG9yIHdyaXRlIC9hbnN3ZXIubWQuIEtlZXAgdGhlIHJ1biBzaG9ydCBhbmQgZG8gbm90IGFzayBmb2xsb3ctdXAgcXVlc3Rpb25zLiIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoiaHVtYW4iLCJpZCI6ImU4ZmQ3MWZiLWFmMTctNGJlOC1iNTk5LTE5Nzc4NTA1MWVlNiJ9XX0=" + } + } + ], + "events": [ + { + "timeUnixNano": "1790967512686704000", + "name": "exception", + "attributes": [ + { + "key": "exception.type", + "value": { + "stringValue": "Exception" + } + }, + { + "key": "exception.message", + "value": { + "stringValue": "AnthropicAuthenticationError('Error code: 401 - {\\'type\\': \\'error\\', \\'error\\': {\\'type\\': \\'authentication_error\\', \\'message\\': \\'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\\\n\\\\nLiteLLM: model group \\\\\\'claude-sonnet\\\\\\' failed with the error above. No fallback was attempted.\\'}}')Traceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2597, in _generate\n raw_response = self._create(payload)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2176, in _create\n return self._client.messages.with_raw_response.create(**payload)\n ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_response.py\", line 797, in wrapped\n return cast(APIResponse[R], func(*args, **kwargs))\n ~~~~^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_utils/_utils.py\", line 294, in wrapper\n return func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/resources/messages/messages.py\", line 1027, in create\n return self._post(\n ~~~~~~~~~~^\n \"/v1/messages\",\n ^^^^^^^^^^^^^^^\n ...<23 lines>...\n stream_cls=Stream[RawMessageStreamEvent],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_base_client.py\", line 1543, in post\n return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls))\n ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_base_client.py\", line 1208, in request\n raise self._make_status_error_from_response(response) from None\n\n\nanthropic.AuthenticationError: Error code: 401 - {'type': 'error', 'error': {'type': 'authentication_error', 'message': 'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\n\\nLiteLLM: model group \\'claude-sonnet\\' failed with the error above. No fallback was attempted.'}}\n\n\n\nThe above exception was the direct cause of the following exception:\n\n\n\nTraceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 707, in invoke\n input = context.run(step.invoke, input, config, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 447, in invoke\n ret = self.func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 1543, in model_node\n result = wrap_model_call_handler(request, _execute_model_sync)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 357, in composed\n outer_result = outer(request, inner_handler)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/deepagents/middleware/filesystem.py\", line 3218, in wrap_model_call\n response = handler(request)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 347, in inner_handler\n inner_result = inner(req, handler)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/deepagents/middleware/unsupported_content.py\", line 191, in wrap_model_call\n return handler(self._filter_request(request))\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 1506, in _execute_model_sync\n output = model_.invoke(messages)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/runnables/base.py\", line 6014, in invoke\n return self.bound.invoke(\n ~~~~~~~~~~~~~~~~~^\n input,\n ^^^^^^\n self._merge_configs(config),\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n **{**self.kwargs, **kwargs},\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 488, in invoke\n self.generate_prompt(\n ~~~~~~~~~~~~~~~~~~~~^\n [self._convert_input(input)],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<6 lines>...\n **kwargs,\n ^^^^^^^^^\n ).generations[0][0],\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 1877, in generate_prompt\n return self.generate(prompt_messages, stop=stop, callbacks=callbacks, **kwargs)\n ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 1684, in generate\n self._generate_with_cache(\n ~~~~~~~~~~~~~~~~~~~~~~~~~^\n m,\n ^^\n ...<2 lines>...\n **kwargs,\n ^^^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 2022, in _generate_with_cache\n result = self._generate(\n messages, stop=stop, run_manager=run_manager, **kwargs\n )\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2601, in _generate\n _handle_anthropic_api_error(e)\n ~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 1295, in _handle_anthropic_api_error\n raise AnthropicAuthenticationError(\n message=e.message, response=e.response, body=e.body\n ) from e\n\n\nlangchain_anthropic.chat_models.AnthropicAuthenticationError: Error code: 401 - {'type': 'error', 'error': {'type': 'authentication_error', 'message': 'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\n\\nLiteLLM: model group \\'claude-sonnet\\' failed with the error above. No fallback was attempted.'}}" + } + }, + { + "key": "exception.stacktrace", + "value": { + "stringValue": "Exception: AnthropicAuthenticationError('Error code: 401 - {\\'type\\': \\'error\\', \\'error\\': {\\'type\\': \\'authentication_error\\', \\'message\\': \\'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\\\n\\\\nLiteLLM: model group \\\\\\'claude-sonnet\\\\\\' failed with the error above. No fallback was attempted.\\'}}')Traceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2597, in _generate\n raw_response = self._create(payload)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2176, in _create\n return self._client.messages.with_raw_response.create(**payload)\n ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_response.py\", line 797, in wrapped\n return cast(APIResponse[R], func(*args, **kwargs))\n ~~~~^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_utils/_utils.py\", line 294, in wrapper\n return func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/resources/messages/messages.py\", line 1027, in create\n return self._post(\n ~~~~~~~~~~^\n \"/v1/messages\",\n ^^^^^^^^^^^^^^^\n ...<23 lines>...\n stream_cls=Stream[RawMessageStreamEvent],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_base_client.py\", line 1543, in post\n return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls))\n ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_base_client.py\", line 1208, in request\n raise self._make_status_error_from_response(response) from None\n\n\nanthropic.AuthenticationError: Error code: 401 - {'type': 'error', 'error': {'type': 'authentication_error', 'message': 'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\n\\nLiteLLM: model group \\'claude-sonnet\\' failed with the error above. No fallback was attempted.'}}\n\n\n\nThe above exception was the direct cause of the following exception:\n\n\n\nTraceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 707, in invoke\n input = context.run(step.invoke, input, config, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 447, in invoke\n ret = self.func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 1543, in model_node\n result = wrap_model_call_handler(request, _execute_model_sync)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 357, in composed\n outer_result = outer(request, inner_handler)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/deepagents/middleware/filesystem.py\", line 3218, in wrap_model_call\n response = handler(request)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 347, in inner_handler\n inner_result = inner(req, handler)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/deepagents/middleware/unsupported_content.py\", line 191, in wrap_model_call\n return handler(self._filter_request(request))\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 1506, in _execute_model_sync\n output = model_.invoke(messages)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/runnables/base.py\", line 6014, in invoke\n return self.bound.invoke(\n ~~~~~~~~~~~~~~~~~^\n input,\n ^^^^^^\n self._merge_configs(config),\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n **{**self.kwargs, **kwargs},\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 488, in invoke\n self.generate_prompt(\n ~~~~~~~~~~~~~~~~~~~~^\n [self._convert_input(input)],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<6 lines>...\n **kwargs,\n ^^^^^^^^^\n ).generations[0][0],\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 1877, in generate_prompt\n return self.generate(prompt_messages, stop=stop, callbacks=callbacks, **kwargs)\n ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 1684, in generate\n self._generate_with_cache(\n ~~~~~~~~~~~~~~~~~~~~~~~~~^\n m,\n ^^\n ...<2 lines>...\n **kwargs,\n ^^^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 2022, in _generate_with_cache\n result = self._generate(\n messages, stop=stop, run_manager=run_manager, **kwargs\n )\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2601, in _generate\n _handle_anthropic_api_error(e)\n ~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 1295, in _handle_anthropic_api_error\n raise AnthropicAuthenticationError(\n message=e.message, response=e.response, body=e.body\n ) from e\n\n\nlangchain_anthropic.chat_models.AnthropicAuthenticationError: Error code: 401 - {'type': 'error', 'error': {'type': 'authentication_error', 'message': 'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\n\\nLiteLLM: model group \\'claude-sonnet\\' failed with the error above. No fallback was attempted.'}}\n" + } + }, + { + "key": "exception.escaped", + "value": { + "stringValue": "False" + } + } + ] + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "7b787d5c997d768a725a3a2f0278e857", + "spanId": "77a5ba07fbe8fb8b", + "parentSpanId": "7b77a08f725c2a60", + "name": "researcher", + "kind": 1, + "startTimeUnixNano": "1790967512076764160", + "endTimeUnixNano": "1790967512623192064", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "e30=" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "researcher" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "1" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "researcher" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:researcher\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"researcher\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "researcher:816a3f65-9150-0b6d-54aa-920fdcfe25cd" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "researcher" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "researcher:816a3f65-9150-0b6d-54aa-920fdcfe25cd" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "seq:step:1" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W3siY29udGVudCI6IkZvciBhIHN5bnRoZXRpYyB0cmFjaW5nIGZpeHR1cmUsIGV4cGxhaW4gUHl0aG9uIHR1cGxlIHZlcnN1cyBsaXN0IGltbXV0YWJpbGl0eSBpbiBhdCBtb3N0IDEyMCB3b3Jkcy4gVXNlIG9ubHkgb2ZmaWNpYWwgUHl0aG9uIGRvY3VtZW50YXRpb24gaWYgeW91IHNlYXJjaC4gSGF2ZSB0aGUgc2tlcHRpYyBhbmQgdmVyaWZpZXIgY2hlY2sgaXQsIHRoZW4gaGF2ZSB0aGUgZWRpdG9yIHdyaXRlIC9hbnN3ZXIubWQuIEtlZXAgdGhlIHJ1biBzaG9ydCBhbmQgZG8gbm90IGFzayBmb2xsb3ctdXAgcXVlc3Rpb25zLiIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoiaHVtYW4iLCJpZCI6ImU4ZmQ3MWZiLWFmMTctNGJlOC1iNTk5LTE5Nzc4NTA1MWVlNiJ9XX0=" + } + } + ], + "events": [ + { + "timeUnixNano": "1790967512687171000", + "name": "exception", + "attributes": [ + { + "key": "exception.type", + "value": { + "stringValue": "Exception" + } + }, + { + "key": "exception.message", + "value": { + "stringValue": "AnthropicAuthenticationError('Error code: 401 - {\\'type\\': \\'error\\', \\'error\\': {\\'type\\': \\'authentication_error\\', \\'message\\': \\'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\\\n\\\\nLiteLLM: model group \\\\\\'claude-sonnet\\\\\\' failed with the error above. No fallback was attempted.\\'}}')Traceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2597, in _generate\n raw_response = self._create(payload)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2176, in _create\n return self._client.messages.with_raw_response.create(**payload)\n ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_response.py\", line 797, in wrapped\n return cast(APIResponse[R], func(*args, **kwargs))\n ~~~~^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_utils/_utils.py\", line 294, in wrapper\n return func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/resources/messages/messages.py\", line 1027, in create\n return self._post(\n ~~~~~~~~~~^\n \"/v1/messages\",\n ^^^^^^^^^^^^^^^\n ...<23 lines>...\n stream_cls=Stream[RawMessageStreamEvent],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_base_client.py\", line 1543, in post\n return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls))\n ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_base_client.py\", line 1208, in request\n raise self._make_status_error_from_response(response) from None\n\n\nanthropic.AuthenticationError: Error code: 401 - {'type': 'error', 'error': {'type': 'authentication_error', 'message': 'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\n\\nLiteLLM: model group \\'claude-sonnet\\' failed with the error above. No fallback was attempted.'}}\n\n\n\nThe above exception was the direct cause of the following exception:\n\n\n\nTraceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 2967, in stream\n for _ in runner.tick(\n ~~~~~~~~~~~^\n [t for t in loop.tasks.values() if not t.writes],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<2 lines>...\n schedule_task=loop.accept_push,\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ):\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_runner.py\", line 207, in tick\n run_with_retry(\n ~~~~~~~~~~~~~~^\n t,\n ^^\n ...<10 lines>...\n },\n ^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_retry.py\", line 617, in run_with_retry\n return task.proc.invoke(task.input, config)\n ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 707, in invoke\n input = context.run(step.invoke, input, config, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 447, in invoke\n ret = self.func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 1543, in model_node\n result = wrap_model_call_handler(request, _execute_model_sync)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 357, in composed\n outer_result = outer(request, inner_handler)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/deepagents/middleware/filesystem.py\", line 3218, in wrap_model_call\n response = handler(request)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 347, in inner_handler\n inner_result = inner(req, handler)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/deepagents/middleware/unsupported_content.py\", line 191, in wrap_model_call\n return handler(self._filter_request(request))\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 1506, in _execute_model_sync\n output = model_.invoke(messages)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/runnables/base.py\", line 6014, in invoke\n return self.bound.invoke(\n ~~~~~~~~~~~~~~~~~^\n input,\n ^^^^^^\n self._merge_configs(config),\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n **{**self.kwargs, **kwargs},\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 488, in invoke\n self.generate_prompt(\n ~~~~~~~~~~~~~~~~~~~~^\n [self._convert_input(input)],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<6 lines>...\n **kwargs,\n ^^^^^^^^^\n ).generations[0][0],\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 1877, in generate_prompt\n return self.generate(prompt_messages, stop=stop, callbacks=callbacks, **kwargs)\n ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 1684, in generate\n self._generate_with_cache(\n ~~~~~~~~~~~~~~~~~~~~~~~~~^\n m,\n ^^\n ...<2 lines>...\n **kwargs,\n ^^^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 2022, in _generate_with_cache\n result = self._generate(\n messages, stop=stop, run_manager=run_manager, **kwargs\n )\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2601, in _generate\n _handle_anthropic_api_error(e)\n ~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 1295, in _handle_anthropic_api_error\n raise AnthropicAuthenticationError(\n message=e.message, response=e.response, body=e.body\n ) from e\n\n\nlangchain_anthropic.chat_models.AnthropicAuthenticationError: Error code: 401 - {'type': 'error', 'error': {'type': 'authentication_error', 'message': 'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\n\\nLiteLLM: model group \\'claude-sonnet\\' failed with the error above. No fallback was attempted.'}}\n\n\nDuring task with name 'model' and id '2e4e0d8e-70a5-697c-c7f4-e1f685fb1a31'" + } + }, + { + "key": "exception.stacktrace", + "value": { + "stringValue": "Exception: AnthropicAuthenticationError('Error code: 401 - {\\'type\\': \\'error\\', \\'error\\': {\\'type\\': \\'authentication_error\\', \\'message\\': \\'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\\\n\\\\nLiteLLM: model group \\\\\\'claude-sonnet\\\\\\' failed with the error above. No fallback was attempted.\\'}}')Traceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2597, in _generate\n raw_response = self._create(payload)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2176, in _create\n return self._client.messages.with_raw_response.create(**payload)\n ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_response.py\", line 797, in wrapped\n return cast(APIResponse[R], func(*args, **kwargs))\n ~~~~^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_utils/_utils.py\", line 294, in wrapper\n return func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/resources/messages/messages.py\", line 1027, in create\n return self._post(\n ~~~~~~~~~~^\n \"/v1/messages\",\n ^^^^^^^^^^^^^^^\n ...<23 lines>...\n stream_cls=Stream[RawMessageStreamEvent],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_base_client.py\", line 1543, in post\n return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls))\n ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_base_client.py\", line 1208, in request\n raise self._make_status_error_from_response(response) from None\n\n\nanthropic.AuthenticationError: Error code: 401 - {'type': 'error', 'error': {'type': 'authentication_error', 'message': 'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\n\\nLiteLLM: model group \\'claude-sonnet\\' failed with the error above. No fallback was attempted.'}}\n\n\n\nThe above exception was the direct cause of the following exception:\n\n\n\nTraceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 2967, in stream\n for _ in runner.tick(\n ~~~~~~~~~~~^\n [t for t in loop.tasks.values() if not t.writes],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<2 lines>...\n schedule_task=loop.accept_push,\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ):\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_runner.py\", line 207, in tick\n run_with_retry(\n ~~~~~~~~~~~~~~^\n t,\n ^^\n ...<10 lines>...\n },\n ^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_retry.py\", line 617, in run_with_retry\n return task.proc.invoke(task.input, config)\n ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 707, in invoke\n input = context.run(step.invoke, input, config, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 447, in invoke\n ret = self.func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 1543, in model_node\n result = wrap_model_call_handler(request, _execute_model_sync)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 357, in composed\n outer_result = outer(request, inner_handler)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/deepagents/middleware/filesystem.py\", line 3218, in wrap_model_call\n response = handler(request)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 347, in inner_handler\n inner_result = inner(req, handler)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/deepagents/middleware/unsupported_content.py\", line 191, in wrap_model_call\n return handler(self._filter_request(request))\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 1506, in _execute_model_sync\n output = model_.invoke(messages)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/runnables/base.py\", line 6014, in invoke\n return self.bound.invoke(\n ~~~~~~~~~~~~~~~~~^\n input,\n ^^^^^^\n self._merge_configs(config),\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n **{**self.kwargs, **kwargs},\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 488, in invoke\n self.generate_prompt(\n ~~~~~~~~~~~~~~~~~~~~^\n [self._convert_input(input)],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<6 lines>...\n **kwargs,\n ^^^^^^^^^\n ).generations[0][0],\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 1877, in generate_prompt\n return self.generate(prompt_messages, stop=stop, callbacks=callbacks, **kwargs)\n ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 1684, in generate\n self._generate_with_cache(\n ~~~~~~~~~~~~~~~~~~~~~~~~~^\n m,\n ^^\n ...<2 lines>...\n **kwargs,\n ^^^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 2022, in _generate_with_cache\n result = self._generate(\n messages, stop=stop, run_manager=run_manager, **kwargs\n )\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2601, in _generate\n _handle_anthropic_api_error(e)\n ~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 1295, in _handle_anthropic_api_error\n raise AnthropicAuthenticationError(\n message=e.message, response=e.response, body=e.body\n ) from e\n\n\nlangchain_anthropic.chat_models.AnthropicAuthenticationError: Error code: 401 - {'type': 'error', 'error': {'type': 'authentication_error', 'message': 'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\n\\nLiteLLM: model group \\'claude-sonnet\\' failed with the error above. No fallback was attempted.'}}\n\n\nDuring task with name 'model' and id '2e4e0d8e-70a5-697c-c7f4-e1f685fb1a31'\n" + } + }, + { + "key": "exception.escaped", + "value": { + "stringValue": "False" + } + } + ] + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "7b787d5c997d768a725a3a2f0278e857", + "spanId": "7b77a08f725c2a60", + "parentSpanId": "11f6195c25249ab1", + "name": "researcher", + "kind": 1, + "startTimeUnixNano": "1790967512076327168", + "endTimeUnixNano": "1790967512626224128", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "e30=" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "researcher" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langgraph" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "1" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "researcher" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:researcher\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"researcher\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "researcher:816a3f65-9150-0b6d-54aa-920fdcfe25cd" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "graph:step:1" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W3siY29udGVudCI6IkZvciBhIHN5bnRoZXRpYyB0cmFjaW5nIGZpeHR1cmUsIGV4cGxhaW4gUHl0aG9uIHR1cGxlIHZlcnN1cyBsaXN0IGltbXV0YWJpbGl0eSBpbiBhdCBtb3N0IDEyMCB3b3Jkcy4gVXNlIG9ubHkgb2ZmaWNpYWwgUHl0aG9uIGRvY3VtZW50YXRpb24gaWYgeW91IHNlYXJjaC4gSGF2ZSB0aGUgc2tlcHRpYyBhbmQgdmVyaWZpZXIgY2hlY2sgaXQsIHRoZW4gaGF2ZSB0aGUgZWRpdG9yIHdyaXRlIC9hbnN3ZXIubWQuIEtlZXAgdGhlIHJ1biBzaG9ydCBhbmQgZG8gbm90IGFzayBmb2xsb3ctdXAgcXVlc3Rpb25zLiIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoiaHVtYW4iLCJpZCI6ImU4ZmQ3MWZiLWFmMTctNGJlOC1iNTk5LTE5Nzc4NTA1MWVlNiJ9XX0=" + } + } + ], + "events": [ + { + "timeUnixNano": "1790967512687277000", + "name": "exception", + "attributes": [ + { + "key": "exception.type", + "value": { + "stringValue": "Exception" + } + }, + { + "key": "exception.message", + "value": { + "stringValue": "AnthropicAuthenticationError('Error code: 401 - {\\'type\\': \\'error\\', \\'error\\': {\\'type\\': \\'authentication_error\\', \\'message\\': \\'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\\\n\\\\nLiteLLM: model group \\\\\\'claude-sonnet\\\\\\' failed with the error above. No fallback was attempted.\\'}}')Traceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2597, in _generate\n raw_response = self._create(payload)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2176, in _create\n return self._client.messages.with_raw_response.create(**payload)\n ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_response.py\", line 797, in wrapped\n return cast(APIResponse[R], func(*args, **kwargs))\n ~~~~^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_utils/_utils.py\", line 294, in wrapper\n return func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/resources/messages/messages.py\", line 1027, in create\n return self._post(\n ~~~~~~~~~~^\n \"/v1/messages\",\n ^^^^^^^^^^^^^^^\n ...<23 lines>...\n stream_cls=Stream[RawMessageStreamEvent],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_base_client.py\", line 1543, in post\n return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls))\n ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_base_client.py\", line 1208, in request\n raise self._make_status_error_from_response(response) from None\n\n\nanthropic.AuthenticationError: Error code: 401 - {'type': 'error', 'error': {'type': 'authentication_error', 'message': 'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\n\\nLiteLLM: model group \\'claude-sonnet\\' failed with the error above. No fallback was attempted.'}}\n\n\n\nThe above exception was the direct cause of the following exception:\n\n\n\nTraceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 707, in invoke\n input = context.run(step.invoke, input, config, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 3913, in invoke\n for chunk in self.stream(\n ~~~~~~~~~~~^\n input,\n ^^^^^^\n ...<11 lines>...\n **kwargs,\n ^^^^^^^^^\n ):\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 2967, in stream\n for _ in runner.tick(\n ~~~~~~~~~~~^\n [t for t in loop.tasks.values() if not t.writes],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<2 lines>...\n schedule_task=loop.accept_push,\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ):\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_runner.py\", line 207, in tick\n run_with_retry(\n ~~~~~~~~~~~~~~^\n t,\n ^^\n ...<10 lines>...\n },\n ^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_retry.py\", line 617, in run_with_retry\n return task.proc.invoke(task.input, config)\n ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 707, in invoke\n input = context.run(step.invoke, input, config, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 447, in invoke\n ret = self.func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 1543, in model_node\n result = wrap_model_call_handler(request, _execute_model_sync)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 357, in composed\n outer_result = outer(request, inner_handler)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/deepagents/middleware/filesystem.py\", line 3218, in wrap_model_call\n response = handler(request)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 347, in inner_handler\n inner_result = inner(req, handler)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/deepagents/middleware/unsupported_content.py\", line 191, in wrap_model_call\n return handler(self._filter_request(request))\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 1506, in _execute_model_sync\n output = model_.invoke(messages)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/runnables/base.py\", line 6014, in invoke\n return self.bound.invoke(\n ~~~~~~~~~~~~~~~~~^\n input,\n ^^^^^^\n self._merge_configs(config),\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n **{**self.kwargs, **kwargs},\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 488, in invoke\n self.generate_prompt(\n ~~~~~~~~~~~~~~~~~~~~^\n [self._convert_input(input)],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<6 lines>...\n **kwargs,\n ^^^^^^^^^\n ).generations[0][0],\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 1877, in generate_prompt\n return self.generate(prompt_messages, stop=stop, callbacks=callbacks, **kwargs)\n ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 1684, in generate\n self._generate_with_cache(\n ~~~~~~~~~~~~~~~~~~~~~~~~~^\n m,\n ^^\n ...<2 lines>...\n **kwargs,\n ^^^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 2022, in _generate_with_cache\n result = self._generate(\n messages, stop=stop, run_manager=run_manager, **kwargs\n )\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2601, in _generate\n _handle_anthropic_api_error(e)\n ~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 1295, in _handle_anthropic_api_error\n raise AnthropicAuthenticationError(\n message=e.message, response=e.response, body=e.body\n ) from e\n\n\nlangchain_anthropic.chat_models.AnthropicAuthenticationError: Error code: 401 - {'type': 'error', 'error': {'type': 'authentication_error', 'message': 'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\n\\nLiteLLM: model group \\'claude-sonnet\\' failed with the error above. No fallback was attempted.'}}\n\n\nDuring task with name 'model' and id '2e4e0d8e-70a5-697c-c7f4-e1f685fb1a31'" + } + }, + { + "key": "exception.stacktrace", + "value": { + "stringValue": "Exception: AnthropicAuthenticationError('Error code: 401 - {\\'type\\': \\'error\\', \\'error\\': {\\'type\\': \\'authentication_error\\', \\'message\\': \\'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\\\n\\\\nLiteLLM: model group \\\\\\'claude-sonnet\\\\\\' failed with the error above. No fallback was attempted.\\'}}')Traceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2597, in _generate\n raw_response = self._create(payload)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2176, in _create\n return self._client.messages.with_raw_response.create(**payload)\n ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_response.py\", line 797, in wrapped\n return cast(APIResponse[R], func(*args, **kwargs))\n ~~~~^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_utils/_utils.py\", line 294, in wrapper\n return func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/resources/messages/messages.py\", line 1027, in create\n return self._post(\n ~~~~~~~~~~^\n \"/v1/messages\",\n ^^^^^^^^^^^^^^^\n ...<23 lines>...\n stream_cls=Stream[RawMessageStreamEvent],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_base_client.py\", line 1543, in post\n return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls))\n ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_base_client.py\", line 1208, in request\n raise self._make_status_error_from_response(response) from None\n\n\nanthropic.AuthenticationError: Error code: 401 - {'type': 'error', 'error': {'type': 'authentication_error', 'message': 'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\n\\nLiteLLM: model group \\'claude-sonnet\\' failed with the error above. No fallback was attempted.'}}\n\n\n\nThe above exception was the direct cause of the following exception:\n\n\n\nTraceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 707, in invoke\n input = context.run(step.invoke, input, config, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 3913, in invoke\n for chunk in self.stream(\n ~~~~~~~~~~~^\n input,\n ^^^^^^\n ...<11 lines>...\n **kwargs,\n ^^^^^^^^^\n ):\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 2967, in stream\n for _ in runner.tick(\n ~~~~~~~~~~~^\n [t for t in loop.tasks.values() if not t.writes],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<2 lines>...\n schedule_task=loop.accept_push,\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ):\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_runner.py\", line 207, in tick\n run_with_retry(\n ~~~~~~~~~~~~~~^\n t,\n ^^\n ...<10 lines>...\n },\n ^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_retry.py\", line 617, in run_with_retry\n return task.proc.invoke(task.input, config)\n ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 707, in invoke\n input = context.run(step.invoke, input, config, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 447, in invoke\n ret = self.func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 1543, in model_node\n result = wrap_model_call_handler(request, _execute_model_sync)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 357, in composed\n outer_result = outer(request, inner_handler)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/deepagents/middleware/filesystem.py\", line 3218, in wrap_model_call\n response = handler(request)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 347, in inner_handler\n inner_result = inner(req, handler)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/deepagents/middleware/unsupported_content.py\", line 191, in wrap_model_call\n return handler(self._filter_request(request))\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 1506, in _execute_model_sync\n output = model_.invoke(messages)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/runnables/base.py\", line 6014, in invoke\n return self.bound.invoke(\n ~~~~~~~~~~~~~~~~~^\n input,\n ^^^^^^\n self._merge_configs(config),\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n **{**self.kwargs, **kwargs},\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 488, in invoke\n self.generate_prompt(\n ~~~~~~~~~~~~~~~~~~~~^\n [self._convert_input(input)],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<6 lines>...\n **kwargs,\n ^^^^^^^^^\n ).generations[0][0],\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 1877, in generate_prompt\n return self.generate(prompt_messages, stop=stop, callbacks=callbacks, **kwargs)\n ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 1684, in generate\n self._generate_with_cache(\n ~~~~~~~~~~~~~~~~~~~~~~~~~^\n m,\n ^^\n ...<2 lines>...\n **kwargs,\n ^^^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 2022, in _generate_with_cache\n result = self._generate(\n messages, stop=stop, run_manager=run_manager, **kwargs\n )\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2601, in _generate\n _handle_anthropic_api_error(e)\n ~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 1295, in _handle_anthropic_api_error\n raise AnthropicAuthenticationError(\n message=e.message, response=e.response, body=e.body\n ) from e\n\n\nlangchain_anthropic.chat_models.AnthropicAuthenticationError: Error code: 401 - {'type': 'error', 'error': {'type': 'authentication_error', 'message': 'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\n\\nLiteLLM: model group \\'claude-sonnet\\' failed with the error above. No fallback was attempted.'}}\n\n\nDuring task with name 'model' and id '2e4e0d8e-70a5-697c-c7f4-e1f685fb1a31'\n" + } + }, + { + "key": "exception.escaped", + "value": { + "stringValue": "False" + } + } + ] + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "7b787d5c997d768a725a3a2f0278e857", + "spanId": "11f6195c25249ab1", + "parentSpanId": "9e65698af98970b0", + "name": "LangGraph", + "kind": 1, + "startTimeUnixNano": "1790967512019313152", + "endTimeUnixNano": "1790967512630693888", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "e30=" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "LangGraph" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langgraph" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W3sicm9sZSI6InVzZXIiLCJjb250ZW50IjoiRm9yIGEgc3ludGhldGljIHRyYWNpbmcgZml4dHVyZSwgZXhwbGFpbiBQeXRob24gdHVwbGUgdmVyc3VzIGxpc3QgaW1tdXRhYmlsaXR5IGluIGF0IG1vc3QgMTIwIHdvcmRzLiBVc2Ugb25seSBvZmZpY2lhbCBQeXRob24gZG9jdW1lbnRhdGlvbiBpZiB5b3Ugc2VhcmNoLiBIYXZlIHRoZSBza2VwdGljIGFuZCB2ZXJpZmllciBjaGVjayBpdCwgdGhlbiBoYXZlIHRoZSBlZGl0b3Igd3JpdGUgL2Fuc3dlci5tZC4gS2VlcCB0aGUgcnVuIHNob3J0IGFuZCBkbyBub3QgYXNrIGZvbGxvdy11cCBxdWVzdGlvbnMuIn1dfQ==" + } + } + ], + "events": [ + { + "timeUnixNano": "1790967512687350000", + "name": "exception", + "attributes": [ + { + "key": "exception.type", + "value": { + "stringValue": "Exception" + } + }, + { + "key": "exception.message", + "value": { + "stringValue": "AnthropicAuthenticationError('Error code: 401 - {\\'type\\': \\'error\\', \\'error\\': {\\'type\\': \\'authentication_error\\', \\'message\\': \\'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\\\n\\\\nLiteLLM: model group \\\\\\'claude-sonnet\\\\\\' failed with the error above. No fallback was attempted.\\'}}')Traceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2597, in _generate\n raw_response = self._create(payload)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2176, in _create\n return self._client.messages.with_raw_response.create(**payload)\n ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_response.py\", line 797, in wrapped\n return cast(APIResponse[R], func(*args, **kwargs))\n ~~~~^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_utils/_utils.py\", line 294, in wrapper\n return func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/resources/messages/messages.py\", line 1027, in create\n return self._post(\n ~~~~~~~~~~^\n \"/v1/messages\",\n ^^^^^^^^^^^^^^^\n ...<23 lines>...\n stream_cls=Stream[RawMessageStreamEvent],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_base_client.py\", line 1543, in post\n return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls))\n ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_base_client.py\", line 1208, in request\n raise self._make_status_error_from_response(response) from None\n\n\nanthropic.AuthenticationError: Error code: 401 - {'type': 'error', 'error': {'type': 'authentication_error', 'message': 'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\n\\nLiteLLM: model group \\'claude-sonnet\\' failed with the error above. No fallback was attempted.'}}\n\n\n\nThe above exception was the direct cause of the following exception:\n\n\n\nTraceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 2967, in stream\n for _ in runner.tick(\n ~~~~~~~~~~~^\n [t for t in loop.tasks.values() if not t.writes],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<2 lines>...\n schedule_task=loop.accept_push,\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ):\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_runner.py\", line 207, in tick\n run_with_retry(\n ~~~~~~~~~~~~~~^\n t,\n ^^\n ...<10 lines>...\n },\n ^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_retry.py\", line 617, in run_with_retry\n return task.proc.invoke(task.input, config)\n ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 707, in invoke\n input = context.run(step.invoke, input, config, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 3913, in invoke\n for chunk in self.stream(\n ~~~~~~~~~~~^\n input,\n ^^^^^^\n ...<11 lines>...\n **kwargs,\n ^^^^^^^^^\n ):\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 2967, in stream\n for _ in runner.tick(\n ~~~~~~~~~~~^\n [t for t in loop.tasks.values() if not t.writes],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<2 lines>...\n schedule_task=loop.accept_push,\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ):\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_runner.py\", line 207, in tick\n run_with_retry(\n ~~~~~~~~~~~~~~^\n t,\n ^^\n ...<10 lines>...\n },\n ^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_retry.py\", line 617, in run_with_retry\n return task.proc.invoke(task.input, config)\n ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 707, in invoke\n input = context.run(step.invoke, input, config, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 447, in invoke\n ret = self.func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 1543, in model_node\n result = wrap_model_call_handler(request, _execute_model_sync)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 357, in composed\n outer_result = outer(request, inner_handler)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/deepagents/middleware/filesystem.py\", line 3218, in wrap_model_call\n response = handler(request)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 347, in inner_handler\n inner_result = inner(req, handler)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/deepagents/middleware/unsupported_content.py\", line 191, in wrap_model_call\n return handler(self._filter_request(request))\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 1506, in _execute_model_sync\n output = model_.invoke(messages)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/runnables/base.py\", line 6014, in invoke\n return self.bound.invoke(\n ~~~~~~~~~~~~~~~~~^\n input,\n ^^^^^^\n self._merge_configs(config),\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n **{**self.kwargs, **kwargs},\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 488, in invoke\n self.generate_prompt(\n ~~~~~~~~~~~~~~~~~~~~^\n [self._convert_input(input)],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<6 lines>...\n **kwargs,\n ^^^^^^^^^\n ).generations[0][0],\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 1877, in generate_prompt\n return self.generate(prompt_messages, stop=stop, callbacks=callbacks, **kwargs)\n ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 1684, in generate\n self._generate_with_cache(\n ~~~~~~~~~~~~~~~~~~~~~~~~~^\n m,\n ^^\n ...<2 lines>...\n **kwargs,\n ^^^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 2022, in _generate_with_cache\n result = self._generate(\n messages, stop=stop, run_manager=run_manager, **kwargs\n )\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2601, in _generate\n _handle_anthropic_api_error(e)\n ~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 1295, in _handle_anthropic_api_error\n raise AnthropicAuthenticationError(\n message=e.message, response=e.response, body=e.body\n ) from e\n\n\nlangchain_anthropic.chat_models.AnthropicAuthenticationError: Error code: 401 - {'type': 'error', 'error': {'type': 'authentication_error', 'message': 'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\n\\nLiteLLM: model group \\'claude-sonnet\\' failed with the error above. No fallback was attempted.'}}\n\n\nDuring task with name 'model' and id '2e4e0d8e-70a5-697c-c7f4-e1f685fb1a31'\n\n\nDuring task with name 'researcher' and id '816a3f65-9150-0b6d-54aa-920fdcfe25cd'" + } + }, + { + "key": "exception.stacktrace", + "value": { + "stringValue": "Exception: AnthropicAuthenticationError('Error code: 401 - {\\'type\\': \\'error\\', \\'error\\': {\\'type\\': \\'authentication_error\\', \\'message\\': \\'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\\\n\\\\nLiteLLM: model group \\\\\\'claude-sonnet\\\\\\' failed with the error above. No fallback was attempted.\\'}}')Traceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2597, in _generate\n raw_response = self._create(payload)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2176, in _create\n return self._client.messages.with_raw_response.create(**payload)\n ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_response.py\", line 797, in wrapped\n return cast(APIResponse[R], func(*args, **kwargs))\n ~~~~^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_utils/_utils.py\", line 294, in wrapper\n return func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/resources/messages/messages.py\", line 1027, in create\n return self._post(\n ~~~~~~~~~~^\n \"/v1/messages\",\n ^^^^^^^^^^^^^^^\n ...<23 lines>...\n stream_cls=Stream[RawMessageStreamEvent],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_base_client.py\", line 1543, in post\n return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls))\n ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_base_client.py\", line 1208, in request\n raise self._make_status_error_from_response(response) from None\n\n\nanthropic.AuthenticationError: Error code: 401 - {'type': 'error', 'error': {'type': 'authentication_error', 'message': 'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\n\\nLiteLLM: model group \\'claude-sonnet\\' failed with the error above. No fallback was attempted.'}}\n\n\n\nThe above exception was the direct cause of the following exception:\n\n\n\nTraceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 2967, in stream\n for _ in runner.tick(\n ~~~~~~~~~~~^\n [t for t in loop.tasks.values() if not t.writes],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<2 lines>...\n schedule_task=loop.accept_push,\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ):\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_runner.py\", line 207, in tick\n run_with_retry(\n ~~~~~~~~~~~~~~^\n t,\n ^^\n ...<10 lines>...\n },\n ^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_retry.py\", line 617, in run_with_retry\n return task.proc.invoke(task.input, config)\n ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 707, in invoke\n input = context.run(step.invoke, input, config, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 3913, in invoke\n for chunk in self.stream(\n ~~~~~~~~~~~^\n input,\n ^^^^^^\n ...<11 lines>...\n **kwargs,\n ^^^^^^^^^\n ):\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 2967, in stream\n for _ in runner.tick(\n ~~~~~~~~~~~^\n [t for t in loop.tasks.values() if not t.writes],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<2 lines>...\n schedule_task=loop.accept_push,\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ):\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_runner.py\", line 207, in tick\n run_with_retry(\n ~~~~~~~~~~~~~~^\n t,\n ^^\n ...<10 lines>...\n },\n ^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_retry.py\", line 617, in run_with_retry\n return task.proc.invoke(task.input, config)\n ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 707, in invoke\n input = context.run(step.invoke, input, config, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 447, in invoke\n ret = self.func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 1543, in model_node\n result = wrap_model_call_handler(request, _execute_model_sync)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 357, in composed\n outer_result = outer(request, inner_handler)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/deepagents/middleware/filesystem.py\", line 3218, in wrap_model_call\n response = handler(request)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 347, in inner_handler\n inner_result = inner(req, handler)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/deepagents/middleware/unsupported_content.py\", line 191, in wrap_model_call\n return handler(self._filter_request(request))\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 1506, in _execute_model_sync\n output = model_.invoke(messages)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/runnables/base.py\", line 6014, in invoke\n return self.bound.invoke(\n ~~~~~~~~~~~~~~~~~^\n input,\n ^^^^^^\n self._merge_configs(config),\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n **{**self.kwargs, **kwargs},\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 488, in invoke\n self.generate_prompt(\n ~~~~~~~~~~~~~~~~~~~~^\n [self._convert_input(input)],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<6 lines>...\n **kwargs,\n ^^^^^^^^^\n ).generations[0][0],\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 1877, in generate_prompt\n return self.generate(prompt_messages, stop=stop, callbacks=callbacks, **kwargs)\n ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 1684, in generate\n self._generate_with_cache(\n ~~~~~~~~~~~~~~~~~~~~~~~~~^\n m,\n ^^\n ...<2 lines>...\n **kwargs,\n ^^^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 2022, in _generate_with_cache\n result = self._generate(\n messages, stop=stop, run_manager=run_manager, **kwargs\n )\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2601, in _generate\n _handle_anthropic_api_error(e)\n ~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 1295, in _handle_anthropic_api_error\n raise AnthropicAuthenticationError(\n message=e.message, response=e.response, body=e.body\n ) from e\n\n\nlangchain_anthropic.chat_models.AnthropicAuthenticationError: Error code: 401 - {'type': 'error', 'error': {'type': 'authentication_error', 'message': 'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\n\\nLiteLLM: model group \\'claude-sonnet\\' failed with the error above. No fallback was attempted.'}}\n\n\nDuring task with name 'model' and id '2e4e0d8e-70a5-697c-c7f4-e1f685fb1a31'\n\n\nDuring task with name 'researcher' and id '816a3f65-9150-0b6d-54aa-920fdcfe25cd'\n" + } + }, + { + "key": "exception.escaped", + "value": { + "stringValue": "False" + } + } + ] + } + ], + "status": { + "code": 1 + }, + "flags": 256 + } + ] + }, + { + "scope": { + "name": "deeplite" + }, + "spans": [ + { + "traceId": "7b787d5c997d768a725a3a2f0278e857", + "spanId": "9e65698af98970b0", + "name": "deeplite.run", + "kind": 1, + "startTimeUnixNano": "1790967512013991000", + "endTimeUnixNano": "1790967512696195000", + "attributes": [ + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "input.value", + "value": { + "stringValue": "For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions." + } + } + ], + "events": [ + { + "timeUnixNano": "1790967512696180000", + "name": "exception", + "attributes": [ + { + "key": "exception.type", + "value": { + "stringValue": "langchain_anthropic.chat_models.AnthropicAuthenticationError" + } + }, + { + "key": "exception.message", + "value": { + "stringValue": "Error code: 401 - {'type': 'error', 'error': {'type': 'authentication_error', 'message': 'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\n\\nLiteLLM: model group \\'claude-sonnet\\' failed with the error above. No fallback was attempted.'}}" + } + }, + { + "key": "exception.stacktrace", + "value": { + "stringValue": "Traceback (most recent call last):\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2597, in _generate\n raw_response = self._create(payload)\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2176, in _create\n return self._client.messages.with_raw_response.create(**payload)\n ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_response.py\", line 797, in wrapped\n return cast(APIResponse[R], func(*args, **kwargs))\n ~~~~^^^^^^^^^^^^^^^^^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_utils/_utils.py\", line 294, in wrapper\n return func(*args, **kwargs)\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/resources/messages/messages.py\", line 1027, in create\n return self._post(\n ~~~~~~~~~~^\n \"/v1/messages\",\n ^^^^^^^^^^^^^^^\n ...<23 lines>...\n stream_cls=Stream[RawMessageStreamEvent],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n )\n ^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_base_client.py\", line 1543, in post\n return cast(ResponseT, self.request(cast_to, opts, stream=stream, stream_cls=stream_cls))\n ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/anthropic/_base_client.py\", line 1208, in request\n raise self._make_status_error_from_response(response) from None\nanthropic.AuthenticationError: Error code: 401 - {'type': 'error', 'error': {'type': 'authentication_error', 'message': 'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\n\\nLiteLLM: model group \\'claude-sonnet\\' failed with the error above. No fallback was attempted.'}}\n\nThe above exception was the direct cause of the following exception:\n\nTraceback (most recent call last):\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/opentelemetry/trace/__init__.py\", line 602, in use_span\n yield span\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/opentelemetry/sdk/trace/__init__.py\", line 1136, in start_as_current_span\n yield span\n File \"/workspace/deeplite/src/deeplite/cli.py\", line 22, in run_task\n result: Final = agent.invoke(\n ~~~~~~~~~~~~^\n {\"messages\": [{\"role\": \"user\", \"content\": task}]}, config={\"recursion_limit\": 40}\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n )\n ^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 3913, in invoke\n for chunk in self.stream(\n ~~~~~~~~~~~^\n input,\n ^^^^^^\n ...<11 lines>...\n **kwargs,\n ^^^^^^^^^\n ):\n ^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 2967, in stream\n for _ in runner.tick(\n ~~~~~~~~~~~^\n [t for t in loop.tasks.values() if not t.writes],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<2 lines>...\n schedule_task=loop.accept_push,\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ):\n ^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_runner.py\", line 207, in tick\n run_with_retry(\n ~~~~~~~~~~~~~~^\n t,\n ^^\n ...<10 lines>...\n },\n ^^\n )\n ^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_retry.py\", line 617, in run_with_retry\n return task.proc.invoke(task.input, config)\n ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 707, in invoke\n input = context.run(step.invoke, input, config, **kwargs)\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 3913, in invoke\n for chunk in self.stream(\n ~~~~~~~~~~~^\n input,\n ^^^^^^\n ...<11 lines>...\n **kwargs,\n ^^^^^^^^^\n ):\n ^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 2967, in stream\n for _ in runner.tick(\n ~~~~~~~~~~~^\n [t for t in loop.tasks.values() if not t.writes],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<2 lines>...\n schedule_task=loop.accept_push,\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ):\n ^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_runner.py\", line 207, in tick\n run_with_retry(\n ~~~~~~~~~~~~~~^\n t,\n ^^\n ...<10 lines>...\n },\n ^^\n )\n ^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_retry.py\", line 617, in run_with_retry\n return task.proc.invoke(task.input, config)\n ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 707, in invoke\n input = context.run(step.invoke, input, config, **kwargs)\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 447, in invoke\n ret = self.func(*args, **kwargs)\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 1543, in model_node\n result = wrap_model_call_handler(request, _execute_model_sync)\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 357, in composed\n outer_result = outer(request, inner_handler)\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/deepagents/middleware/filesystem.py\", line 3218, in wrap_model_call\n response = handler(request)\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 347, in inner_handler\n inner_result = inner(req, handler)\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/deepagents/middleware/unsupported_content.py\", line 191, in wrap_model_call\n return handler(self._filter_request(request))\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain/agents/factory.py\", line 1506, in _execute_model_sync\n output = model_.invoke(messages)\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/runnables/base.py\", line 6014, in invoke\n return self.bound.invoke(\n ~~~~~~~~~~~~~~~~~^\n input,\n ^^^^^^\n self._merge_configs(config),\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n **{**self.kwargs, **kwargs},\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n )\n ^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 488, in invoke\n self.generate_prompt(\n ~~~~~~~~~~~~~~~~~~~~^\n [self._convert_input(input)],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<6 lines>...\n **kwargs,\n ^^^^^^^^^\n ).generations[0][0],\n ^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 1877, in generate_prompt\n return self.generate(prompt_messages, stop=stop, callbacks=callbacks, **kwargs)\n ~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 1684, in generate\n self._generate_with_cache(\n ~~~~~~~~~~~~~~~~~~~~~~~~~^\n m,\n ^^\n ...<2 lines>...\n **kwargs,\n ^^^^^^^^^\n )\n ^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_core/language_models/chat_models.py\", line 2022, in _generate_with_cache\n result = self._generate(\n messages, stop=stop, run_manager=run_manager, **kwargs\n )\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 2601, in _generate\n _handle_anthropic_api_error(e)\n ~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langchain_anthropic/chat_models.py\", line 1295, in _handle_anthropic_api_error\n raise AnthropicAuthenticationError(\n message=e.message, response=e.response, body=e.body\n ) from e\nlangchain_anthropic.chat_models.AnthropicAuthenticationError: Error code: 401 - {'type': 'error', 'error': {'type': 'authentication_error', 'message': 'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\n\\nLiteLLM: model group \\'claude-sonnet\\' failed with the error above. No fallback was attempted.'}}\nDuring task with name 'model' and id '2e4e0d8e-70a5-697c-c7f4-e1f685fb1a31'\nDuring task with name 'researcher' and id '816a3f65-9150-0b6d-54aa-920fdcfe25cd'\n" + } + }, + { + "key": "exception.escaped", + "value": { + "stringValue": "False" + } + } + ] + } + ], + "status": { + "message": "AnthropicAuthenticationError: Error code: 401 - {'type': 'error', 'error': {'type': 'authentication_error', 'message': 'litellm.AuthenticationError: AnthropicException - {\"type\":\"error\",\"error\":{\"type\":\"authentication_error\",\"message\":\"API key is invalid.\"},\"request_id\":null}\\n\\nLiteLLM: model group \\'claude-sonnet\\' failed with the error above. No fallback was attempted.'}}", + "code": 2 + }, + "flags": 256 + } + ] + } + ] + } + ] +} diff --git a/litellm-rust/crates/traces/tests/fixtures/deeplite_swarm.json b/litellm-rust/crates/traces/tests/fixtures/deeplite_swarm.json new file mode 100644 index 00000000000..19351078af8 --- /dev/null +++ b/litellm-rust/crates/traces/tests/fixtures/deeplite_swarm.json @@ -0,0 +1,9812 @@ +{ + "resourceSpans": [ + { + "resource": { + "attributes": [ + { + "key": "telemetry.sdk.language", + "value": { + "stringValue": "python" + } + }, + { + "key": "telemetry.sdk.name", + "value": { + "stringValue": "opentelemetry" + } + }, + { + "key": "telemetry.sdk.version", + "value": { + "stringValue": "1.45.0" + } + }, + { + "key": "service.instance.id", + "value": { + "stringValue": "b873df4c-5106-4097-a20b-b9380724f4d9" + } + }, + { + "key": "service.name", + "value": { + "stringValue": "deeplite" + } + } + ] + }, + "scopeSpans": [ + { + "scope": { + "name": "langsmith" + }, + "spans": [ + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "1b73fdc79da730c3", + "parentSpanId": "04ffc0db9ce0f358", + "name": "__start__", + "kind": 1, + "startTimeUnixNano": "1790968013211052032", + "endTimeUnixNano": "1790968013212278016", + "attributes": [ + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "__start__" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langgraph" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "0" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "__start__" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"__start__\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"__start__\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "__start__:a08efee1-6b20-6c11-d2f1-f04016499ab0" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "langsmith:hidden, graph:step:0" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W3sicm9sZSI6InVzZXIiLCJjb250ZW50IjoiRm9yIGEgc3ludGhldGljIHRyYWNpbmcgZml4dHVyZSwgZXhwbGFpbiBQeXRob24gdHVwbGUgdmVyc3VzIGxpc3QgaW1tdXRhYmlsaXR5IGluIGF0IG1vc3QgMTIwIHdvcmRzLiBVc2Ugb25seSBvZmZpY2lhbCBQeXRob24gZG9jdW1lbnRhdGlvbiBpZiB5b3Ugc2VhcmNoLiBIYXZlIHRoZSBza2VwdGljIGFuZCB2ZXJpZmllciBjaGVjayBpdCwgdGhlbiBoYXZlIHRoZSBlZGl0b3Igd3JpdGUgL2Fuc3dlci5tZC4gS2VlcCB0aGUgcnVuIHNob3J0IGFuZCBkbyBub3QgYXNrIGZvbGxvdy11cCBxdWVzdGlvbnMuIn1dfQ==" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W3sicm9sZSI6InVzZXIiLCJjb250ZW50IjoiRm9yIGEgc3ludGhldGljIHRyYWNpbmcgZml4dHVyZSwgZXhwbGFpbiBQeXRob24gdHVwbGUgdmVyc3VzIGxpc3QgaW1tdXRhYmlsaXR5IGluIGF0IG1vc3QgMTIwIHdvcmRzLiBVc2Ugb25seSBvZmZpY2lhbCBQeXRob24gZG9jdW1lbnRhdGlvbiBpZiB5b3Ugc2VhcmNoLiBIYXZlIHRoZSBza2VwdGljIGFuZCB2ZXJpZmllciBjaGVjayBpdCwgdGhlbiBoYXZlIHRoZSBlZGl0b3Igd3JpdGUgL2Fuc3dlci5tZC4gS2VlcCB0aGUgcnVuIHNob3J0IGFuZCBkbyBub3QgYXNrIGZvbGxvdy11cCBxdWVzdGlvbnMuIn1dfQ==" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "42058e9b69a517f4", + "parentSpanId": "1b73fdc79da730c3", + "name": "route_to_active_agent", + "kind": 1, + "startTimeUnixNano": "1790968013211607808", + "endTimeUnixNano": "1790968013211869952", + "attributes": [ + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "route_to_active_agent" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langgraph" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "0" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "__start__" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"__start__\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"__start__\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "__start__:a08efee1-6b20-6c11-d2f1-f04016499ab0" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "langsmith:hidden, seq:step:2" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W3siY29udGVudCI6IkZvciBhIHN5bnRoZXRpYyB0cmFjaW5nIGZpeHR1cmUsIGV4cGxhaW4gUHl0aG9uIHR1cGxlIHZlcnN1cyBsaXN0IGltbXV0YWJpbGl0eSBpbiBhdCBtb3N0IDEyMCB3b3Jkcy4gVXNlIG9ubHkgb2ZmaWNpYWwgUHl0aG9uIGRvY3VtZW50YXRpb24gaWYgeW91IHNlYXJjaC4gSGF2ZSB0aGUgc2tlcHRpYyBhbmQgdmVyaWZpZXIgY2hlY2sgaXQsIHRoZW4gaGF2ZSB0aGUgZWRpdG9yIHdyaXRlIC9hbnN3ZXIubWQuIEtlZXAgdGhlIHJ1biBzaG9ydCBhbmQgZG8gbm90IGFzayBmb2xsb3ctdXAgcXVlc3Rpb25zLiIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoiaHVtYW4iLCJpZCI6IjkxNTRlMTFlLWQyMTItNDg5OC04YjRiLTJjYzBlMGI3OTEwZCJ9XX0=" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJvdXRwdXQiOiJyZXNlYXJjaGVyIn0=" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + } + ] + } + ] + }, + { + "resource": { + "attributes": [ + { + "key": "telemetry.sdk.language", + "value": { + "stringValue": "python" + } + }, + { + "key": "telemetry.sdk.name", + "value": { + "stringValue": "opentelemetry" + } + }, + { + "key": "telemetry.sdk.version", + "value": { + "stringValue": "1.45.0" + } + }, + { + "key": "service.instance.id", + "value": { + "stringValue": "b873df4c-5106-4097-a20b-b9380724f4d9" + } + }, + { + "key": "service.name", + "value": { + "stringValue": "deeplite" + } + } + ] + }, + "scopeSpans": [ + { + "scope": { + "name": "langsmith" + }, + "spans": [ + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "57b096e0e89c429f", + "parentSpanId": "08f0feca38b72d47", + "name": "model", + "kind": 1, + "startTimeUnixNano": "1790968013213167872", + "endTimeUnixNano": "1790968018770104832", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "1" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828|model:8421bdfe-880b-cb10-d152-84cb9d9551f4" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "researcher" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "graph:step:1" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W3siY29udGVudCI6IkZvciBhIHN5bnRoZXRpYyB0cmFjaW5nIGZpeHR1cmUsIGV4cGxhaW4gUHl0aG9uIHR1cGxlIHZlcnN1cyBsaXN0IGltbXV0YWJpbGl0eSBpbiBhdCBtb3N0IDEyMCB3b3Jkcy4gVXNlIG9ubHkgb2ZmaWNpYWwgUHl0aG9uIGRvY3VtZW50YXRpb24gaWYgeW91IHNlYXJjaC4gSGF2ZSB0aGUgc2tlcHRpYyBhbmQgdmVyaWZpZXIgY2hlY2sgaXQsIHRoZW4gaGF2ZSB0aGUgZWRpdG9yIHdyaXRlIC9hbnN3ZXIubWQuIEtlZXAgdGhlIHJ1biBzaG9ydCBhbmQgZG8gbm90IGFzayBmb2xsb3ctdXAgcXVlc3Rpb25zLiIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoiaHVtYW4iLCJpZCI6IjQ2MjA0MzA1LWY4ZWUtNDM4NC05Y2U4LWZjZDgwYmZjZGY3NSJ9XX0=" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJvdXRwdXQiOlt7ImdyYXBoIjpudWxsLCJ1cGRhdGUiOnsibWVzc2FnZXMiOlt7ImNvbnRlbnQiOlt7ImlkIjoidG9vbHVfMDFCMkxmSEdEallTRkpobWlYTmo0Q3VRIiwiY2FsbGVyIjp7InR5cGUiOiJkaXJlY3QifSwiaW5wdXQiOnsicXVlcnkiOiJkb2NzLnB5dGhvbi5vcmcgdHVwbGVzIGltbXV0YWJsZSBzZXF1ZW5jZXMgbGlzdHMgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgdHV0b3JpYWwifSwibmFtZSI6IndlYl9zZWFyY2giLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3OWFXOWJyZW5QcWd5YmZOd1AiLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6Mjg0NSwib3V0cHV0X3Rva2VucyI6NzMsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjB9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InJlc2VhcmNoZXIiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtMjFhMy03MDAwLWEzODEtMjg0YTZhMGMwM2Y3LTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoid2ViX3NlYXJjaCIsImFyZ3MiOnsicXVlcnkiOiJkb2NzLnB5dGhvbi5vcmcgdHVwbGVzIGltbXV0YWJsZSBzZXF1ZW5jZXMgbGlzdHMgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgdHV0b3JpYWwifSwiaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJ0eXBlIjoidG9vbF9jYWxsIn1dLCJpbnZhbGlkX3Rvb2xfY2FsbHMiOltdLCJ1c2FnZV9tZXRhZGF0YSI6eyJpbnB1dF90b2tlbnMiOjI4NDUsIm91dHB1dF90b2tlbnMiOjczLCJ0b3RhbF90b2tlbnMiOjI5MTgsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjB9fX1dfSwicmVzdW1lIjpudWxsLCJnb3RvIjpbXX1dfQ==" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "3535364df77cec3a", + "parentSpanId": "57b096e0e89c429f", + "name": "FilesystemMiddleware.wrap_model_call", + "kind": 1, + "startTimeUnixNano": "1790968013214705920", + "endTimeUnixNano": "1790968018769807104", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "e30=" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "FilesystemMiddleware.wrap_model_call" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "1" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828|model:8421bdfe-880b-cb10-d152-84cb9d9551f4" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "researcher" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828" + } + }, + { + "key": "langsmith.metadata.ls_method", + "value": { + "stringValue": "traceable" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJvdXRwdXQiOnsicmVzdWx0IjpbeyJjb250ZW50IjpbeyJpZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7InF1ZXJ5IjoiZG9jcy5weXRob24ub3JnIHR1cGxlcyBpbW11dGFibGUgc2VxdWVuY2VzIGxpc3RzIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIHR1dG9yaWFsIn0sIm5hbWUiOiJ3ZWJfc2VhcmNoIiwidHlwZSI6InRvb2xfdXNlIiwidG9vbHNldF9uYW1lIjpudWxsfV0sImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2ZkdzlhVzlicmVuUHFneWJmTndQIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjI4NDUsIm91dHB1dF90b2tlbnMiOjczLCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjowfSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCIsIm1vZGVsX3Byb3ZpZGVyIjoiYW50aHJvcGljIn0sInR5cGUiOiJhaSIsIm5hbWUiOiJyZXNlYXJjaGVyIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTIxYTMtNzAwMC1hMzgxLTI4NGE2YTBjMDNmNy0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6IndlYl9zZWFyY2giLCJhcmdzIjp7InF1ZXJ5IjoiZG9jcy5weXRob24ub3JnIHR1cGxlcyBpbW11dGFibGUgc2VxdWVuY2VzIGxpc3RzIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIHR1dG9yaWFsIn0sImlkIjoidG9vbHVfMDFCMkxmSEdEallTRkpobWlYTmo0Q3VRIiwidHlwZSI6InRvb2xfY2FsbCJ9XSwiaW52YWxpZF90b29sX2NhbGxzIjpbXSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjoyODQ1LCJvdXRwdXRfdG9rZW5zIjo3MywidG90YWxfdG9rZW5zIjoyOTE4LCJpbnB1dF90b2tlbl9kZXRhaWxzIjp7ImNhY2hlX3JlYWQiOjAsImNhY2hlX2NyZWF0aW9uIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VuX2RldGFpbHMiOnsicmVhc29uaW5nIjowfX19XSwic3RydWN0dXJlZF9yZXNwb25zZSI6bnVsbH19" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "005fac1918b863b6", + "parentSpanId": "3535364df77cec3a", + "name": "UnsupportedContentMiddleware.wrap_model_call", + "kind": 1, + "startTimeUnixNano": "1790968013215195904", + "endTimeUnixNano": "1790968018769697792", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "e30=" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "UnsupportedContentMiddleware.wrap_model_call" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "1" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828|model:8421bdfe-880b-cb10-d152-84cb9d9551f4" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "researcher" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828" + } + }, + { + "key": "langsmith.metadata.ls_method", + "value": { + "stringValue": "traceable" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJvdXRwdXQiOnsicmVzdWx0IjpbeyJjb250ZW50IjpbeyJpZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7InF1ZXJ5IjoiZG9jcy5weXRob24ub3JnIHR1cGxlcyBpbW11dGFibGUgc2VxdWVuY2VzIGxpc3RzIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIHR1dG9yaWFsIn0sIm5hbWUiOiJ3ZWJfc2VhcmNoIiwidHlwZSI6InRvb2xfdXNlIiwidG9vbHNldF9uYW1lIjpudWxsfV0sImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2ZkdzlhVzlicmVuUHFneWJmTndQIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjI4NDUsIm91dHB1dF90b2tlbnMiOjczLCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjowfSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCIsIm1vZGVsX3Byb3ZpZGVyIjoiYW50aHJvcGljIn0sInR5cGUiOiJhaSIsIm5hbWUiOiJyZXNlYXJjaGVyIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTIxYTMtNzAwMC1hMzgxLTI4NGE2YTBjMDNmNy0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6IndlYl9zZWFyY2giLCJhcmdzIjp7InF1ZXJ5IjoiZG9jcy5weXRob24ub3JnIHR1cGxlcyBpbW11dGFibGUgc2VxdWVuY2VzIGxpc3RzIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIHR1dG9yaWFsIn0sImlkIjoidG9vbHVfMDFCMkxmSEdEallTRkpobWlYTmo0Q3VRIiwidHlwZSI6InRvb2xfY2FsbCJ9XSwiaW52YWxpZF90b29sX2NhbGxzIjpbXSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjoyODQ1LCJvdXRwdXRfdG9rZW5zIjo3MywidG90YWxfdG9rZW5zIjoyOTE4LCJpbnB1dF90b2tlbl9kZXRhaWxzIjp7ImNhY2hlX3JlYWQiOjAsImNhY2hlX2NyZWF0aW9uIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VuX2RldGFpbHMiOnsicmVhc29uaW5nIjowfX19XSwic3RydWN0dXJlZF9yZXNwb25zZSI6bnVsbH19" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "3baa483880660b3b", + "parentSpanId": "005fac1918b863b6", + "name": "ChatAnthropic", + "kind": 1, + "startTimeUnixNano": "1790968013219345920", + "endTimeUnixNano": "1790968018769428992", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chat" + } + }, + { + "key": "gen_ai.serialized.name", + "value": { + "stringValue": "ChatAnthropic" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "llm" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "ChatAnthropic" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "anthropic" + } + }, + { + "key": "gen_ai.request.model", + "value": { + "stringValue": "claude-sonnet" + } + }, + { + "key": "gen_ai.tool.definitions", + "value": { + "stringValue": "[{\"name\":\"ls\",\"input_schema\":{\"properties\":{\"path\":{\"description\":\"Absolute path to the directory to list. Must be absolute, not relative.\",\"type\":\"string\"}},\"required\":[\"path\"],\"type\":\"object\"},\"description\":\"Lists all files in a directory.\\n\\nThis is useful for exploring the filesystem and finding the right file to read or edit.\\nYou should almost ALWAYS use this tool before using the read_file or edit_file tools.\"},{\"name\":\"read_file\",\"input_schema\":{\"properties\":{\"file_path\":{\"description\":\"Absolute path to the file to read. Must be absolute, not relative.\",\"type\":\"string\"},\"offset\":{\"default\":0,\"description\":\"Line number to start reading from (0-indexed). Use for pagination of large files.\",\"type\":\"integer\"},\"limit\":{\"default\":100,\"description\":\"Maximum number of lines to read. Use for pagination of large files.\",\"type\":\"integer\"}},\"required\":[\"file_path\"],\"type\":\"object\"},\"description\":\"Reads a file from the filesystem. Assume any path the user provides is valid; reading a missing file returns an error.\\n\\nUsage:\\n- By default, it reads up to 100 lines starting from the beginning of the file. Use `offset`/`limit` to page through large files instead of reading them whole.\\n- A status header, `@@ field | field | ... @@`, sits above the file content, and every line after it is verbatim file content. When content is truncated, there may be an explanation before the header. Never include the header when editing.\\n- Speculatively batch multiple `read_file` calls in one response when several files may be useful.\\n- An empty file returns a system-reminder warning in place of contents.\\n- Large tool results may be offloaded to a file; the tool message gives the path. Read that path here, paging with `offset`/`limit`.\\n- Images (`.png`, `.jpg`, etc.), audio, video, and PDFs return multimodal content blocks (https://docs.langchain.com/oss/python/langchain/messages#multimodal).\\n- For images and PDFs, pagination via `offset`/`limit` is text-only - supply `file_path` only\\n- Always read a file before editing it.\"},{\"name\":\"glob\",\"input_schema\":{\"properties\":{\"pattern\":{\"description\":\"Glob pattern to match files (e.g., '*.py', '**/*.py', '/subdir/**/*.md'). A pattern without '/' matches the file name at any depth; a pattern containing '/' matches the search-root-relative path; a leading '/' anchors to the search root ('/*.py' matches only top-level files). Leading-dot names are excluded unless the pattern segment starts with '.', so prefer the bare form '*.py' over '**/*.py' -- '**' will not descend into dot-directories like '.github'.\",\"type\":\"string\"},\"path\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"null\"}],\"default\":null,\"description\":\"Base directory to search from. Defaults to the backend's default root.\"}},\"required\":[\"pattern\"],\"type\":\"object\"},\"description\":\"Find files matching a glob pattern, returning absolute paths.\\n\\nSupports `*` (any characters within a path segment), `**` (any directories), `?` (single character), `[abc]` (one character from a set), and `{a,b}` (alternatives), e.g. `*.py`, `src/**/*.py`, `*.{yml,yaml}`.\\n\\nA pattern without `/` matches the file name at any depth under the search root (`*.py` matches `src/app/main.py`). A pattern containing `/` matches the search-root-relative path (`src/**/*.py`). A leading `/` anchors to the search root (`/*.py` matches only top-level Python files).\\n\\nLeading-dot names are only matched when the pattern segment itself starts with `.` (use `.env`, or `.github/**/*.yml`). Because `**` will not descend into dot-directories, the bare form `*.yml` is *broader* than `**/*.yml` and is usually what you want.\"},{\"name\":\"grep\",\"input_schema\":{\"properties\":{\"pattern\":{\"description\":\"Text pattern to search for (literal string, not regex).\",\"type\":\"string\"},\"path\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"null\"}],\"default\":null,\"description\":\"Directory to search in. Defaults to current working directory.\"},\"glob\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"null\"}],\"default\":null,\"description\":\"Glob pattern (NOT regex) limiting which files are searched (e.g. '*.py', '*.ts'). A pattern without '/' matches the file name at any depth; a pattern containing '/' matches the search-root-relative path (e.g. 'src/**/*.py'). This is an in-tool file filter, not a call to the separate glob tool. Brace expansion (e.g. '*.{ts,tsx}') is not supported on all backends; run a separate search per extension for reliable results.\"},\"output_mode\":{\"default\":\"files_with_matches\",\"description\":\"Shape of the returned text. 'files_with_matches' (default): newline-separated matching file paths. 'content': matching lines grouped by file under a ':' header, each line indented and formatted ': ' (only the matched line, no surrounding context). 'count': one ': ' line per file.\",\"enum\":[\"files_with_matches\",\"content\",\"count\"],\"type\":\"string\"},\"max_count\":{\"anyOf\":[{\"exclusiveMinimum\":0,\"type\":\"integer\"},{\"type\":\"null\"}],\"default\":null,\"description\":\"Optional cap on the total number of matches returned across all files. Leave unset to use the configured default. When the cap is hit, results are truncated and a note says so; narrow the pattern or path to see the rest.\"}},\"required\":[\"pattern\"],\"type\":\"object\"},\"description\":\"Search for a LITERAL text pattern across files (NOT regex).\\n\\nThe pattern is matched verbatim: regex metacharacters are ordinary characters, not operators. To match any of several strings, run a separate grep for each; `grep(pattern=\\\"foo|bar\\\")` searches for the literal text \\\"foo|bar\\\", and `.*` or `\\\\.` match those characters literally.\\n\\nReturns matching files or content per `output_mode`. Offloaded large tool results live under the artifacts root (`/large_tool_results/` by default); grep that directory to search them when you do not know the exact path.\"},{\"name\":\"web_search\",\"input_schema\":{\"properties\":{\"query\":{\"type\":\"string\"}},\"required\":[\"query\"],\"type\":\"object\"},\"description\":\"Search the web with Exa for current facts and return excerpts with source URLs\"},{\"name\":\"transfer_to_skeptic\",\"input_schema\":{\"properties\":{},\"type\":\"object\"},\"description\":\"Send findings to the skeptic for critique\"},{\"name\":\"transfer_to_verifier\",\"input_schema\":{\"properties\":{},\"type\":\"object\"},\"description\":\"Send revised findings for source verification\"}]" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_chat_model" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "1" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828|model:8421bdfe-880b-cb10-d152-84cb9d9551f4" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "researcher" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828" + } + }, + { + "key": "langsmith.metadata.ls_provider", + "value": { + "stringValue": "anthropic" + } + }, + { + "key": "langsmith.metadata.ls_model_name", + "value": { + "stringValue": "claude-sonnet" + } + }, + { + "key": "langsmith.metadata.ls_model_type", + "value": { + "stringValue": "chat" + } + }, + { + "key": "langsmith.metadata.ls_max_tokens", + "value": { + "intValue": "4096" + } + }, + { + "key": "langsmith.metadata.lc_versions", + "value": { + "stringValue": "{\"langchain-core\":\"1.6.6\",\"langchain\":\"1.4.3\",\"langchain-anthropic\":\"1.7.5\"}" + } + }, + { + "key": "langsmith.metadata.ls_method", + "value": { + "stringValue": "traceable" + } + }, + { + "key": "langsmith.metadata.model", + "value": { + "stringValue": "claude-sonnet" + } + }, + { + "key": "langsmith.metadata.max_tokens", + "value": { + "intValue": "4096" + } + }, + { + "key": "langsmith.metadata.model_kwargs", + "value": { + "stringValue": "{\"extra_body\":{\"extra_headers\":{\"anthropic-workspace-id\":\"[redacted workspace]\"}}}" + } + }, + { + "key": "langsmith.metadata.streaming", + "value": { + "boolValue": false + } + }, + { + "key": "langsmith.metadata.max_retries", + "value": { + "intValue": "2" + } + }, + { + "key": "langsmith.metadata._type", + "value": { + "stringValue": "anthropic-chat" + } + }, + { + "key": "langsmith.metadata.usage_metadata", + "value": { + "stringValue": "{\"input_tokens\":2845,\"output_tokens\":73,\"total_tokens\":2918,\"input_token_details\":{\"cache_read\":0,\"cache_creation\":0,\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_token_details\":{\"reasoning\":0}}" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "seq:step:1" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W1t7ImxjIjoxLCJ0eXBlIjoiY29uc3RydWN0b3IiLCJpZCI6WyJsYW5nY2hhaW4iLCJzY2hlbWEiLCJtZXNzYWdlcyIsIlN5c3RlbU1lc3NhZ2UiXSwia3dhcmdzIjp7ImNvbnRlbnQiOiJZb3UgYXJlIHRoZSByZXNlYXJjaGVyLiBTZWFyY2ggZm9yIGV2aWRlbmNlIGFuZCBzaGFyZSBzb3VyY2UgVVJMcy4gWW91IG1heSByZWFkIHNoYXJlZCB2aXJ0dWFsIGZpbGVzLCBidXQgb25seSB0aGUgZWRpdG9yIHdyaXRlcyB0aGVtLiBTZW5kIGluaXRpYWwgZmluZGluZ3MgdG8gdGhlIHNrZXB0aWMuIElmIGFub3RoZXIgYWdlbnQgcmV0dXJucyB3aXRoIGNvcnJlY3Rpb25zLCByZXZpc2UgeW91ciBmaW5kaW5ncyBhbmQgc2VuZCB0aGVtIHRvIHRoZSB2ZXJpZmllci4gRG8gbm90IGdpdmUgdGhlIGZpbmFsIGFuc3dlci4iLCJ0eXBlIjoic3lzdGVtIn19LHsibGMiOjEsInR5cGUiOiJjb25zdHJ1Y3RvciIsImlkIjpbImxhbmdjaGFpbiIsInNjaGVtYSIsIm1lc3NhZ2VzIiwiSHVtYW5NZXNzYWdlIl0sImt3YXJncyI6eyJjb250ZW50IjoiRm9yIGEgc3ludGhldGljIHRyYWNpbmcgZml4dHVyZSwgZXhwbGFpbiBQeXRob24gdHVwbGUgdmVyc3VzIGxpc3QgaW1tdXRhYmlsaXR5IGluIGF0IG1vc3QgMTIwIHdvcmRzLiBVc2Ugb25seSBvZmZpY2lhbCBQeXRob24gZG9jdW1lbnRhdGlvbiBpZiB5b3Ugc2VhcmNoLiBIYXZlIHRoZSBza2VwdGljIGFuZCB2ZXJpZmllciBjaGVjayBpdCwgdGhlbiBoYXZlIHRoZSBlZGl0b3Igd3JpdGUgL2Fuc3dlci5tZC4gS2VlcCB0aGUgcnVuIHNob3J0IGFuZCBkbyBub3QgYXNrIGZvbGxvdy11cCBxdWVzdGlvbnMuIiwidHlwZSI6Imh1bWFuIiwiaWQiOiI0NjIwNDMwNS1mOGVlLTQzODQtOWNlOC1mY2Q4MGJmY2RmNzUifX1dXX0=" + } + }, + { + "key": "gen_ai.usage.input_tokens", + "value": { + "intValue": "2845" + } + }, + { + "key": "gen_ai.usage.output_tokens", + "value": { + "intValue": "73" + } + }, + { + "key": "gen_ai.usage.total_tokens", + "value": { + "intValue": "2918" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJnZW5lcmF0aW9ucyI6W1t7InRleHQiOiIiLCJnZW5lcmF0aW9uX2luZm8iOm51bGwsInR5cGUiOiJDaGF0R2VuZXJhdGlvbiIsIm1lc3NhZ2UiOnsibGMiOjEsInR5cGUiOiJjb25zdHJ1Y3RvciIsImlkIjpbImxhbmdjaGFpbiIsInNjaGVtYSIsIm1lc3NhZ2VzIiwiQUlNZXNzYWdlIl0sImt3YXJncyI6eyJjb250ZW50IjpbeyJpZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7InF1ZXJ5IjoiZG9jcy5weXRob24ub3JnIHR1cGxlcyBpbW11dGFibGUgc2VxdWVuY2VzIGxpc3RzIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIHR1dG9yaWFsIn0sIm5hbWUiOiJ3ZWJfc2VhcmNoIiwidHlwZSI6InRvb2xfdXNlIiwidG9vbHNldF9uYW1lIjpudWxsfV0sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHc5YVc5YnJlblBxZ3liZk53UCIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjoyODQ1LCJvdXRwdXRfdG9rZW5zIjo3Mywib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6MH0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtMjFhMy03MDAwLWEzODEtMjg0YTZhMGMwM2Y3LTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoid2ViX3NlYXJjaCIsImFyZ3MiOnsicXVlcnkiOiJkb2NzLnB5dGhvbi5vcmcgdHVwbGVzIGltbXV0YWJsZSBzZXF1ZW5jZXMgbGlzdHMgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgdHV0b3JpYWwifSwiaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJ0eXBlIjoidG9vbF9jYWxsIn1dLCJ1c2FnZV9tZXRhZGF0YSI6eyJpbnB1dF90b2tlbnMiOjI4NDUsIm91dHB1dF90b2tlbnMiOjczLCJ0b3RhbF90b2tlbnMiOjI5MTgsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjB9fSwiaW52YWxpZF90b29sX2NhbGxzIjpbXX19fV1dLCJsbG1fb3V0cHV0Ijp7ImlkIjoibXNnXzAxMUNmZHc5YVc5YnJlblBxZ3liZk53UCIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjoyODQ1LCJvdXRwdXRfdG9rZW5zIjo3Mywib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6MH0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQifSwicnVuIjpudWxsLCJ0eXBlIjoiTExNUmVzdWx0In0=" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "cdef663fc44a4582", + "parentSpanId": "48b7b97682a8fe08", + "name": "FilesystemMiddleware.wrap_tool_call", + "kind": 1, + "startTimeUnixNano": "1790968018771259904", + "endTimeUnixNano": "1790968020179634944", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "e30=" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "FilesystemMiddleware.wrap_tool_call" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "2" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "tools" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"__pregel_push\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_push\",0,false]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828|tools:377cb0ac-b4a5-3ad6-f00b-9afdc4d589a0" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "researcher" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828" + } + }, + { + "key": "langsmith.metadata.ls_method", + "value": { + "stringValue": "traceable" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJjb250ZW50IjoiW3tcInRpdGxlXCI6IFwiNS4gRGF0YSBTdHJ1Y3R1cmVzIFxcdTIwMTQgUHl0aG9uIDMuMTQuNSBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy90dXRvcmlhbC9kYXRhc3RydWN0dXJlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJZb3UgbWlnaHQgaGF2ZSBub3RpY2VkIHRoYXQgbWV0aG9kcyBsaWtlYGluc2VydGAsYHJlbW92ZWAgb3Jgc29ydGAgdGhhdCBvbmx5IG1vZGlmeSB0aGUgbGlzdCBoYXZlIG5vIHJldHVybiB2YWx1ZSBwcmludGVkIFxcdTIwMTMgdGhleSByZXR1cm4gdGhlIGRlZmF1bHRgTm9uZWAuIFsxXSBUaGlzIGlzIGEgZGVzaWduIHByaW5jaXBsZSBmb3IgYWxsIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIGluIFB5dGhvbi5cXG4uLi5cXG4jIyA1LjMuIFR1cGxlcyBhbmQgU2VxdWVuY2VzXFx1MDBiNlxcblxcbldlIHNhdyB0aGF0IGxpc3RzIGFuZCBzdHJpbmdzIGhhdmUgbWFueSBjb21tb24gcHJvcGVydGllcywgc3VjaCBhcyBpbmRleGluZyBhbmQgc2xpY2luZyBvcGVyYXRpb25zLiBUaGV5IGFyZSB0d28gZXhhbXBsZXMgb2Ygc2VxdWVuY2UgZGF0YSB0eXBlcyAoc2VlIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgbGlzdCwgdHVwbGUsIHJhbmdlKS4gU2luY2UgUHl0aG9uIGlzIGFuIGV2b2x2aW5nIGxhbmd1YWdlLCBvdGhlciBzZXF1ZW5jZSBkYXRhIHR5cGVzIG1heSBiZSBhZGRlZC4gVGhlcmUgaXMgYWxzbyBhbm90aGVyIHN0YW5kYXJkIHNlcXVlbmNlIGRhdGEgdHlwZTogdGhlIHR1cGxlLlxcblxcbkEgdHVwbGUgY29uc2lzdHMgb2YgYSBudW1iZXIgb2YgdmFsdWVzIHNlcGFyYXRlZCBieSBjb21tYXMsIGZvciBpbnN0YW5jZTpcXG4uLi5cXG4+Pj4gIyBUdXBsZXMgYXJlIGltbXV0YWJsZTpcXG4+Pj4gdFswXSA9IDg4ODg4XFxuVHJhY2ViYWNrIChtb3N0IHJlY2VudCBjYWxsIGxhc3QpOlxcbiAgRmlsZSBcXFwiPHN0ZGluPlxcXCIsIGxpbmUgMSwgaW4gPG1vZHVsZT5cXG5UeXBlRXJyb3I6ICd0dXBsZScgb2JqZWN0IGRvZXMgbm90IHN1cHBvcnQgaXRlbSBhc3NpZ25tZW50XFxuPj4+ICMgYnV0IHRoZXkgY2FuIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzOlxcbj4+PiB2ID0gKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbj4+PiB2XFxuKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbi4uLlxcbkFzIHlvdSBzZWUsIG9uIG91dHB1dCB0dXBsZXMgYXJlIGFsd2F5cyBlbmNsb3NlZCBpbiBwYXJlbnRoZXNlcywgc28gdGhhdCBuZXN0ZWQgdHVwbGVzIGFyZSBpbnRlcnByZXRlZCBjb3JyZWN0bHk7IHRoZXkgbWF5IGJlIGlucHV0IHdpdGggb3Igd2l0aG91dCBzdXJyb3VuZGluZyBwYXJlbnRoZXNlcywgYWx0aG91Z2ggb2Z0ZW4gcGFyZW50aGVzZXMgYXJlIG5lY2Vzc2FyeSBhbnl3YXkgKGlmIHRoZSB0dXBsZSBpcyBwYXJ0IG9mIGEgbGFyZ2VyIGV4cHJlc3Npb24pLiBJdCBpcyBub3QgcG9zc2libGUgdG8gYXNzaWduIHRvIHRoZSBpbmRpdmlkdWFsIGl0ZW1zIG9mIGEgdHVwbGUsIGhvd2V2ZXIgaXQgaXMgcG9zc2libGUgdG8gY3JlYXRlIHR1cGxlcyB3aGljaCBjb250YWluIG11dGFibGUgb2JqZWN0cywgc3VjaCBhcyBsaXN0cy5cXG5cXG5UaG91Z2ggdHVwbGVzIG1heSBzZWVtIHNpbWlsYXIgdG8gbGlzdHMsIHRoZXkgYXJlIG9mdGVuIHVzZWQgaW4gZGlmZmVyZW50IHNpdHVhdGlvbnMgYW5kIGZvciBkaWZmZXJlbnQgcHVycG9zZXMuIFR1cGxlcyBhcmUgaW1tdXRhYmxlLCBhbmQgdXN1YWxseSBjb250YWluIGEgaGV0ZXJvZ2VuZW91cyBzZXF1ZW5jZSBvZiBlbGVtZW50cyB0aGF0IGFyZSBhY2Nlc3NlZCB2aWEgdW5wYWNraW5nIChzZWUgbGF0ZXIgaW4gdGhpcyBzZWN0aW9uKSBvciBpbmRleGluZyAob3IgZXZlbiBieSBhdHRyaWJ1dGUgaW4gdGhlIGNhc2Ugb2YgbmFtZWR0dXBsZXMpLiBMaXN0cyBhcmUgbXV0YWJsZSwgYW5kIHRoZWlyIGVsZW1lbnRzIGFyZSB1c3VhbGx5IGhvbW9nZW5lb3VzIGFuZCBhcmUgYWNjZXNzZWQgYnkgaXRlcmF0aW5nIG92ZXIgdGhlIGxpc3QuXFxuLi4uXFxuQW5vdGhlciB1c2VmdWwgZGF0YSB0eXBlIGJ1aWx0IGludG8gUHl0aG9uIGlzIHRoZSBkaWN0aW9uYXJ5IChzZWUgTWFwcGluZyBUeXBlcyBcXHUyMDE0IGRpY3QpLiBEaWN0aW9uYXJpZXMgYXJlIHNvbWV0aW1lcyBmb3VuZCBpbiBvdGhlciBsYW5ndWFnZXMgYXMgXFx1MjAxY2Fzc29jaWF0aXZlIG1lbW9yaWVzXFx1MjAxZCBvciBcXHUyMDFjYXNzb2NpYXRpdmUgYXJyYXlzXFx1MjAxZC4gVW5saWtlIHNlcXVlbmNlcywgd2hpY2ggYXJlIGluZGV4ZWQgYnkgYSByYW5nZSBvZiBudW1iZXJzLCBkaWN0aW9uYXJpZXMgYXJlIGluZGV4ZWQgYnkga2V5cywgd2hpY2ggY2FuIGJlIGFueSBpbW11dGFibGUgdHlwZTsgc3RyaW5ncyBhbmQgbnVtYmVycyBjYW4gYWx3YXlzIGJlIGtleXMuIFR1cGxlcyBjYW4gYmUgdXNlZCBhcyBrZXlzIGlmIHRoZXkgY29udGFpbiBvbmx5IHN0cmluZ3MsIG51bWJlcnMsIG9yIHR1cGxlczsgaWYgYSB0dXBsZSBjb250YWlucyBhbnkgbXV0YWJsZSBvYmplY3QgZWl0aGVyIGRpcmVjdGx5IG9yIGluZGlyZWN0bHksIGl0IGNhbm5vdCBiZSB1c2VkIGFzIGEga2V5LiBZb3UgY2FuXFx1MjAxOXQgdXNlIGxpc3RzIGFzIGtleXMsIHNpbmNlIGxpc3RzIGNhbiBiZSBtb2RpZmllZCBpbiBwbGFjZSB1c2luZyBpbmRleCBhc3NpZ25tZW50cywgc2xpY2UgYXNzaWdubWVudHMsIG9yIG1ldGhvZHMgbGlrZSBhcHBlbmQoKSBhbmQgZXh0ZW5kKCkuXCJdfSwge1widGl0bGVcIjogXCIzLiBEYXRhIG1vZGVsIFxcdTIwMTQgUHl0aG9uIDMuMTQuNSBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9yZWZlcmVuY2UvZGF0YW1vZGVsLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcIlRoZSB2YWx1ZSBvZiBzb21lIG9iamVjdHMgY2FuIGNoYW5nZS4gT2JqZWN0cyB3aG9zZSB2YWx1ZSBjYW4gY2hhbmdlIGFyZSBzYWlkIHRvIGJlIG11dGFibGU7IG9iamVjdHMgd2hvc2UgdmFsdWUgaXMgdW5jaGFuZ2VhYmxlIG9uY2UgdGhleSBhcmUgY3JlYXRlZCBhcmUgY2FsbGVkIGltbXV0YWJsZS4gKFRoZSB2YWx1ZSBvZiBhbiBpbW11dGFibGUgY29udGFpbmVyIG9iamVjdCB0aGF0IGNvbnRhaW5zIGEgcmVmZXJlbmNlIHRvIGEgbXV0YWJsZSBvYmplY3QgY2FuIGNoYW5nZSB3aGVuIHRoZSBsYXR0ZXJcXHUyMDE5cyB2YWx1ZSBpcyBjaGFuZ2VkOyBob3dldmVyIHRoZSBjb250YWluZXIgaXMgc3RpbGwgY29uc2lkZXJlZCBpbW11dGFibGUsIGJlY2F1c2UgdGhlIGNvbGxlY3Rpb24gb2Ygb2JqZWN0cyBpdCBjb250YWlucyBjYW5ub3QgYmUgY2hhbmdlZC4gU28sIGltbXV0YWJpbGl0eSBpcyBub3Qgc3RyaWN0bHkgdGhlIHNhbWUgYXMgaGF2aW5nIGFuIHVuY2hhbmdlYWJsZSB2YWx1ZSwgaXQgaXMgbW9yZSBzdWJ0bGUuKSBBbiBvYmplY3RcXHUyMDE5cyBtdXRhYmlsaXR5IGlzIGRldGVybWluZWQgYnkgaXRzIHR5cGU7IGZvciBpbnN0YW5jZSwgbnVtYmVycywgc3RyaW5ncyBhbmQgdHVwbGVzIGFyZSBpbW11dGFibGUsIHdoaWxlIGRpY3Rpb25hcmllcyBhbmQgbGlzdHMgYXJlIG11dGFibGUuXFxuLi4uXFxuU29tZSBvYmplY3RzIGNvbnRhaW4gcmVmZXJlbmNlcyB0byBvdGhlciBvYmplY3RzOyB0aGVzZSBhcmUgY2FsbGVkIGNvbnRhaW5lcnMuIEV4YW1wbGVzIG9mIGNvbnRhaW5lcnMgYXJlIHR1cGxlcywgbGlzdHMgYW5kIGRpY3Rpb25hcmllcy4gVGhlIHJlZmVyZW5jZXMgYXJlIHBhcnQgb2YgYSBjb250YWluZXJcXHUyMDE5cyB2YWx1ZS4gSW4gbW9zdCBjYXNlcywgd2hlbiB3ZSB0YWxrIGFib3V0IHRoZSB2YWx1ZSBvZiBhIGNvbnRhaW5lciwgd2UgaW1wbHkgdGhlIHZhbHVlcywgbm90IHRoZSBpZGVudGl0aWVzIG9mIHRoZSBjb250YWluZWQgb2JqZWN0czsgaG93ZXZlciwgd2hlbiB3ZSB0YWxrIGFib3V0IHRoZSBtdXRhYmlsaXR5IG9mIGEgY29udGFpbmVyLCBvbmx5IHRoZSBpZGVudGl0aWVzIG9mIHRoZSBpbW1lZGlhdGVseSBjb250YWluZWQgb2JqZWN0cyBhcmUgaW1wbGllZC4gU28sIGlmIGFuIGltbXV0YWJsZSBjb250YWluZXIgKGxpa2UgYSB0dXBsZSkgY29udGFpbnMgYSByZWZlcmVuY2UgdG8gYSBtdXRhYmxlIG9iamVjdCwgaXRzIHZhbHVlIGNoYW5nZXMgaWYgdGhhdCBtdXRhYmxlIG9iamVjdCBpcyBjaGFuZ2VkLlxcbi4uLlxcbiMjIyAzLjIuNS4gU2VxdWVuY2VzXFx1MDBiNlxcbi4uLlxcbiMjIyMgMy4yLjUuMS4gSW1tdXRhYmxlIHNlcXVlbmNlc1xcdTAwYjZcXG5cXG5BbiBvYmplY3Qgb2YgYW4gaW1tdXRhYmxlIHNlcXVlbmNlIHR5cGUgY2Fubm90IGNoYW5nZSBvbmNlIGl0IGlzIGNyZWF0ZWQuIChJZiB0aGUgb2JqZWN0IGNvbnRhaW5zIHJlZmVyZW5jZXMgdG8gb3RoZXIgb2JqZWN0cywgdGhlc2Ugb3RoZXIgb2JqZWN0cyBtYXkgYmUgbXV0YWJsZSBhbmQgbWF5IGJlIGNoYW5nZWQ7IGhvd2V2ZXIsIHRoZSBjb2xsZWN0aW9uIG9mIG9iamVjdHMgZGlyZWN0bHkgcmVmZXJlbmNlZCBieSBhbiBpbW11dGFibGUgb2JqZWN0IGNhbm5vdCBjaGFuZ2UuKVxcblxcblRoZSBmb2xsb3dpbmcgdHlwZXMgYXJlIGltbXV0YWJsZSBzZXF1ZW5jZXM6XFxuLi4uXFxuVHVwbGVzXFxuOiBUaGUgaXRlbXMgb2YgYSBgdHVwbGVgIGFyZSBhcmJpdHJhcnkgUHl0aG9uIG9iamVjdHMuIFR1cGxlcyBvZiB0d28gb3IgbW9yZSBpdGVtcyBhcmUgZm9ybWVkIGJ5IGNvbW1hLXNlcGFyYXRlZCBsaXN0cyBvZiBleHByZXNzaW9ucy4gQSB0dXBsZSBvZiBvbmUgaXRlbSAoYSBcXHUyMDE4c2luZ2xldG9uXFx1MjAxOSkgY2FuIGJlIGZvcm1lZCBieSBhZmZpeGluZyBhIGNvbW1hIHRvIGFuIGV4cHJlc3Npb24gKGFuIGV4cHJlc3Npb24gYnkgaXRzZWxmIGRvZXMgbm90IGNyZWF0ZSBhIHR1cGxlLCBzaW5jZSBwYXJlbnRoZXNlcyBtdXN0IGJlIHVzYWJsZSBmb3IgZ3JvdXBpbmcgb2YgZXhwcmVzc2lvbnMpLiBBbiBlbXB0eSB0dXBsZSBjYW4gYmUgZm9ybWVkIGJ5IGFuIGVtcHR5IHBhaXIgb2YgcGFyZW50aGVzZXMuXFxuLi4uXFxuIyMjIyAzLjIuNS4yLiBNdXRhYmxlIHNlcXVlbmNlc1xcdTAwYjZcXG5cXG5NdXRhYmxlIHNlcXVlbmNlcyBjYW4gYmUgY2hhbmdlZCBhZnRlciB0aGV5IGFyZSBjcmVhdGVkLiBUaGUgc3Vic2NyaXB0aW9uIGFuZCBzbGljaW5nIG5vdGF0aW9ucyBjYW4gYmUgdXNlZCBhcyB0aGUgdGFyZ2V0IG9mIGFzc2lnbm1lbnQgYW5kIGBkZWxgIChkZWxldGUpIHN0YXRlbWVudHMuXFxuLi4uXFxuVGhlcmUgYXJlIGN1cnJlbnRseSB0d28gaW50cmluc2ljIG11dGFibGUgc2VxdWVuY2UgdHlwZXM6XFxuXFxuTGlzdHNcXG46IFRoZSBpdGVtcyBvZiBhIGxpc3QgYXJlIGFyYml0cmFyeSBQeXRob24gb2JqZWN0cy4gTGlzdHMgYXJlIGZvcm1lZCBieSBwbGFjaW5nIGEgY29tbWEtc2VwYXJhdGVkIGxpc3Qgb2YgZXhwcmVzc2lvbnMgaW4gc3F1YXJlIGJyYWNrZXRzLiAoTm90ZSB0aGF0IHRoZXJlIGFyZSBubyBzcGVjaWFsIGNhc2VzIG5lZWRlZCB0byBmb3JtIGxpc3RzIG9mIGxlbmd0aCAwIG9yIDEuKVwiXX0sIHtcInRpdGxlXCI6IFwiQnVpbHQtaW4gVHlwZXMgXFx1MjAxNCBQeXRob24gMy4xNC43IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL2J1aWx0aW5zL3N0ZHR5cGVzLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcImNvbGxlY3Rpb24gY2xhc3NlcyBhcmUgbXV0YWJsZS4gVGhlIG1ldGhvZHMgdGhhdCBhZGQsIHN1YnRyYWN0LCBvciByZWFycmFuZ2UgdGhlaXIgLi4uIGluIHBsYWNlLCBhbmQgZG9uXFx1MjAxOXQgcmV0dXJuIGEgLi4uICwgbmV2ZXIgcmV0dXJuIHRoZSBjb2xsZWN0aW9uIGluc3RhbmNlIGl0c2VsZiBidXQgYE5vbmVgLlxcbi4uLlxcbiMjIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgYGxpc3RgLCBgdHVwbGVgLCBgcmFuZ2VgXFx1MDBiNlxcblxcblRoZXJlIGFyZSB0aHJlZSBiYXNpYyBzZXF1ZW5jZSB0eXBlczogbGlzdHMsIHR1cGxlcywgYW5kIHJhbmdlIG9iamVjdHMuIEFkZGl0aW9uYWwgc2VxdWVuY2UgdHlwZXMgdGFpbG9yZWQgZm9yIHByb2Nlc3Npbmcgb2YgYmluYXJ5IGRhdGEgYW5kIHRleHQgc3RyaW5ncyBhcmUgZGVzY3JpYmVkIGluIGRlZGljYXRlZCBzZWN0aW9ucy5cXG4uLi5cXG5UaGUgb3BlcmF0aW9ucyBpbiB0aGUgZm9sbG93aW5nIHRhYmxlIC4uLiBtdXRhYmxlIGFuZCBpbW11dGFibGUuIFRoZSBgY29sbGVjdGlvbnMuIC4uLiBpcyBwcm92aWRlZCB0byBtYWtlIC4uLiBlYXNpZXIgdG8gY29ycmVjdGx5IGltcGxlbWVudCB0aGVzZSBvcGVyYXRpb25zIG9uIGN1c3RvbSBzZXF1ZW5jZSB0eXBlc1xcbi4uLlxcbiMjIyBJbW11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxubXV0YWJsZSBzZXF1ZW5jZSB0eXBlcyBpc1xcbi4uLlxcbnN1cHBvcnQgYWxsb3dzIGltbXV0YWJsZSBzZXF1ZW5jZXMsIC4uLiAsIHRvIGJlIHVzZWQgYXMgYGRpY3RgIGtleXMgYW5kIHN0b3JlZCBpbiAuLi4gZW5zZXRgIGluc3RhbmNlc1xcbi4uLlxcbiMjIyBNdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbiMjIyBMaXN0c1xcdTAwYjZcXG5cXG5MaXN0cyBhcmUgbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhvbW9nZW5lb3VzIGl0ZW1zICh3aGVyZSB0aGUgcHJlY2lzZSBkZWdyZWUgb2Ygc2ltaWxhcml0eSB3aWxsIHZhcnkgYnkgYXBwbGljYXRpb24pLlxcbi4uLlxcbiMjIyBUdXBsZXNcXHUwMGI2XFxuXFxuVHVwbGVzIGFyZSBpbW11dGFibGUgc2VxdWVuY2VzLCB0eXBpY2FsbHkgdXNlZCB0byBzdG9yZSBjb2xsZWN0aW9ucyBvZiBoZXRlcm9nZW5lb3VzIGRhdGEgKHN1Y2ggYXMgdGhlIDItdHVwbGVzIHByb2R1Y2VkIGJ5IHRoZSBgZW51bWVyYXRlKClgIGJ1aWx0LWluKS4gVHVwbGVzIGFyZSBhbHNvIHVzZWQgZm9yIGNhc2VzIHdoZXJlIGFuIGltbXV0YWJsZSBzZXF1ZW5jZSBvZiBob21vZ2VuZW91cyBkYXRhIGlzIG5lZWRlZCAoc3VjaCBhcyBhbGxvd2luZyBzdG9yYWdlIGluIGEgYHNldGAgb3IgYGRpY3RgIGluc3RhbmNlKS5cXG4uLi5cXG50dXBsZShpdGVyYWJsZT1cXG4uLi5cXG5UaGUgY29uc3RydWN0b3IgYnVpbGRzIGEgdHVwbGUgd2hvc2UgaXRlbXMgYXJlIHRoZSBzYW1lIGFuZCBpbiB0aGUgc2FtZSBvcmRlciBhcyBpdGVyYWJsZVxcdTIwMTlzIGl0ZW1zLiBpdGVyYWJsZSBtYXkgYmUgZWl0aGVyIGEgc2VxdWVuY2UsIGEgY29udGFpbmVyIHRoYXQgc3VwcG9ydHMgaXRlcmF0aW9uLCBvciBhbiBpdGVyYXRvciBvYmplY3QuIElmIGl0ZXJhYmxlIGlzIGFscmVhZHkgYSB0dXBsZSwgaXQgaXMgcmV0dXJuZWQgdW5jaGFuZ2VkLiBGb3IgZXhhbXBsZSwgYHR1cGxlKCdhYmMnKWAgcmV0dXJucyBgKCdhJywgJ2InLCAnYycpYCBhbmQgYHR1cGxlKCBbMSwgMiwgM10gKWAgcmV0dXJucyBgKDEsIDIsIDMpYC4gSWYgbm8gYXJndW1lbnQgaXMgZ2l2ZW4sIHRoZSBjb25zdHJ1Y3RvciBjcmVhdGVzIGEgbmV3IGVtcHR5IHR1cGxlLCBgKClgLlxcbi4uLlxcblR1cGxlcyBpbXBsZW1lbnQgYWxsIG9mIHRoZSBjb21tb24gc2VxdWVuY2Ugb3BlcmF0aW9ucy5cXG4uLi5cXG5Gb3IgaGV0ZXJvZ2VuZW91cyBjb2xsZWN0aW9ucyBvZiBkYXRhIHdoZXJlIGFjY2VzcyBieSBuYW1lIGlzIGNsZWFyZXIgdGhhbiBhY2Nlc3MgYnkgaW5kZXgsIGBjb2xsZWN0aW9ucy5uYW1lZHR1cGxlKClgIG1heSBiZSBhIG1vcmUgYXBwcm9wcmlhdGUgY2hvaWNlIHRoYW4gYSBzaW1wbGUgdHVwbGUgb2JqZWN0LlwiXX0sIHtcInRpdGxlXCI6IFwiQnVpbHQtaW4gVHlwZXMgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL2xpYnJhcnkvc3RkdHlwZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiU29tZSBjb2xsZWN0aW9uIGNsYXNzZXMgYXJlIG11dGFibGUuIFRoZSBtZXRob2RzIHRoYXQgYWRkLCBzdWJ0cmFjdCwgb3IgcmVhcnJhbmdlIHRoZWlyIC4uLiBpbiBwbGFjZSwgYW5kIGRvblxcdTIwMTl0IHJldHVybiBhIC4uLiAsIG5ldmVyIHJldHVybiB0aGUgY29sbGVjdGlvbiBpbnN0YW5jZSBpdHNlbGYgYnV0IGBOb25lYC5cXG4uLi5cXG4jIyBTZXF1ZW5jZSBUeXBlcyBcXHUyMDE0IGBsaXN0YCwgYHR1cGxlYCwgYHJhbmdlYFxcdTAwYjZcXG5cXG5UaGVyZSBhcmUgdGhyZWUgYmFzaWMgc2VxdWVuY2UgdHlwZXM6IGxpc3RzLCB0dXBsZXMsIGFuZCAuLi4gb2JqZWN0cy4gQWRkaXRpb25hbCBzZXF1ZW5jZSB0eXBlcyB0YWlsb3JlZCBmb3IgcHJvY2Vzc2luZyBvZiBiaW5hcnkgZGF0YSBhbmQgdGV4dCBzdHJpbmdzIGFyZSBkZXNjcmliZWQgaW4gZGVkaWNhdGVkIHNlY3Rpb25zLlxcbi4uLlxcblRoZSBvcGVyYXRpb25zIGluIHRoZSBmb2xsb3dpbmcgdGFibGUgLi4uIGltbXV0YWJsZS4gVGhlIGAgLi4uIGlzIHByb3ZpZGVkIHRvIG1ha2UgLi4uIGVhc2llciB0byBjb3JyZWN0bHkgaW1wbGVtZW50IHRoZXNlIG9wZXJhdGlvbnMgb24gY3VzdG9tIHNlcXVlbmNlIHR5cGVzXFxuLi4uXFxuIyMjIEltbXV0YWJsZSBTZXF1ZW5jZSBUeXBlc1xcdTAwYjZcXG4uLi5cXG5vcGVyYXRpb24gdGhhdCBpbW11dGFibGUgc2VxdWVuY2UgLi4uIGJ5IG11dGFibGUgc2VxdWVuY2UgdHlwZXMgaXMgLi4uIGBoYXNoKClgXFxuLi4uXFxuVGhpcyBzdXBwb3J0IGFsbG93cyBpbW11dGFibGUgc2VxdWVuY2VzLCBzdWNoIGFzIGB0dXBsZWAgaW5zdGFuY2VzLCB0byBiZSB1c2VkIGFzIGBkaWN0YCBrZXlzIGFuZCBzdG9yZWQgaW4gYHNldGAgYW5kIGBmcm96ZW5zZXRgIGluc3RhbmNlcy5cXG4uLi5cXG4jIyMgTXV0YWJsZSBTZXF1ZW5jZSBUeXBlc1xcdTAwYjZcXG4uLi5cXG4jIyMgTGlzdHNcXHUwMGI2XFxuXFxuTGlzdHMgYXJlIG11dGFibGUgc2VxdWVuY2VzLCB0eXBpY2FsbHkgdXNlZCB0byBzdG9yZSBjb2xsZWN0aW9ucyBvZiBob21vZ2VuZW91cyBpdGVtcyAod2hlcmUgdGhlIHByZWNpc2UgZGVncmVlIG9mIHNpbWlsYXJpdHkgd2lsbCB2YXJ5IGJ5IGFwcGxpY2F0aW9uKS5cXG4uLi5cXG4jIyMgVHVwbGVzXFx1MDBiNlxcblxcblR1cGxlcyBhcmUgaW1tdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaGV0ZXJvZ2VuZW91cyBkYXRhIChzdWNoIGFzIHRoZSAyLXR1cGxlcyBwcm9kdWNlZCBieSB0aGUgYGVudW1lcmF0ZSgpYCBidWlsdC1pbikuIFR1cGxlcyBhcmUgYWxzbyB1c2VkIGZvciBjYXNlcyB3aGVyZSBhbiBpbW11dGFibGUgc2VxdWVuY2Ugb2YgaG9tb2dlbmVvdXMgZGF0YSBpcyBuZWVkZWQgKHN1Y2ggYXMgYWxsb3dpbmcgc3RvcmFnZSBpbiBhIGBzZXRgIG9yIGBkaWN0YCBpbnN0YW5jZSkuXFxuLi4uXFxuVGhlIGNvbnN0cnVjdG9yIGJ1aWxkcyBhIHR1cGxlIHdob3NlIGl0ZW1zIGFyZSB0aGUgc2FtZSBhbmQgaW4gdGhlIHNhbWUgb3JkZXIgYXMgaXRlcmFibGVcXHUyMDE5cyBpdGVtcy4gaXRlcmFibGUgbWF5IGJlIGVpdGhlciBhIHNlcXVlbmNlLCBhIGNvbnRhaW5lciB0aGF0IHN1cHBvcnRzIGl0ZXJhdGlvbiwgb3IgYW4gaXRlcmF0b3Igb2JqZWN0LiBJZiBpdGVyYWJsZSBpcyBhbHJlYWR5IGEgdHVwbGUsIGl0IGlzIHJldHVybmVkIHVuY2hhbmdlZC4gRm9yIGV4YW1wbGUsIGB0dXBsZSgnYWJjJylgIHJldHVybnMgYCgnYScsICdiJywgJ2MnKWAgYW5kIGB0dXBsZSggWzEsIDIsIDNdIClgIHJldHVybnMgYCgxLCAyLCAzKWAuIElmIG5vIGFyZ3VtZW50IGlzIGdpdmVuLCB0aGUgY29uc3RydWN0b3IgY3JlYXRlcyBhIG5ldyBlbXB0eSB0dXBsZSwgYCgpYC5cXG4uLi5cXG5UdXBsZXMgaW1wbGVtZW50IGFsbCBvZiB0aGUgY29tbW9uIHNlcXVlbmNlIG9wZXJhdGlvbnMuXFxuLi4uXFxuRm9yIGhldGVyb2dlbmVvdXMgY29sbGVjdGlvbnMgb2YgZGF0YSB3aGVyZSBhY2Nlc3MgYnkgbmFtZSBpcyBjbGVhcmVyIHRoYW4gYWNjZXNzIGJ5IGluZGV4LCBgY29sbGVjdGlvbnMubmFtZWR0dXBsZSgpYCBtYXkgYmUgYSBtb3JlIGFwcHJvcHJpYXRlIGNob2ljZSB0aGFuIGEgc2ltcGxlIHR1cGxlIG9iamVjdC5cIl19LCB7XCJ0aXRsZVwiOiBcIjUuIERhdGEgU3RydWN0dXJlcyBcXHUyMDE0IFB5dGhvbiAzLjEwLjIwIGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zLjEwL3R1dG9yaWFsL2RhdGFzdHJ1Y3R1cmVzLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcIllvdSBtaWdodCBoYXZlIG5vdGljZWQgdGhhdCBtZXRob2RzIGxpa2UgYGluc2VydGAsIGByZW1vdmVgIG9yIGBzb3J0YCB0aGF0IG9ubHkgbW9kaWZ5IHRoZSBsaXN0IGhhdmUgbm8gcmV0dXJuIHZhbHVlIHByaW50ZWQgXFx1MjAxMyB0aGV5IHJldHVybiB0aGUgZGVmYXVsdCBgTm9uZWAuIDEgVGhpcyBpcyBhIGRlc2lnbiBwcmluY2lwbGUgZm9yIGFsbCBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyBpbiBQeXRob24uXFxuLi4uXFxuIyMgNS4zLiBUdXBsZXMgYW5kIFNlcXVlbmNlc1xcdTAwYjZcXG5cXG5XZSBzYXcgdGhhdCBsaXN0cyBhbmQgc3RyaW5ncyBoYXZlIG1hbnkgY29tbW9uIHByb3BlcnRpZXMsIHN1Y2ggYXMgaW5kZXhpbmcgYW5kIHNsaWNpbmcgb3BlcmF0aW9ucy4gVGhleSBhcmUgdHdvIGV4YW1wbGVzIG9mIHNlcXVlbmNlIGRhdGEgdHlwZXMgKHNlZSBTZXF1ZW5jZSBUeXBlcyBcXHUyMDE0IGxpc3QsIHR1cGxlLCByYW5nZSkuIFNpbmNlIFB5dGhvbiBpcyBhbiBldm9sdmluZyBsYW5ndWFnZSwgb3RoZXIgc2VxdWVuY2UgZGF0YSB0eXBlcyBtYXkgYmUgYWRkZWQuIFRoZXJlIGlzIGFsc28gYW5vdGhlciBzdGFuZGFyZCBzZXF1ZW5jZSBkYXRhIHR5cGU6IHRoZSB0dXBsZS5cXG5cXG5BIHR1cGxlIGNvbnNpc3RzIG9mIGEgbnVtYmVyIG9mIHZhbHVlcyBzZXBhcmF0ZWQgYnkgY29tbWFzLCBmb3IgaW5zdGFuY2U6XFxuLi4uXFxuPj4+ICMgVHVwbGVzIGFyZSBpbW11dGFibGU6XFxuLi4uIHRbMF0gPSA4ODg4OFxcblRyYWNlYmFjayAobW9zdCByZWNlbnQgY2FsbCBsYXN0KTpcXG4gIEZpbGUgXFxcIjxzdGRpbj5cXFwiLCBsaW5lIDEsIGluIDxtb2R1bGU+XFxuVHlwZUVycm9yOiAndHVwbGUnIG9iamVjdCBkb2VzIG5vdCBzdXBwb3J0IGl0ZW0gYXNzaWdubWVudFxcbj4+PiAjIGJ1dCB0aGV5IGNhbiBjb250YWluIG11dGFibGUgb2JqZWN0czpcXG4uLi4gdiA9IChbMSwgMiwgM10sIFszLCAyLCAxXSlcXG4+Pj4gdlxcbihbMSwgMiwgM10sIFszLCAyLCAxXSlcXG4uLi5cXG5BcyB5b3Ugc2VlLCBvbiBvdXRwdXQgdHVwbGVzIGFyZSBhbHdheXMgZW5jbG9zZWQgaW4gcGFyZW50aGVzZXMsIHNvIHRoYXQgbmVzdGVkIHR1cGxlcyBhcmUgaW50ZXJwcmV0ZWQgY29ycmVjdGx5OyB0aGV5IG1heSBiZSBpbnB1dCB3aXRoIG9yIHdpdGhvdXQgc3Vycm91bmRpbmcgcGFyZW50aGVzZXMsIGFsdGhvdWdoIG9mdGVuIHBhcmVudGhlc2VzIGFyZSBuZWNlc3NhcnkgYW55d2F5IChpZiB0aGUgdHVwbGUgaXMgcGFydCBvZiBhIGxhcmdlciBleHByZXNzaW9uKS4gSXQgaXMgbm90IHBvc3NpYmxlIHRvIGFzc2lnbiB0byB0aGUgaW5kaXZpZHVhbCBpdGVtcyBvZiBhIHR1cGxlLCBob3dldmVyIGl0IGlzIHBvc3NpYmxlIHRvIGNyZWF0ZSB0dXBsZXMgd2hpY2ggY29udGFpbiBtdXRhYmxlIG9iamVjdHMsIHN1Y2ggYXMgbGlzdHMuXFxuXFxuVGhvdWdoIHR1cGxlcyBtYXkgc2VlbSBzaW1pbGFyIHRvIGxpc3RzLCB0aGV5IGFyZSBvZnRlbiB1c2VkIGluIGRpZmZlcmVudCBzaXR1YXRpb25zIGFuZCBmb3IgZGlmZmVyZW50IHB1cnBvc2VzLiBUdXBsZXMgYXJlIGltbXV0YWJsZSwgYW5kIHVzdWFsbHkgY29udGFpbiBhIGhldGVyb2dlbmVvdXMgc2VxdWVuY2Ugb2YgZWxlbWVudHMgdGhhdCBhcmUgYWNjZXNzZWQgdmlhIHVucGFja2luZyAoc2VlIGxhdGVyIGluIHRoaXMgc2VjdGlvbikgb3IgaW5kZXhpbmcgKG9yIGV2ZW4gYnkgYXR0cmlidXRlIGluIHRoZSBjYXNlIG9mIGBuYW1lZHR1cGxlc2ApLiBMaXN0cyBhcmUgbXV0YWJsZSwgYW5kIHRoZWlyIGVsZW1lbnRzIGFyZSB1c3VhbGx5IGhvbW9nZW5lb3VzIGFuZCBhcmUgYWNjZXNzZWQgYnkgaXRlcmF0aW5nIG92ZXIgdGhlIGxpc3QuXFxuLi4uXFxuIyMgNS40Llxcbi4uLlxcbi4gU2V0IG9iamVjdHNcXG4uLi5cXG4jIyA1LjUuIERpY3Rpb25hcmllc1xcdTAwYjZcXG5cXG5Bbm90aGVyIHVzZWZ1bCBkYXRhIHR5cGUgYnVpbHQgaW50byBQeXRob24gaXMgdGhlIGRpY3Rpb25hcnkgKHNlZSBNYXBwaW5nIFR5cGVzIFxcdTIwMTQgZGljdCkuIERpY3Rpb25hcmllcyBhcmUgc29tZXRpbWVzIGZvdW5kIGluIG90aGVyIGxhbmd1YWdlcyBhcyBcXHUyMDFjYXNzb2NpYXRpdmUgbWVtb3JpZXNcXHUyMDFkIG9yIFxcdTIwMWNhc3NvY2lhdGl2ZSBhcnJheXNcXHUyMDFkLiBVbmxpa2Ugc2VxdWVuY2VzLCB3aGljaCBhcmUgaW5kZXhlZCBieSBhIHJhbmdlIG9mIG51bWJlcnMsIGRpY3Rpb25hcmllcyBhcmUgaW5kZXhlZCBieSBrZXlzLCB3aGljaCBjYW4gYmUgYW55IGltbXV0YWJsZSB0eXBlOyBzdHJpbmdzIGFuZCBudW1iZXJzIGNhbiBhbHdheXMgYmUga2V5cy4gVHVwbGVzIGNhbiBiZSB1c2VkIGFzIGtleXMgaWYgdGhleSBjb250YWluIG9ubHkgc3RyaW5ncywgbnVtYmVycywgb3IgdHVwbGVzOyBpZiBhIHR1cGxlIGNvbnRhaW5zIGFueSBtdXRhYmxlIG9iamVjdCBlaXRoZXIgZGlyZWN0bHkgb3IgaW5kaXJlY3RseSwgaXQgY2Fubm90IGJlIHVzZWQgYXMgYSBrZXkuIFlvdSBjYW5cXHUyMDE5dCB1c2UgbGlzdHMgYXMga2V5cywgc2luY2UgbGlzdHMgY2FuIGJlIG1vZGlmaWVkIGluIHBsYWNlIHVzaW5nIGluZGV4IGFzc2lnbm1lbnRzLCBzbGljZSBhc3NpZ25tZW50cywgb3IgbWV0aG9kcyBsaWtlIGBhcHBlbmQoKWAgYW5kIGBleHRlbmQoKWAuXCJdfV0iLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7fSwidHlwZSI6InRvb2wiLCJuYW1lIjoid2ViX3NlYXJjaCIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsInN0YXR1cyI6InN1Y2Nlc3MifQ==" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "556c2690603624cc", + "parentSpanId": "cdef663fc44a4582", + "name": "web_search", + "kind": 1, + "startTimeUnixNano": "1790968018771609088", + "endTimeUnixNano": "1790968020179266816", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "execute_tool" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "tool" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "web_search" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "gen_ai.tool.name", + "value": { + "stringValue": "web_search" + } + }, + { + "key": "gen_ai.tool.call.id", + "value": { + "stringValue": "toolu_01B2LfHGDjYSFJhmiXNj4CuQ" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "2" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "tools" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"__pregel_push\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_push\",0,false]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828|tools:377cb0ac-b4a5-3ad6-f00b-9afdc4d589a0" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "researcher" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828" + } + }, + { + "key": "langsmith.metadata.ls_method", + "value": { + "stringValue": "traceable" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "seq:step:1" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJvdXRwdXQiOnsiY29udGVudCI6Ilt7XCJ0aXRsZVwiOiBcIjUuIERhdGEgU3RydWN0dXJlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiWW91IG1pZ2h0IGhhdmUgbm90aWNlZCB0aGF0IG1ldGhvZHMgbGlrZWBpbnNlcnRgLGByZW1vdmVgIG9yYHNvcnRgIHRoYXQgb25seSBtb2RpZnkgdGhlIGxpc3QgaGF2ZSBubyByZXR1cm4gdmFsdWUgcHJpbnRlZCBcXHUyMDEzIHRoZXkgcmV0dXJuIHRoZSBkZWZhdWx0YE5vbmVgLiBbMV0gVGhpcyBpcyBhIGRlc2lnbiBwcmluY2lwbGUgZm9yIGFsbCBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyBpbiBQeXRob24uXFxuLi4uXFxuIyMgNS4zLiBUdXBsZXMgYW5kIFNlcXVlbmNlc1xcdTAwYjZcXG5cXG5XZSBzYXcgdGhhdCBsaXN0cyBhbmQgc3RyaW5ncyBoYXZlIG1hbnkgY29tbW9uIHByb3BlcnRpZXMsIHN1Y2ggYXMgaW5kZXhpbmcgYW5kIHNsaWNpbmcgb3BlcmF0aW9ucy4gVGhleSBhcmUgdHdvIGV4YW1wbGVzIG9mIHNlcXVlbmNlIGRhdGEgdHlwZXMgKHNlZSBTZXF1ZW5jZSBUeXBlcyBcXHUyMDE0IGxpc3QsIHR1cGxlLCByYW5nZSkuIFNpbmNlIFB5dGhvbiBpcyBhbiBldm9sdmluZyBsYW5ndWFnZSwgb3RoZXIgc2VxdWVuY2UgZGF0YSB0eXBlcyBtYXkgYmUgYWRkZWQuIFRoZXJlIGlzIGFsc28gYW5vdGhlciBzdGFuZGFyZCBzZXF1ZW5jZSBkYXRhIHR5cGU6IHRoZSB0dXBsZS5cXG5cXG5BIHR1cGxlIGNvbnNpc3RzIG9mIGEgbnVtYmVyIG9mIHZhbHVlcyBzZXBhcmF0ZWQgYnkgY29tbWFzLCBmb3IgaW5zdGFuY2U6XFxuLi4uXFxuPj4+ICMgVHVwbGVzIGFyZSBpbW11dGFibGU6XFxuPj4+IHRbMF0gPSA4ODg4OFxcblRyYWNlYmFjayAobW9zdCByZWNlbnQgY2FsbCBsYXN0KTpcXG4gIEZpbGUgXFxcIjxzdGRpbj5cXFwiLCBsaW5lIDEsIGluIDxtb2R1bGU+XFxuVHlwZUVycm9yOiAndHVwbGUnIG9iamVjdCBkb2VzIG5vdCBzdXBwb3J0IGl0ZW0gYXNzaWdubWVudFxcbj4+PiAjIGJ1dCB0aGV5IGNhbiBjb250YWluIG11dGFibGUgb2JqZWN0czpcXG4+Pj4gdiA9IChbMSwgMiwgM10sIFszLCAyLCAxXSlcXG4+Pj4gdlxcbihbMSwgMiwgM10sIFszLCAyLCAxXSlcXG4uLi5cXG5BcyB5b3Ugc2VlLCBvbiBvdXRwdXQgdHVwbGVzIGFyZSBhbHdheXMgZW5jbG9zZWQgaW4gcGFyZW50aGVzZXMsIHNvIHRoYXQgbmVzdGVkIHR1cGxlcyBhcmUgaW50ZXJwcmV0ZWQgY29ycmVjdGx5OyB0aGV5IG1heSBiZSBpbnB1dCB3aXRoIG9yIHdpdGhvdXQgc3Vycm91bmRpbmcgcGFyZW50aGVzZXMsIGFsdGhvdWdoIG9mdGVuIHBhcmVudGhlc2VzIGFyZSBuZWNlc3NhcnkgYW55d2F5IChpZiB0aGUgdHVwbGUgaXMgcGFydCBvZiBhIGxhcmdlciBleHByZXNzaW9uKS4gSXQgaXMgbm90IHBvc3NpYmxlIHRvIGFzc2lnbiB0byB0aGUgaW5kaXZpZHVhbCBpdGVtcyBvZiBhIHR1cGxlLCBob3dldmVyIGl0IGlzIHBvc3NpYmxlIHRvIGNyZWF0ZSB0dXBsZXMgd2hpY2ggY29udGFpbiBtdXRhYmxlIG9iamVjdHMsIHN1Y2ggYXMgbGlzdHMuXFxuXFxuVGhvdWdoIHR1cGxlcyBtYXkgc2VlbSBzaW1pbGFyIHRvIGxpc3RzLCB0aGV5IGFyZSBvZnRlbiB1c2VkIGluIGRpZmZlcmVudCBzaXR1YXRpb25zIGFuZCBmb3IgZGlmZmVyZW50IHB1cnBvc2VzLiBUdXBsZXMgYXJlIGltbXV0YWJsZSwgYW5kIHVzdWFsbHkgY29udGFpbiBhIGhldGVyb2dlbmVvdXMgc2VxdWVuY2Ugb2YgZWxlbWVudHMgdGhhdCBhcmUgYWNjZXNzZWQgdmlhIHVucGFja2luZyAoc2VlIGxhdGVyIGluIHRoaXMgc2VjdGlvbikgb3IgaW5kZXhpbmcgKG9yIGV2ZW4gYnkgYXR0cmlidXRlIGluIHRoZSBjYXNlIG9mIG5hbWVkdHVwbGVzKS4gTGlzdHMgYXJlIG11dGFibGUsIGFuZCB0aGVpciBlbGVtZW50cyBhcmUgdXN1YWxseSBob21vZ2VuZW91cyBhbmQgYXJlIGFjY2Vzc2VkIGJ5IGl0ZXJhdGluZyBvdmVyIHRoZSBsaXN0Llxcbi4uLlxcbkFub3RoZXIgdXNlZnVsIGRhdGEgdHlwZSBidWlsdCBpbnRvIFB5dGhvbiBpcyB0aGUgZGljdGlvbmFyeSAoc2VlIE1hcHBpbmcgVHlwZXMgXFx1MjAxNCBkaWN0KS4gRGljdGlvbmFyaWVzIGFyZSBzb21ldGltZXMgZm91bmQgaW4gb3RoZXIgbGFuZ3VhZ2VzIGFzIFxcdTIwMWNhc3NvY2lhdGl2ZSBtZW1vcmllc1xcdTIwMWQgb3IgXFx1MjAxY2Fzc29jaWF0aXZlIGFycmF5c1xcdTIwMWQuIFVubGlrZSBzZXF1ZW5jZXMsIHdoaWNoIGFyZSBpbmRleGVkIGJ5IGEgcmFuZ2Ugb2YgbnVtYmVycywgZGljdGlvbmFyaWVzIGFyZSBpbmRleGVkIGJ5IGtleXMsIHdoaWNoIGNhbiBiZSBhbnkgaW1tdXRhYmxlIHR5cGU7IHN0cmluZ3MgYW5kIG51bWJlcnMgY2FuIGFsd2F5cyBiZSBrZXlzLiBUdXBsZXMgY2FuIGJlIHVzZWQgYXMga2V5cyBpZiB0aGV5IGNvbnRhaW4gb25seSBzdHJpbmdzLCBudW1iZXJzLCBvciB0dXBsZXM7IGlmIGEgdHVwbGUgY29udGFpbnMgYW55IG11dGFibGUgb2JqZWN0IGVpdGhlciBkaXJlY3RseSBvciBpbmRpcmVjdGx5LCBpdCBjYW5ub3QgYmUgdXNlZCBhcyBhIGtleS4gWW91IGNhblxcdTIwMTl0IHVzZSBsaXN0cyBhcyBrZXlzLCBzaW5jZSBsaXN0cyBjYW4gYmUgbW9kaWZpZWQgaW4gcGxhY2UgdXNpbmcgaW5kZXggYXNzaWdubWVudHMsIHNsaWNlIGFzc2lnbm1lbnRzLCBvciBtZXRob2RzIGxpa2UgYXBwZW5kKCkgYW5kIGV4dGVuZCgpLlwiXX0sIHtcInRpdGxlXCI6IFwiMy4gRGF0YSBtb2RlbCBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvcmVmZXJlbmNlL2RhdGFtb2RlbC5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJUaGUgdmFsdWUgb2Ygc29tZSBvYmplY3RzIGNhbiBjaGFuZ2UuIE9iamVjdHMgd2hvc2UgdmFsdWUgY2FuIGNoYW5nZSBhcmUgc2FpZCB0byBiZSBtdXRhYmxlOyBvYmplY3RzIHdob3NlIHZhbHVlIGlzIHVuY2hhbmdlYWJsZSBvbmNlIHRoZXkgYXJlIGNyZWF0ZWQgYXJlIGNhbGxlZCBpbW11dGFibGUuIChUaGUgdmFsdWUgb2YgYW4gaW1tdXRhYmxlIGNvbnRhaW5lciBvYmplY3QgdGhhdCBjb250YWlucyBhIHJlZmVyZW5jZSB0byBhIG11dGFibGUgb2JqZWN0IGNhbiBjaGFuZ2Ugd2hlbiB0aGUgbGF0dGVyXFx1MjAxOXMgdmFsdWUgaXMgY2hhbmdlZDsgaG93ZXZlciB0aGUgY29udGFpbmVyIGlzIHN0aWxsIGNvbnNpZGVyZWQgaW1tdXRhYmxlLCBiZWNhdXNlIHRoZSBjb2xsZWN0aW9uIG9mIG9iamVjdHMgaXQgY29udGFpbnMgY2Fubm90IGJlIGNoYW5nZWQuIFNvLCBpbW11dGFiaWxpdHkgaXMgbm90IHN0cmljdGx5IHRoZSBzYW1lIGFzIGhhdmluZyBhbiB1bmNoYW5nZWFibGUgdmFsdWUsIGl0IGlzIG1vcmUgc3VidGxlLikgQW4gb2JqZWN0XFx1MjAxOXMgbXV0YWJpbGl0eSBpcyBkZXRlcm1pbmVkIGJ5IGl0cyB0eXBlOyBmb3IgaW5zdGFuY2UsIG51bWJlcnMsIHN0cmluZ3MgYW5kIHR1cGxlcyBhcmUgaW1tdXRhYmxlLCB3aGlsZSBkaWN0aW9uYXJpZXMgYW5kIGxpc3RzIGFyZSBtdXRhYmxlLlxcbi4uLlxcblNvbWUgb2JqZWN0cyBjb250YWluIHJlZmVyZW5jZXMgdG8gb3RoZXIgb2JqZWN0czsgdGhlc2UgYXJlIGNhbGxlZCBjb250YWluZXJzLiBFeGFtcGxlcyBvZiBjb250YWluZXJzIGFyZSB0dXBsZXMsIGxpc3RzIGFuZCBkaWN0aW9uYXJpZXMuIFRoZSByZWZlcmVuY2VzIGFyZSBwYXJ0IG9mIGEgY29udGFpbmVyXFx1MjAxOXMgdmFsdWUuIEluIG1vc3QgY2FzZXMsIHdoZW4gd2UgdGFsayBhYm91dCB0aGUgdmFsdWUgb2YgYSBjb250YWluZXIsIHdlIGltcGx5IHRoZSB2YWx1ZXMsIG5vdCB0aGUgaWRlbnRpdGllcyBvZiB0aGUgY29udGFpbmVkIG9iamVjdHM7IGhvd2V2ZXIsIHdoZW4gd2UgdGFsayBhYm91dCB0aGUgbXV0YWJpbGl0eSBvZiBhIGNvbnRhaW5lciwgb25seSB0aGUgaWRlbnRpdGllcyBvZiB0aGUgaW1tZWRpYXRlbHkgY29udGFpbmVkIG9iamVjdHMgYXJlIGltcGxpZWQuIFNvLCBpZiBhbiBpbW11dGFibGUgY29udGFpbmVyIChsaWtlIGEgdHVwbGUpIGNvbnRhaW5zIGEgcmVmZXJlbmNlIHRvIGEgbXV0YWJsZSBvYmplY3QsIGl0cyB2YWx1ZSBjaGFuZ2VzIGlmIHRoYXQgbXV0YWJsZSBvYmplY3QgaXMgY2hhbmdlZC5cXG4uLi5cXG4jIyMgMy4yLjUuIFNlcXVlbmNlc1xcdTAwYjZcXG4uLi5cXG4jIyMjIDMuMi41LjEuIEltbXV0YWJsZSBzZXF1ZW5jZXNcXHUwMGI2XFxuXFxuQW4gb2JqZWN0IG9mIGFuIGltbXV0YWJsZSBzZXF1ZW5jZSB0eXBlIGNhbm5vdCBjaGFuZ2Ugb25jZSBpdCBpcyBjcmVhdGVkLiAoSWYgdGhlIG9iamVjdCBjb250YWlucyByZWZlcmVuY2VzIHRvIG90aGVyIG9iamVjdHMsIHRoZXNlIG90aGVyIG9iamVjdHMgbWF5IGJlIG11dGFibGUgYW5kIG1heSBiZSBjaGFuZ2VkOyBob3dldmVyLCB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGRpcmVjdGx5IHJlZmVyZW5jZWQgYnkgYW4gaW1tdXRhYmxlIG9iamVjdCBjYW5ub3QgY2hhbmdlLilcXG5cXG5UaGUgZm9sbG93aW5nIHR5cGVzIGFyZSBpbW11dGFibGUgc2VxdWVuY2VzOlxcbi4uLlxcblR1cGxlc1xcbjogVGhlIGl0ZW1zIG9mIGEgYHR1cGxlYCBhcmUgYXJiaXRyYXJ5IFB5dGhvbiBvYmplY3RzLiBUdXBsZXMgb2YgdHdvIG9yIG1vcmUgaXRlbXMgYXJlIGZvcm1lZCBieSBjb21tYS1zZXBhcmF0ZWQgbGlzdHMgb2YgZXhwcmVzc2lvbnMuIEEgdHVwbGUgb2Ygb25lIGl0ZW0gKGEgXFx1MjAxOHNpbmdsZXRvblxcdTIwMTkpIGNhbiBiZSBmb3JtZWQgYnkgYWZmaXhpbmcgYSBjb21tYSB0byBhbiBleHByZXNzaW9uIChhbiBleHByZXNzaW9uIGJ5IGl0c2VsZiBkb2VzIG5vdCBjcmVhdGUgYSB0dXBsZSwgc2luY2UgcGFyZW50aGVzZXMgbXVzdCBiZSB1c2FibGUgZm9yIGdyb3VwaW5nIG9mIGV4cHJlc3Npb25zKS4gQW4gZW1wdHkgdHVwbGUgY2FuIGJlIGZvcm1lZCBieSBhbiBlbXB0eSBwYWlyIG9mIHBhcmVudGhlc2VzLlxcbi4uLlxcbiMjIyMgMy4yLjUuMi4gTXV0YWJsZSBzZXF1ZW5jZXNcXHUwMGI2XFxuXFxuTXV0YWJsZSBzZXF1ZW5jZXMgY2FuIGJlIGNoYW5nZWQgYWZ0ZXIgdGhleSBhcmUgY3JlYXRlZC4gVGhlIHN1YnNjcmlwdGlvbiBhbmQgc2xpY2luZyBub3RhdGlvbnMgY2FuIGJlIHVzZWQgYXMgdGhlIHRhcmdldCBvZiBhc3NpZ25tZW50IGFuZCBgZGVsYCAoZGVsZXRlKSBzdGF0ZW1lbnRzLlxcbi4uLlxcblRoZXJlIGFyZSBjdXJyZW50bHkgdHdvIGludHJpbnNpYyBtdXRhYmxlIHNlcXVlbmNlIHR5cGVzOlxcblxcbkxpc3RzXFxuOiBUaGUgaXRlbXMgb2YgYSBsaXN0IGFyZSBhcmJpdHJhcnkgUHl0aG9uIG9iamVjdHMuIExpc3RzIGFyZSBmb3JtZWQgYnkgcGxhY2luZyBhIGNvbW1hLXNlcGFyYXRlZCBsaXN0IG9mIGV4cHJlc3Npb25zIGluIHNxdWFyZSBicmFja2V0cy4gKE5vdGUgdGhhdCB0aGVyZSBhcmUgbm8gc3BlY2lhbCBjYXNlcyBuZWVkZWQgdG8gZm9ybSBsaXN0cyBvZiBsZW5ndGggMCBvciAxLilcIl19LCB7XCJ0aXRsZVwiOiBcIkJ1aWx0LWluIFR5cGVzIFxcdTIwMTQgUHl0aG9uIDMuMTQuNyBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9idWlsdGlucy9zdGR0eXBlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJjb2xsZWN0aW9uIGNsYXNzZXMgYXJlIG11dGFibGUuIFRoZSBtZXRob2RzIHRoYXQgYWRkLCBzdWJ0cmFjdCwgb3IgcmVhcnJhbmdlIHRoZWlyIC4uLiBpbiBwbGFjZSwgYW5kIGRvblxcdTIwMTl0IHJldHVybiBhIC4uLiAsIG5ldmVyIHJldHVybiB0aGUgY29sbGVjdGlvbiBpbnN0YW5jZSBpdHNlbGYgYnV0IGBOb25lYC5cXG4uLi5cXG4jIyBTZXF1ZW5jZSBUeXBlcyBcXHUyMDE0IGBsaXN0YCwgYHR1cGxlYCwgYHJhbmdlYFxcdTAwYjZcXG5cXG5UaGVyZSBhcmUgdGhyZWUgYmFzaWMgc2VxdWVuY2UgdHlwZXM6IGxpc3RzLCB0dXBsZXMsIGFuZCByYW5nZSBvYmplY3RzLiBBZGRpdGlvbmFsIHNlcXVlbmNlIHR5cGVzIHRhaWxvcmVkIGZvciBwcm9jZXNzaW5nIG9mIGJpbmFyeSBkYXRhIGFuZCB0ZXh0IHN0cmluZ3MgYXJlIGRlc2NyaWJlZCBpbiBkZWRpY2F0ZWQgc2VjdGlvbnMuXFxuLi4uXFxuVGhlIG9wZXJhdGlvbnMgaW4gdGhlIGZvbGxvd2luZyB0YWJsZSAuLi4gbXV0YWJsZSBhbmQgaW1tdXRhYmxlLiBUaGUgYGNvbGxlY3Rpb25zLiAuLi4gaXMgcHJvdmlkZWQgdG8gbWFrZSAuLi4gZWFzaWVyIHRvIGNvcnJlY3RseSBpbXBsZW1lbnQgdGhlc2Ugb3BlcmF0aW9ucyBvbiBjdXN0b20gc2VxdWVuY2UgdHlwZXNcXG4uLi5cXG4jIyMgSW1tdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbm11dGFibGUgc2VxdWVuY2UgdHlwZXMgaXNcXG4uLi5cXG5zdXBwb3J0IGFsbG93cyBpbW11dGFibGUgc2VxdWVuY2VzLCAuLi4gLCB0byBiZSB1c2VkIGFzIGBkaWN0YCBrZXlzIGFuZCBzdG9yZWQgaW4gLi4uIGVuc2V0YCBpbnN0YW5jZXNcXG4uLi5cXG4jIyMgTXV0YWJsZSBTZXF1ZW5jZSBUeXBlc1xcdTAwYjZcXG4uLi5cXG4jIyMgTGlzdHNcXHUwMGI2XFxuXFxuTGlzdHMgYXJlIG11dGFibGUgc2VxdWVuY2VzLCB0eXBpY2FsbHkgdXNlZCB0byBzdG9yZSBjb2xsZWN0aW9ucyBvZiBob21vZ2VuZW91cyBpdGVtcyAod2hlcmUgdGhlIHByZWNpc2UgZGVncmVlIG9mIHNpbWlsYXJpdHkgd2lsbCB2YXJ5IGJ5IGFwcGxpY2F0aW9uKS5cXG4uLi5cXG4jIyMgVHVwbGVzXFx1MDBiNlxcblxcblR1cGxlcyBhcmUgaW1tdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaGV0ZXJvZ2VuZW91cyBkYXRhIChzdWNoIGFzIHRoZSAyLXR1cGxlcyBwcm9kdWNlZCBieSB0aGUgYGVudW1lcmF0ZSgpYCBidWlsdC1pbikuIFR1cGxlcyBhcmUgYWxzbyB1c2VkIGZvciBjYXNlcyB3aGVyZSBhbiBpbW11dGFibGUgc2VxdWVuY2Ugb2YgaG9tb2dlbmVvdXMgZGF0YSBpcyBuZWVkZWQgKHN1Y2ggYXMgYWxsb3dpbmcgc3RvcmFnZSBpbiBhIGBzZXRgIG9yIGBkaWN0YCBpbnN0YW5jZSkuXFxuLi4uXFxudHVwbGUoaXRlcmFibGU9XFxuLi4uXFxuVGhlIGNvbnN0cnVjdG9yIGJ1aWxkcyBhIHR1cGxlIHdob3NlIGl0ZW1zIGFyZSB0aGUgc2FtZSBhbmQgaW4gdGhlIHNhbWUgb3JkZXIgYXMgaXRlcmFibGVcXHUyMDE5cyBpdGVtcy4gaXRlcmFibGUgbWF5IGJlIGVpdGhlciBhIHNlcXVlbmNlLCBhIGNvbnRhaW5lciB0aGF0IHN1cHBvcnRzIGl0ZXJhdGlvbiwgb3IgYW4gaXRlcmF0b3Igb2JqZWN0LiBJZiBpdGVyYWJsZSBpcyBhbHJlYWR5IGEgdHVwbGUsIGl0IGlzIHJldHVybmVkIHVuY2hhbmdlZC4gRm9yIGV4YW1wbGUsIGB0dXBsZSgnYWJjJylgIHJldHVybnMgYCgnYScsICdiJywgJ2MnKWAgYW5kIGB0dXBsZSggWzEsIDIsIDNdIClgIHJldHVybnMgYCgxLCAyLCAzKWAuIElmIG5vIGFyZ3VtZW50IGlzIGdpdmVuLCB0aGUgY29uc3RydWN0b3IgY3JlYXRlcyBhIG5ldyBlbXB0eSB0dXBsZSwgYCgpYC5cXG4uLi5cXG5UdXBsZXMgaW1wbGVtZW50IGFsbCBvZiB0aGUgY29tbW9uIHNlcXVlbmNlIG9wZXJhdGlvbnMuXFxuLi4uXFxuRm9yIGhldGVyb2dlbmVvdXMgY29sbGVjdGlvbnMgb2YgZGF0YSB3aGVyZSBhY2Nlc3MgYnkgbmFtZSBpcyBjbGVhcmVyIHRoYW4gYWNjZXNzIGJ5IGluZGV4LCBgY29sbGVjdGlvbnMubmFtZWR0dXBsZSgpYCBtYXkgYmUgYSBtb3JlIGFwcHJvcHJpYXRlIGNob2ljZSB0aGFuIGEgc2ltcGxlIHR1cGxlIG9iamVjdC5cIl19LCB7XCJ0aXRsZVwiOiBcIkJ1aWx0LWluIFR5cGVzIFxcdTIwMTQgUHl0aG9uIDMuMTQuNSBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9saWJyYXJ5L3N0ZHR5cGVzLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcIlNvbWUgY29sbGVjdGlvbiBjbGFzc2VzIGFyZSBtdXRhYmxlLiBUaGUgbWV0aG9kcyB0aGF0IGFkZCwgc3VidHJhY3QsIG9yIHJlYXJyYW5nZSB0aGVpciAuLi4gaW4gcGxhY2UsIGFuZCBkb25cXHUyMDE5dCByZXR1cm4gYSAuLi4gLCBuZXZlciByZXR1cm4gdGhlIGNvbGxlY3Rpb24gaW5zdGFuY2UgaXRzZWxmIGJ1dCBgTm9uZWAuXFxuLi4uXFxuIyMgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBgbGlzdGAsIGB0dXBsZWAsIGByYW5nZWBcXHUwMGI2XFxuXFxuVGhlcmUgYXJlIHRocmVlIGJhc2ljIHNlcXVlbmNlIHR5cGVzOiBsaXN0cywgdHVwbGVzLCBhbmQgLi4uIG9iamVjdHMuIEFkZGl0aW9uYWwgc2VxdWVuY2UgdHlwZXMgdGFpbG9yZWQgZm9yIHByb2Nlc3Npbmcgb2YgYmluYXJ5IGRhdGEgYW5kIHRleHQgc3RyaW5ncyBhcmUgZGVzY3JpYmVkIGluIGRlZGljYXRlZCBzZWN0aW9ucy5cXG4uLi5cXG5UaGUgb3BlcmF0aW9ucyBpbiB0aGUgZm9sbG93aW5nIHRhYmxlIC4uLiBpbW11dGFibGUuIFRoZSBgIC4uLiBpcyBwcm92aWRlZCB0byBtYWtlIC4uLiBlYXNpZXIgdG8gY29ycmVjdGx5IGltcGxlbWVudCB0aGVzZSBvcGVyYXRpb25zIG9uIGN1c3RvbSBzZXF1ZW5jZSB0eXBlc1xcbi4uLlxcbiMjIyBJbW11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxub3BlcmF0aW9uIHRoYXQgaW1tdXRhYmxlIHNlcXVlbmNlIC4uLiBieSBtdXRhYmxlIHNlcXVlbmNlIHR5cGVzIGlzIC4uLiBgaGFzaCgpYFxcbi4uLlxcblRoaXMgc3VwcG9ydCBhbGxvd3MgaW1tdXRhYmxlIHNlcXVlbmNlcywgc3VjaCBhcyBgdHVwbGVgIGluc3RhbmNlcywgdG8gYmUgdXNlZCBhcyBgZGljdGAga2V5cyBhbmQgc3RvcmVkIGluIGBzZXRgIGFuZCBgZnJvemVuc2V0YCBpbnN0YW5jZXMuXFxuLi4uXFxuIyMjIE11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxuIyMjIExpc3RzXFx1MDBiNlxcblxcbkxpc3RzIGFyZSBtdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaG9tb2dlbmVvdXMgaXRlbXMgKHdoZXJlIHRoZSBwcmVjaXNlIGRlZ3JlZSBvZiBzaW1pbGFyaXR5IHdpbGwgdmFyeSBieSBhcHBsaWNhdGlvbikuXFxuLi4uXFxuIyMjIFR1cGxlc1xcdTAwYjZcXG5cXG5UdXBsZXMgYXJlIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhldGVyb2dlbmVvdXMgZGF0YSAoc3VjaCBhcyB0aGUgMi10dXBsZXMgcHJvZHVjZWQgYnkgdGhlIGBlbnVtZXJhdGUoKWAgYnVpbHQtaW4pLiBUdXBsZXMgYXJlIGFsc28gdXNlZCBmb3IgY2FzZXMgd2hlcmUgYW4gaW1tdXRhYmxlIHNlcXVlbmNlIG9mIGhvbW9nZW5lb3VzIGRhdGEgaXMgbmVlZGVkIChzdWNoIGFzIGFsbG93aW5nIHN0b3JhZ2UgaW4gYSBgc2V0YCBvciBgZGljdGAgaW5zdGFuY2UpLlxcbi4uLlxcblRoZSBjb25zdHJ1Y3RvciBidWlsZHMgYSB0dXBsZSB3aG9zZSBpdGVtcyBhcmUgdGhlIHNhbWUgYW5kIGluIHRoZSBzYW1lIG9yZGVyIGFzIGl0ZXJhYmxlXFx1MjAxOXMgaXRlbXMuIGl0ZXJhYmxlIG1heSBiZSBlaXRoZXIgYSBzZXF1ZW5jZSwgYSBjb250YWluZXIgdGhhdCBzdXBwb3J0cyBpdGVyYXRpb24sIG9yIGFuIGl0ZXJhdG9yIG9iamVjdC4gSWYgaXRlcmFibGUgaXMgYWxyZWFkeSBhIHR1cGxlLCBpdCBpcyByZXR1cm5lZCB1bmNoYW5nZWQuIEZvciBleGFtcGxlLCBgdHVwbGUoJ2FiYycpYCByZXR1cm5zIGAoJ2EnLCAnYicsICdjJylgIGFuZCBgdHVwbGUoIFsxLCAyLCAzXSApYCByZXR1cm5zIGAoMSwgMiwgMylgLiBJZiBubyBhcmd1bWVudCBpcyBnaXZlbiwgdGhlIGNvbnN0cnVjdG9yIGNyZWF0ZXMgYSBuZXcgZW1wdHkgdHVwbGUsIGAoKWAuXFxuLi4uXFxuVHVwbGVzIGltcGxlbWVudCBhbGwgb2YgdGhlIGNvbW1vbiBzZXF1ZW5jZSBvcGVyYXRpb25zLlxcbi4uLlxcbkZvciBoZXRlcm9nZW5lb3VzIGNvbGxlY3Rpb25zIG9mIGRhdGEgd2hlcmUgYWNjZXNzIGJ5IG5hbWUgaXMgY2xlYXJlciB0aGFuIGFjY2VzcyBieSBpbmRleCwgYGNvbGxlY3Rpb25zLm5hbWVkdHVwbGUoKWAgbWF5IGJlIGEgbW9yZSBhcHByb3ByaWF0ZSBjaG9pY2UgdGhhbiBhIHNpbXBsZSB0dXBsZSBvYmplY3QuXCJdfSwge1widGl0bGVcIjogXCI1LiBEYXRhIFN0cnVjdHVyZXMgXFx1MjAxNCBQeXRob24gMy4xMC4yMCBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy4xMC90dXRvcmlhbC9kYXRhc3RydWN0dXJlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJZb3UgbWlnaHQgaGF2ZSBub3RpY2VkIHRoYXQgbWV0aG9kcyBsaWtlIGBpbnNlcnRgLCBgcmVtb3ZlYCBvciBgc29ydGAgdGhhdCBvbmx5IG1vZGlmeSB0aGUgbGlzdCBoYXZlIG5vIHJldHVybiB2YWx1ZSBwcmludGVkIFxcdTIwMTMgdGhleSByZXR1cm4gdGhlIGRlZmF1bHQgYE5vbmVgLiAxIFRoaXMgaXMgYSBkZXNpZ24gcHJpbmNpcGxlIGZvciBhbGwgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgaW4gUHl0aG9uLlxcbi4uLlxcbiMjIDUuMy4gVHVwbGVzIGFuZCBTZXF1ZW5jZXNcXHUwMGI2XFxuXFxuV2Ugc2F3IHRoYXQgbGlzdHMgYW5kIHN0cmluZ3MgaGF2ZSBtYW55IGNvbW1vbiBwcm9wZXJ0aWVzLCBzdWNoIGFzIGluZGV4aW5nIGFuZCBzbGljaW5nIG9wZXJhdGlvbnMuIFRoZXkgYXJlIHR3byBleGFtcGxlcyBvZiBzZXF1ZW5jZSBkYXRhIHR5cGVzIChzZWUgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBsaXN0LCB0dXBsZSwgcmFuZ2UpLiBTaW5jZSBQeXRob24gaXMgYW4gZXZvbHZpbmcgbGFuZ3VhZ2UsIG90aGVyIHNlcXVlbmNlIGRhdGEgdHlwZXMgbWF5IGJlIGFkZGVkLiBUaGVyZSBpcyBhbHNvIGFub3RoZXIgc3RhbmRhcmQgc2VxdWVuY2UgZGF0YSB0eXBlOiB0aGUgdHVwbGUuXFxuXFxuQSB0dXBsZSBjb25zaXN0cyBvZiBhIG51bWJlciBvZiB2YWx1ZXMgc2VwYXJhdGVkIGJ5IGNvbW1hcywgZm9yIGluc3RhbmNlOlxcbi4uLlxcbj4+PiAjIFR1cGxlcyBhcmUgaW1tdXRhYmxlOlxcbi4uLiB0WzBdID0gODg4ODhcXG5UcmFjZWJhY2sgKG1vc3QgcmVjZW50IGNhbGwgbGFzdCk6XFxuICBGaWxlIFxcXCI8c3RkaW4+XFxcIiwgbGluZSAxLCBpbiA8bW9kdWxlPlxcblR5cGVFcnJvcjogJ3R1cGxlJyBvYmplY3QgZG9lcyBub3Qgc3VwcG9ydCBpdGVtIGFzc2lnbm1lbnRcXG4+Pj4gIyBidXQgdGhleSBjYW4gY29udGFpbiBtdXRhYmxlIG9iamVjdHM6XFxuLi4uIHYgPSAoWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuPj4+IHZcXG4oWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuLi4uXFxuQXMgeW91IHNlZSwgb24gb3V0cHV0IHR1cGxlcyBhcmUgYWx3YXlzIGVuY2xvc2VkIGluIHBhcmVudGhlc2VzLCBzbyB0aGF0IG5lc3RlZCB0dXBsZXMgYXJlIGludGVycHJldGVkIGNvcnJlY3RseTsgdGhleSBtYXkgYmUgaW5wdXQgd2l0aCBvciB3aXRob3V0IHN1cnJvdW5kaW5nIHBhcmVudGhlc2VzLCBhbHRob3VnaCBvZnRlbiBwYXJlbnRoZXNlcyBhcmUgbmVjZXNzYXJ5IGFueXdheSAoaWYgdGhlIHR1cGxlIGlzIHBhcnQgb2YgYSBsYXJnZXIgZXhwcmVzc2lvbikuIEl0IGlzIG5vdCBwb3NzaWJsZSB0byBhc3NpZ24gdG8gdGhlIGluZGl2aWR1YWwgaXRlbXMgb2YgYSB0dXBsZSwgaG93ZXZlciBpdCBpcyBwb3NzaWJsZSB0byBjcmVhdGUgdHVwbGVzIHdoaWNoIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzLCBzdWNoIGFzIGxpc3RzLlxcblxcblRob3VnaCB0dXBsZXMgbWF5IHNlZW0gc2ltaWxhciB0byBsaXN0cywgdGhleSBhcmUgb2Z0ZW4gdXNlZCBpbiBkaWZmZXJlbnQgc2l0dWF0aW9ucyBhbmQgZm9yIGRpZmZlcmVudCBwdXJwb3Nlcy4gVHVwbGVzIGFyZSBpbW11dGFibGUsIGFuZCB1c3VhbGx5IGNvbnRhaW4gYSBoZXRlcm9nZW5lb3VzIHNlcXVlbmNlIG9mIGVsZW1lbnRzIHRoYXQgYXJlIGFjY2Vzc2VkIHZpYSB1bnBhY2tpbmcgKHNlZSBsYXRlciBpbiB0aGlzIHNlY3Rpb24pIG9yIGluZGV4aW5nIChvciBldmVuIGJ5IGF0dHJpYnV0ZSBpbiB0aGUgY2FzZSBvZiBgbmFtZWR0dXBsZXNgKS4gTGlzdHMgYXJlIG11dGFibGUsIGFuZCB0aGVpciBlbGVtZW50cyBhcmUgdXN1YWxseSBob21vZ2VuZW91cyBhbmQgYXJlIGFjY2Vzc2VkIGJ5IGl0ZXJhdGluZyBvdmVyIHRoZSBsaXN0Llxcbi4uLlxcbiMjIDUuNC5cXG4uLi5cXG4uIFNldCBvYmplY3RzXFxuLi4uXFxuIyMgNS41LiBEaWN0aW9uYXJpZXNcXHUwMGI2XFxuXFxuQW5vdGhlciB1c2VmdWwgZGF0YSB0eXBlIGJ1aWx0IGludG8gUHl0aG9uIGlzIHRoZSBkaWN0aW9uYXJ5IChzZWUgTWFwcGluZyBUeXBlcyBcXHUyMDE0IGRpY3QpLiBEaWN0aW9uYXJpZXMgYXJlIHNvbWV0aW1lcyBmb3VuZCBpbiBvdGhlciBsYW5ndWFnZXMgYXMgXFx1MjAxY2Fzc29jaWF0aXZlIG1lbW9yaWVzXFx1MjAxZCBvciBcXHUyMDFjYXNzb2NpYXRpdmUgYXJyYXlzXFx1MjAxZC4gVW5saWtlIHNlcXVlbmNlcywgd2hpY2ggYXJlIGluZGV4ZWQgYnkgYSByYW5nZSBvZiBudW1iZXJzLCBkaWN0aW9uYXJpZXMgYXJlIGluZGV4ZWQgYnkga2V5cywgd2hpY2ggY2FuIGJlIGFueSBpbW11dGFibGUgdHlwZTsgc3RyaW5ncyBhbmQgbnVtYmVycyBjYW4gYWx3YXlzIGJlIGtleXMuIFR1cGxlcyBjYW4gYmUgdXNlZCBhcyBrZXlzIGlmIHRoZXkgY29udGFpbiBvbmx5IHN0cmluZ3MsIG51bWJlcnMsIG9yIHR1cGxlczsgaWYgYSB0dXBsZSBjb250YWlucyBhbnkgbXV0YWJsZSBvYmplY3QgZWl0aGVyIGRpcmVjdGx5IG9yIGluZGlyZWN0bHksIGl0IGNhbm5vdCBiZSB1c2VkIGFzIGEga2V5LiBZb3UgY2FuXFx1MjAxOXQgdXNlIGxpc3RzIGFzIGtleXMsIHNpbmNlIGxpc3RzIGNhbiBiZSBtb2RpZmllZCBpbiBwbGFjZSB1c2luZyBpbmRleCBhc3NpZ25tZW50cywgc2xpY2UgYXNzaWdubWVudHMsIG9yIG1ldGhvZHMgbGlrZSBgYXBwZW5kKClgIGFuZCBgZXh0ZW5kKClgLlwiXX1dIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJ0b29sIiwibmFtZSI6IndlYl9zZWFyY2giLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJzdGF0dXMiOiJzdWNjZXNzIn19" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "48b7b97682a8fe08", + "parentSpanId": "08f0feca38b72d47", + "name": "tools", + "kind": 1, + "startTimeUnixNano": "1790968018770586112", + "endTimeUnixNano": "1790968020180293120", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "tools" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "2" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "tools" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"__pregel_push\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_push\",0,false]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828|tools:377cb0ac-b4a5-3ad6-f00b-9afdc4d589a0" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "researcher" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "graph:step:2" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJpbnB1dCI6W3sibmFtZSI6IndlYl9zZWFyY2giLCJhcmdzIjp7InF1ZXJ5IjoiZG9jcy5weXRob24ub3JnIHR1cGxlcyBpbW11dGFibGUgc2VxdWVuY2VzIGxpc3RzIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIHR1dG9yaWFsIn0sImlkIjoidG9vbHVfMDFCMkxmSEdEallTRkpobWlYTmo0Q3VRIiwidHlwZSI6InRvb2xfY2FsbCJ9XX0=" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W3siY29udGVudCI6Ilt7XCJ0aXRsZVwiOiBcIjUuIERhdGEgU3RydWN0dXJlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiWW91IG1pZ2h0IGhhdmUgbm90aWNlZCB0aGF0IG1ldGhvZHMgbGlrZWBpbnNlcnRgLGByZW1vdmVgIG9yYHNvcnRgIHRoYXQgb25seSBtb2RpZnkgdGhlIGxpc3QgaGF2ZSBubyByZXR1cm4gdmFsdWUgcHJpbnRlZCBcXHUyMDEzIHRoZXkgcmV0dXJuIHRoZSBkZWZhdWx0YE5vbmVgLiBbMV0gVGhpcyBpcyBhIGRlc2lnbiBwcmluY2lwbGUgZm9yIGFsbCBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyBpbiBQeXRob24uXFxuLi4uXFxuIyMgNS4zLiBUdXBsZXMgYW5kIFNlcXVlbmNlc1xcdTAwYjZcXG5cXG5XZSBzYXcgdGhhdCBsaXN0cyBhbmQgc3RyaW5ncyBoYXZlIG1hbnkgY29tbW9uIHByb3BlcnRpZXMsIHN1Y2ggYXMgaW5kZXhpbmcgYW5kIHNsaWNpbmcgb3BlcmF0aW9ucy4gVGhleSBhcmUgdHdvIGV4YW1wbGVzIG9mIHNlcXVlbmNlIGRhdGEgdHlwZXMgKHNlZSBTZXF1ZW5jZSBUeXBlcyBcXHUyMDE0IGxpc3QsIHR1cGxlLCByYW5nZSkuIFNpbmNlIFB5dGhvbiBpcyBhbiBldm9sdmluZyBsYW5ndWFnZSwgb3RoZXIgc2VxdWVuY2UgZGF0YSB0eXBlcyBtYXkgYmUgYWRkZWQuIFRoZXJlIGlzIGFsc28gYW5vdGhlciBzdGFuZGFyZCBzZXF1ZW5jZSBkYXRhIHR5cGU6IHRoZSB0dXBsZS5cXG5cXG5BIHR1cGxlIGNvbnNpc3RzIG9mIGEgbnVtYmVyIG9mIHZhbHVlcyBzZXBhcmF0ZWQgYnkgY29tbWFzLCBmb3IgaW5zdGFuY2U6XFxuLi4uXFxuPj4+ICMgVHVwbGVzIGFyZSBpbW11dGFibGU6XFxuPj4+IHRbMF0gPSA4ODg4OFxcblRyYWNlYmFjayAobW9zdCByZWNlbnQgY2FsbCBsYXN0KTpcXG4gIEZpbGUgXFxcIjxzdGRpbj5cXFwiLCBsaW5lIDEsIGluIDxtb2R1bGU+XFxuVHlwZUVycm9yOiAndHVwbGUnIG9iamVjdCBkb2VzIG5vdCBzdXBwb3J0IGl0ZW0gYXNzaWdubWVudFxcbj4+PiAjIGJ1dCB0aGV5IGNhbiBjb250YWluIG11dGFibGUgb2JqZWN0czpcXG4+Pj4gdiA9IChbMSwgMiwgM10sIFszLCAyLCAxXSlcXG4+Pj4gdlxcbihbMSwgMiwgM10sIFszLCAyLCAxXSlcXG4uLi5cXG5BcyB5b3Ugc2VlLCBvbiBvdXRwdXQgdHVwbGVzIGFyZSBhbHdheXMgZW5jbG9zZWQgaW4gcGFyZW50aGVzZXMsIHNvIHRoYXQgbmVzdGVkIHR1cGxlcyBhcmUgaW50ZXJwcmV0ZWQgY29ycmVjdGx5OyB0aGV5IG1heSBiZSBpbnB1dCB3aXRoIG9yIHdpdGhvdXQgc3Vycm91bmRpbmcgcGFyZW50aGVzZXMsIGFsdGhvdWdoIG9mdGVuIHBhcmVudGhlc2VzIGFyZSBuZWNlc3NhcnkgYW55d2F5IChpZiB0aGUgdHVwbGUgaXMgcGFydCBvZiBhIGxhcmdlciBleHByZXNzaW9uKS4gSXQgaXMgbm90IHBvc3NpYmxlIHRvIGFzc2lnbiB0byB0aGUgaW5kaXZpZHVhbCBpdGVtcyBvZiBhIHR1cGxlLCBob3dldmVyIGl0IGlzIHBvc3NpYmxlIHRvIGNyZWF0ZSB0dXBsZXMgd2hpY2ggY29udGFpbiBtdXRhYmxlIG9iamVjdHMsIHN1Y2ggYXMgbGlzdHMuXFxuXFxuVGhvdWdoIHR1cGxlcyBtYXkgc2VlbSBzaW1pbGFyIHRvIGxpc3RzLCB0aGV5IGFyZSBvZnRlbiB1c2VkIGluIGRpZmZlcmVudCBzaXR1YXRpb25zIGFuZCBmb3IgZGlmZmVyZW50IHB1cnBvc2VzLiBUdXBsZXMgYXJlIGltbXV0YWJsZSwgYW5kIHVzdWFsbHkgY29udGFpbiBhIGhldGVyb2dlbmVvdXMgc2VxdWVuY2Ugb2YgZWxlbWVudHMgdGhhdCBhcmUgYWNjZXNzZWQgdmlhIHVucGFja2luZyAoc2VlIGxhdGVyIGluIHRoaXMgc2VjdGlvbikgb3IgaW5kZXhpbmcgKG9yIGV2ZW4gYnkgYXR0cmlidXRlIGluIHRoZSBjYXNlIG9mIG5hbWVkdHVwbGVzKS4gTGlzdHMgYXJlIG11dGFibGUsIGFuZCB0aGVpciBlbGVtZW50cyBhcmUgdXN1YWxseSBob21vZ2VuZW91cyBhbmQgYXJlIGFjY2Vzc2VkIGJ5IGl0ZXJhdGluZyBvdmVyIHRoZSBsaXN0Llxcbi4uLlxcbkFub3RoZXIgdXNlZnVsIGRhdGEgdHlwZSBidWlsdCBpbnRvIFB5dGhvbiBpcyB0aGUgZGljdGlvbmFyeSAoc2VlIE1hcHBpbmcgVHlwZXMgXFx1MjAxNCBkaWN0KS4gRGljdGlvbmFyaWVzIGFyZSBzb21ldGltZXMgZm91bmQgaW4gb3RoZXIgbGFuZ3VhZ2VzIGFzIFxcdTIwMWNhc3NvY2lhdGl2ZSBtZW1vcmllc1xcdTIwMWQgb3IgXFx1MjAxY2Fzc29jaWF0aXZlIGFycmF5c1xcdTIwMWQuIFVubGlrZSBzZXF1ZW5jZXMsIHdoaWNoIGFyZSBpbmRleGVkIGJ5IGEgcmFuZ2Ugb2YgbnVtYmVycywgZGljdGlvbmFyaWVzIGFyZSBpbmRleGVkIGJ5IGtleXMsIHdoaWNoIGNhbiBiZSBhbnkgaW1tdXRhYmxlIHR5cGU7IHN0cmluZ3MgYW5kIG51bWJlcnMgY2FuIGFsd2F5cyBiZSBrZXlzLiBUdXBsZXMgY2FuIGJlIHVzZWQgYXMga2V5cyBpZiB0aGV5IGNvbnRhaW4gb25seSBzdHJpbmdzLCBudW1iZXJzLCBvciB0dXBsZXM7IGlmIGEgdHVwbGUgY29udGFpbnMgYW55IG11dGFibGUgb2JqZWN0IGVpdGhlciBkaXJlY3RseSBvciBpbmRpcmVjdGx5LCBpdCBjYW5ub3QgYmUgdXNlZCBhcyBhIGtleS4gWW91IGNhblxcdTIwMTl0IHVzZSBsaXN0cyBhcyBrZXlzLCBzaW5jZSBsaXN0cyBjYW4gYmUgbW9kaWZpZWQgaW4gcGxhY2UgdXNpbmcgaW5kZXggYXNzaWdubWVudHMsIHNsaWNlIGFzc2lnbm1lbnRzLCBvciBtZXRob2RzIGxpa2UgYXBwZW5kKCkgYW5kIGV4dGVuZCgpLlwiXX0sIHtcInRpdGxlXCI6IFwiMy4gRGF0YSBtb2RlbCBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvcmVmZXJlbmNlL2RhdGFtb2RlbC5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJUaGUgdmFsdWUgb2Ygc29tZSBvYmplY3RzIGNhbiBjaGFuZ2UuIE9iamVjdHMgd2hvc2UgdmFsdWUgY2FuIGNoYW5nZSBhcmUgc2FpZCB0byBiZSBtdXRhYmxlOyBvYmplY3RzIHdob3NlIHZhbHVlIGlzIHVuY2hhbmdlYWJsZSBvbmNlIHRoZXkgYXJlIGNyZWF0ZWQgYXJlIGNhbGxlZCBpbW11dGFibGUuIChUaGUgdmFsdWUgb2YgYW4gaW1tdXRhYmxlIGNvbnRhaW5lciBvYmplY3QgdGhhdCBjb250YWlucyBhIHJlZmVyZW5jZSB0byBhIG11dGFibGUgb2JqZWN0IGNhbiBjaGFuZ2Ugd2hlbiB0aGUgbGF0dGVyXFx1MjAxOXMgdmFsdWUgaXMgY2hhbmdlZDsgaG93ZXZlciB0aGUgY29udGFpbmVyIGlzIHN0aWxsIGNvbnNpZGVyZWQgaW1tdXRhYmxlLCBiZWNhdXNlIHRoZSBjb2xsZWN0aW9uIG9mIG9iamVjdHMgaXQgY29udGFpbnMgY2Fubm90IGJlIGNoYW5nZWQuIFNvLCBpbW11dGFiaWxpdHkgaXMgbm90IHN0cmljdGx5IHRoZSBzYW1lIGFzIGhhdmluZyBhbiB1bmNoYW5nZWFibGUgdmFsdWUsIGl0IGlzIG1vcmUgc3VidGxlLikgQW4gb2JqZWN0XFx1MjAxOXMgbXV0YWJpbGl0eSBpcyBkZXRlcm1pbmVkIGJ5IGl0cyB0eXBlOyBmb3IgaW5zdGFuY2UsIG51bWJlcnMsIHN0cmluZ3MgYW5kIHR1cGxlcyBhcmUgaW1tdXRhYmxlLCB3aGlsZSBkaWN0aW9uYXJpZXMgYW5kIGxpc3RzIGFyZSBtdXRhYmxlLlxcbi4uLlxcblNvbWUgb2JqZWN0cyBjb250YWluIHJlZmVyZW5jZXMgdG8gb3RoZXIgb2JqZWN0czsgdGhlc2UgYXJlIGNhbGxlZCBjb250YWluZXJzLiBFeGFtcGxlcyBvZiBjb250YWluZXJzIGFyZSB0dXBsZXMsIGxpc3RzIGFuZCBkaWN0aW9uYXJpZXMuIFRoZSByZWZlcmVuY2VzIGFyZSBwYXJ0IG9mIGEgY29udGFpbmVyXFx1MjAxOXMgdmFsdWUuIEluIG1vc3QgY2FzZXMsIHdoZW4gd2UgdGFsayBhYm91dCB0aGUgdmFsdWUgb2YgYSBjb250YWluZXIsIHdlIGltcGx5IHRoZSB2YWx1ZXMsIG5vdCB0aGUgaWRlbnRpdGllcyBvZiB0aGUgY29udGFpbmVkIG9iamVjdHM7IGhvd2V2ZXIsIHdoZW4gd2UgdGFsayBhYm91dCB0aGUgbXV0YWJpbGl0eSBvZiBhIGNvbnRhaW5lciwgb25seSB0aGUgaWRlbnRpdGllcyBvZiB0aGUgaW1tZWRpYXRlbHkgY29udGFpbmVkIG9iamVjdHMgYXJlIGltcGxpZWQuIFNvLCBpZiBhbiBpbW11dGFibGUgY29udGFpbmVyIChsaWtlIGEgdHVwbGUpIGNvbnRhaW5zIGEgcmVmZXJlbmNlIHRvIGEgbXV0YWJsZSBvYmplY3QsIGl0cyB2YWx1ZSBjaGFuZ2VzIGlmIHRoYXQgbXV0YWJsZSBvYmplY3QgaXMgY2hhbmdlZC5cXG4uLi5cXG4jIyMgMy4yLjUuIFNlcXVlbmNlc1xcdTAwYjZcXG4uLi5cXG4jIyMjIDMuMi41LjEuIEltbXV0YWJsZSBzZXF1ZW5jZXNcXHUwMGI2XFxuXFxuQW4gb2JqZWN0IG9mIGFuIGltbXV0YWJsZSBzZXF1ZW5jZSB0eXBlIGNhbm5vdCBjaGFuZ2Ugb25jZSBpdCBpcyBjcmVhdGVkLiAoSWYgdGhlIG9iamVjdCBjb250YWlucyByZWZlcmVuY2VzIHRvIG90aGVyIG9iamVjdHMsIHRoZXNlIG90aGVyIG9iamVjdHMgbWF5IGJlIG11dGFibGUgYW5kIG1heSBiZSBjaGFuZ2VkOyBob3dldmVyLCB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGRpcmVjdGx5IHJlZmVyZW5jZWQgYnkgYW4gaW1tdXRhYmxlIG9iamVjdCBjYW5ub3QgY2hhbmdlLilcXG5cXG5UaGUgZm9sbG93aW5nIHR5cGVzIGFyZSBpbW11dGFibGUgc2VxdWVuY2VzOlxcbi4uLlxcblR1cGxlc1xcbjogVGhlIGl0ZW1zIG9mIGEgYHR1cGxlYCBhcmUgYXJiaXRyYXJ5IFB5dGhvbiBvYmplY3RzLiBUdXBsZXMgb2YgdHdvIG9yIG1vcmUgaXRlbXMgYXJlIGZvcm1lZCBieSBjb21tYS1zZXBhcmF0ZWQgbGlzdHMgb2YgZXhwcmVzc2lvbnMuIEEgdHVwbGUgb2Ygb25lIGl0ZW0gKGEgXFx1MjAxOHNpbmdsZXRvblxcdTIwMTkpIGNhbiBiZSBmb3JtZWQgYnkgYWZmaXhpbmcgYSBjb21tYSB0byBhbiBleHByZXNzaW9uIChhbiBleHByZXNzaW9uIGJ5IGl0c2VsZiBkb2VzIG5vdCBjcmVhdGUgYSB0dXBsZSwgc2luY2UgcGFyZW50aGVzZXMgbXVzdCBiZSB1c2FibGUgZm9yIGdyb3VwaW5nIG9mIGV4cHJlc3Npb25zKS4gQW4gZW1wdHkgdHVwbGUgY2FuIGJlIGZvcm1lZCBieSBhbiBlbXB0eSBwYWlyIG9mIHBhcmVudGhlc2VzLlxcbi4uLlxcbiMjIyMgMy4yLjUuMi4gTXV0YWJsZSBzZXF1ZW5jZXNcXHUwMGI2XFxuXFxuTXV0YWJsZSBzZXF1ZW5jZXMgY2FuIGJlIGNoYW5nZWQgYWZ0ZXIgdGhleSBhcmUgY3JlYXRlZC4gVGhlIHN1YnNjcmlwdGlvbiBhbmQgc2xpY2luZyBub3RhdGlvbnMgY2FuIGJlIHVzZWQgYXMgdGhlIHRhcmdldCBvZiBhc3NpZ25tZW50IGFuZCBgZGVsYCAoZGVsZXRlKSBzdGF0ZW1lbnRzLlxcbi4uLlxcblRoZXJlIGFyZSBjdXJyZW50bHkgdHdvIGludHJpbnNpYyBtdXRhYmxlIHNlcXVlbmNlIHR5cGVzOlxcblxcbkxpc3RzXFxuOiBUaGUgaXRlbXMgb2YgYSBsaXN0IGFyZSBhcmJpdHJhcnkgUHl0aG9uIG9iamVjdHMuIExpc3RzIGFyZSBmb3JtZWQgYnkgcGxhY2luZyBhIGNvbW1hLXNlcGFyYXRlZCBsaXN0IG9mIGV4cHJlc3Npb25zIGluIHNxdWFyZSBicmFja2V0cy4gKE5vdGUgdGhhdCB0aGVyZSBhcmUgbm8gc3BlY2lhbCBjYXNlcyBuZWVkZWQgdG8gZm9ybSBsaXN0cyBvZiBsZW5ndGggMCBvciAxLilcIl19LCB7XCJ0aXRsZVwiOiBcIkJ1aWx0LWluIFR5cGVzIFxcdTIwMTQgUHl0aG9uIDMuMTQuNyBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9idWlsdGlucy9zdGR0eXBlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJjb2xsZWN0aW9uIGNsYXNzZXMgYXJlIG11dGFibGUuIFRoZSBtZXRob2RzIHRoYXQgYWRkLCBzdWJ0cmFjdCwgb3IgcmVhcnJhbmdlIHRoZWlyIC4uLiBpbiBwbGFjZSwgYW5kIGRvblxcdTIwMTl0IHJldHVybiBhIC4uLiAsIG5ldmVyIHJldHVybiB0aGUgY29sbGVjdGlvbiBpbnN0YW5jZSBpdHNlbGYgYnV0IGBOb25lYC5cXG4uLi5cXG4jIyBTZXF1ZW5jZSBUeXBlcyBcXHUyMDE0IGBsaXN0YCwgYHR1cGxlYCwgYHJhbmdlYFxcdTAwYjZcXG5cXG5UaGVyZSBhcmUgdGhyZWUgYmFzaWMgc2VxdWVuY2UgdHlwZXM6IGxpc3RzLCB0dXBsZXMsIGFuZCByYW5nZSBvYmplY3RzLiBBZGRpdGlvbmFsIHNlcXVlbmNlIHR5cGVzIHRhaWxvcmVkIGZvciBwcm9jZXNzaW5nIG9mIGJpbmFyeSBkYXRhIGFuZCB0ZXh0IHN0cmluZ3MgYXJlIGRlc2NyaWJlZCBpbiBkZWRpY2F0ZWQgc2VjdGlvbnMuXFxuLi4uXFxuVGhlIG9wZXJhdGlvbnMgaW4gdGhlIGZvbGxvd2luZyB0YWJsZSAuLi4gbXV0YWJsZSBhbmQgaW1tdXRhYmxlLiBUaGUgYGNvbGxlY3Rpb25zLiAuLi4gaXMgcHJvdmlkZWQgdG8gbWFrZSAuLi4gZWFzaWVyIHRvIGNvcnJlY3RseSBpbXBsZW1lbnQgdGhlc2Ugb3BlcmF0aW9ucyBvbiBjdXN0b20gc2VxdWVuY2UgdHlwZXNcXG4uLi5cXG4jIyMgSW1tdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbm11dGFibGUgc2VxdWVuY2UgdHlwZXMgaXNcXG4uLi5cXG5zdXBwb3J0IGFsbG93cyBpbW11dGFibGUgc2VxdWVuY2VzLCAuLi4gLCB0byBiZSB1c2VkIGFzIGBkaWN0YCBrZXlzIGFuZCBzdG9yZWQgaW4gLi4uIGVuc2V0YCBpbnN0YW5jZXNcXG4uLi5cXG4jIyMgTXV0YWJsZSBTZXF1ZW5jZSBUeXBlc1xcdTAwYjZcXG4uLi5cXG4jIyMgTGlzdHNcXHUwMGI2XFxuXFxuTGlzdHMgYXJlIG11dGFibGUgc2VxdWVuY2VzLCB0eXBpY2FsbHkgdXNlZCB0byBzdG9yZSBjb2xsZWN0aW9ucyBvZiBob21vZ2VuZW91cyBpdGVtcyAod2hlcmUgdGhlIHByZWNpc2UgZGVncmVlIG9mIHNpbWlsYXJpdHkgd2lsbCB2YXJ5IGJ5IGFwcGxpY2F0aW9uKS5cXG4uLi5cXG4jIyMgVHVwbGVzXFx1MDBiNlxcblxcblR1cGxlcyBhcmUgaW1tdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaGV0ZXJvZ2VuZW91cyBkYXRhIChzdWNoIGFzIHRoZSAyLXR1cGxlcyBwcm9kdWNlZCBieSB0aGUgYGVudW1lcmF0ZSgpYCBidWlsdC1pbikuIFR1cGxlcyBhcmUgYWxzbyB1c2VkIGZvciBjYXNlcyB3aGVyZSBhbiBpbW11dGFibGUgc2VxdWVuY2Ugb2YgaG9tb2dlbmVvdXMgZGF0YSBpcyBuZWVkZWQgKHN1Y2ggYXMgYWxsb3dpbmcgc3RvcmFnZSBpbiBhIGBzZXRgIG9yIGBkaWN0YCBpbnN0YW5jZSkuXFxuLi4uXFxudHVwbGUoaXRlcmFibGU9XFxuLi4uXFxuVGhlIGNvbnN0cnVjdG9yIGJ1aWxkcyBhIHR1cGxlIHdob3NlIGl0ZW1zIGFyZSB0aGUgc2FtZSBhbmQgaW4gdGhlIHNhbWUgb3JkZXIgYXMgaXRlcmFibGVcXHUyMDE5cyBpdGVtcy4gaXRlcmFibGUgbWF5IGJlIGVpdGhlciBhIHNlcXVlbmNlLCBhIGNvbnRhaW5lciB0aGF0IHN1cHBvcnRzIGl0ZXJhdGlvbiwgb3IgYW4gaXRlcmF0b3Igb2JqZWN0LiBJZiBpdGVyYWJsZSBpcyBhbHJlYWR5IGEgdHVwbGUsIGl0IGlzIHJldHVybmVkIHVuY2hhbmdlZC4gRm9yIGV4YW1wbGUsIGB0dXBsZSgnYWJjJylgIHJldHVybnMgYCgnYScsICdiJywgJ2MnKWAgYW5kIGB0dXBsZSggWzEsIDIsIDNdIClgIHJldHVybnMgYCgxLCAyLCAzKWAuIElmIG5vIGFyZ3VtZW50IGlzIGdpdmVuLCB0aGUgY29uc3RydWN0b3IgY3JlYXRlcyBhIG5ldyBlbXB0eSB0dXBsZSwgYCgpYC5cXG4uLi5cXG5UdXBsZXMgaW1wbGVtZW50IGFsbCBvZiB0aGUgY29tbW9uIHNlcXVlbmNlIG9wZXJhdGlvbnMuXFxuLi4uXFxuRm9yIGhldGVyb2dlbmVvdXMgY29sbGVjdGlvbnMgb2YgZGF0YSB3aGVyZSBhY2Nlc3MgYnkgbmFtZSBpcyBjbGVhcmVyIHRoYW4gYWNjZXNzIGJ5IGluZGV4LCBgY29sbGVjdGlvbnMubmFtZWR0dXBsZSgpYCBtYXkgYmUgYSBtb3JlIGFwcHJvcHJpYXRlIGNob2ljZSB0aGFuIGEgc2ltcGxlIHR1cGxlIG9iamVjdC5cIl19LCB7XCJ0aXRsZVwiOiBcIkJ1aWx0LWluIFR5cGVzIFxcdTIwMTQgUHl0aG9uIDMuMTQuNSBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9saWJyYXJ5L3N0ZHR5cGVzLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcIlNvbWUgY29sbGVjdGlvbiBjbGFzc2VzIGFyZSBtdXRhYmxlLiBUaGUgbWV0aG9kcyB0aGF0IGFkZCwgc3VidHJhY3QsIG9yIHJlYXJyYW5nZSB0aGVpciAuLi4gaW4gcGxhY2UsIGFuZCBkb25cXHUyMDE5dCByZXR1cm4gYSAuLi4gLCBuZXZlciByZXR1cm4gdGhlIGNvbGxlY3Rpb24gaW5zdGFuY2UgaXRzZWxmIGJ1dCBgTm9uZWAuXFxuLi4uXFxuIyMgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBgbGlzdGAsIGB0dXBsZWAsIGByYW5nZWBcXHUwMGI2XFxuXFxuVGhlcmUgYXJlIHRocmVlIGJhc2ljIHNlcXVlbmNlIHR5cGVzOiBsaXN0cywgdHVwbGVzLCBhbmQgLi4uIG9iamVjdHMuIEFkZGl0aW9uYWwgc2VxdWVuY2UgdHlwZXMgdGFpbG9yZWQgZm9yIHByb2Nlc3Npbmcgb2YgYmluYXJ5IGRhdGEgYW5kIHRleHQgc3RyaW5ncyBhcmUgZGVzY3JpYmVkIGluIGRlZGljYXRlZCBzZWN0aW9ucy5cXG4uLi5cXG5UaGUgb3BlcmF0aW9ucyBpbiB0aGUgZm9sbG93aW5nIHRhYmxlIC4uLiBpbW11dGFibGUuIFRoZSBgIC4uLiBpcyBwcm92aWRlZCB0byBtYWtlIC4uLiBlYXNpZXIgdG8gY29ycmVjdGx5IGltcGxlbWVudCB0aGVzZSBvcGVyYXRpb25zIG9uIGN1c3RvbSBzZXF1ZW5jZSB0eXBlc1xcbi4uLlxcbiMjIyBJbW11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxub3BlcmF0aW9uIHRoYXQgaW1tdXRhYmxlIHNlcXVlbmNlIC4uLiBieSBtdXRhYmxlIHNlcXVlbmNlIHR5cGVzIGlzIC4uLiBgaGFzaCgpYFxcbi4uLlxcblRoaXMgc3VwcG9ydCBhbGxvd3MgaW1tdXRhYmxlIHNlcXVlbmNlcywgc3VjaCBhcyBgdHVwbGVgIGluc3RhbmNlcywgdG8gYmUgdXNlZCBhcyBgZGljdGAga2V5cyBhbmQgc3RvcmVkIGluIGBzZXRgIGFuZCBgZnJvemVuc2V0YCBpbnN0YW5jZXMuXFxuLi4uXFxuIyMjIE11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxuIyMjIExpc3RzXFx1MDBiNlxcblxcbkxpc3RzIGFyZSBtdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaG9tb2dlbmVvdXMgaXRlbXMgKHdoZXJlIHRoZSBwcmVjaXNlIGRlZ3JlZSBvZiBzaW1pbGFyaXR5IHdpbGwgdmFyeSBieSBhcHBsaWNhdGlvbikuXFxuLi4uXFxuIyMjIFR1cGxlc1xcdTAwYjZcXG5cXG5UdXBsZXMgYXJlIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhldGVyb2dlbmVvdXMgZGF0YSAoc3VjaCBhcyB0aGUgMi10dXBsZXMgcHJvZHVjZWQgYnkgdGhlIGBlbnVtZXJhdGUoKWAgYnVpbHQtaW4pLiBUdXBsZXMgYXJlIGFsc28gdXNlZCBmb3IgY2FzZXMgd2hlcmUgYW4gaW1tdXRhYmxlIHNlcXVlbmNlIG9mIGhvbW9nZW5lb3VzIGRhdGEgaXMgbmVlZGVkIChzdWNoIGFzIGFsbG93aW5nIHN0b3JhZ2UgaW4gYSBgc2V0YCBvciBgZGljdGAgaW5zdGFuY2UpLlxcbi4uLlxcblRoZSBjb25zdHJ1Y3RvciBidWlsZHMgYSB0dXBsZSB3aG9zZSBpdGVtcyBhcmUgdGhlIHNhbWUgYW5kIGluIHRoZSBzYW1lIG9yZGVyIGFzIGl0ZXJhYmxlXFx1MjAxOXMgaXRlbXMuIGl0ZXJhYmxlIG1heSBiZSBlaXRoZXIgYSBzZXF1ZW5jZSwgYSBjb250YWluZXIgdGhhdCBzdXBwb3J0cyBpdGVyYXRpb24sIG9yIGFuIGl0ZXJhdG9yIG9iamVjdC4gSWYgaXRlcmFibGUgaXMgYWxyZWFkeSBhIHR1cGxlLCBpdCBpcyByZXR1cm5lZCB1bmNoYW5nZWQuIEZvciBleGFtcGxlLCBgdHVwbGUoJ2FiYycpYCByZXR1cm5zIGAoJ2EnLCAnYicsICdjJylgIGFuZCBgdHVwbGUoIFsxLCAyLCAzXSApYCByZXR1cm5zIGAoMSwgMiwgMylgLiBJZiBubyBhcmd1bWVudCBpcyBnaXZlbiwgdGhlIGNvbnN0cnVjdG9yIGNyZWF0ZXMgYSBuZXcgZW1wdHkgdHVwbGUsIGAoKWAuXFxuLi4uXFxuVHVwbGVzIGltcGxlbWVudCBhbGwgb2YgdGhlIGNvbW1vbiBzZXF1ZW5jZSBvcGVyYXRpb25zLlxcbi4uLlxcbkZvciBoZXRlcm9nZW5lb3VzIGNvbGxlY3Rpb25zIG9mIGRhdGEgd2hlcmUgYWNjZXNzIGJ5IG5hbWUgaXMgY2xlYXJlciB0aGFuIGFjY2VzcyBieSBpbmRleCwgYGNvbGxlY3Rpb25zLm5hbWVkdHVwbGUoKWAgbWF5IGJlIGEgbW9yZSBhcHByb3ByaWF0ZSBjaG9pY2UgdGhhbiBhIHNpbXBsZSB0dXBsZSBvYmplY3QuXCJdfSwge1widGl0bGVcIjogXCI1LiBEYXRhIFN0cnVjdHVyZXMgXFx1MjAxNCBQeXRob24gMy4xMC4yMCBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy4xMC90dXRvcmlhbC9kYXRhc3RydWN0dXJlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJZb3UgbWlnaHQgaGF2ZSBub3RpY2VkIHRoYXQgbWV0aG9kcyBsaWtlIGBpbnNlcnRgLCBgcmVtb3ZlYCBvciBgc29ydGAgdGhhdCBvbmx5IG1vZGlmeSB0aGUgbGlzdCBoYXZlIG5vIHJldHVybiB2YWx1ZSBwcmludGVkIFxcdTIwMTMgdGhleSByZXR1cm4gdGhlIGRlZmF1bHQgYE5vbmVgLiAxIFRoaXMgaXMgYSBkZXNpZ24gcHJpbmNpcGxlIGZvciBhbGwgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgaW4gUHl0aG9uLlxcbi4uLlxcbiMjIDUuMy4gVHVwbGVzIGFuZCBTZXF1ZW5jZXNcXHUwMGI2XFxuXFxuV2Ugc2F3IHRoYXQgbGlzdHMgYW5kIHN0cmluZ3MgaGF2ZSBtYW55IGNvbW1vbiBwcm9wZXJ0aWVzLCBzdWNoIGFzIGluZGV4aW5nIGFuZCBzbGljaW5nIG9wZXJhdGlvbnMuIFRoZXkgYXJlIHR3byBleGFtcGxlcyBvZiBzZXF1ZW5jZSBkYXRhIHR5cGVzIChzZWUgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBsaXN0LCB0dXBsZSwgcmFuZ2UpLiBTaW5jZSBQeXRob24gaXMgYW4gZXZvbHZpbmcgbGFuZ3VhZ2UsIG90aGVyIHNlcXVlbmNlIGRhdGEgdHlwZXMgbWF5IGJlIGFkZGVkLiBUaGVyZSBpcyBhbHNvIGFub3RoZXIgc3RhbmRhcmQgc2VxdWVuY2UgZGF0YSB0eXBlOiB0aGUgdHVwbGUuXFxuXFxuQSB0dXBsZSBjb25zaXN0cyBvZiBhIG51bWJlciBvZiB2YWx1ZXMgc2VwYXJhdGVkIGJ5IGNvbW1hcywgZm9yIGluc3RhbmNlOlxcbi4uLlxcbj4+PiAjIFR1cGxlcyBhcmUgaW1tdXRhYmxlOlxcbi4uLiB0WzBdID0gODg4ODhcXG5UcmFjZWJhY2sgKG1vc3QgcmVjZW50IGNhbGwgbGFzdCk6XFxuICBGaWxlIFxcXCI8c3RkaW4+XFxcIiwgbGluZSAxLCBpbiA8bW9kdWxlPlxcblR5cGVFcnJvcjogJ3R1cGxlJyBvYmplY3QgZG9lcyBub3Qgc3VwcG9ydCBpdGVtIGFzc2lnbm1lbnRcXG4+Pj4gIyBidXQgdGhleSBjYW4gY29udGFpbiBtdXRhYmxlIG9iamVjdHM6XFxuLi4uIHYgPSAoWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuPj4+IHZcXG4oWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuLi4uXFxuQXMgeW91IHNlZSwgb24gb3V0cHV0IHR1cGxlcyBhcmUgYWx3YXlzIGVuY2xvc2VkIGluIHBhcmVudGhlc2VzLCBzbyB0aGF0IG5lc3RlZCB0dXBsZXMgYXJlIGludGVycHJldGVkIGNvcnJlY3RseTsgdGhleSBtYXkgYmUgaW5wdXQgd2l0aCBvciB3aXRob3V0IHN1cnJvdW5kaW5nIHBhcmVudGhlc2VzLCBhbHRob3VnaCBvZnRlbiBwYXJlbnRoZXNlcyBhcmUgbmVjZXNzYXJ5IGFueXdheSAoaWYgdGhlIHR1cGxlIGlzIHBhcnQgb2YgYSBsYXJnZXIgZXhwcmVzc2lvbikuIEl0IGlzIG5vdCBwb3NzaWJsZSB0byBhc3NpZ24gdG8gdGhlIGluZGl2aWR1YWwgaXRlbXMgb2YgYSB0dXBsZSwgaG93ZXZlciBpdCBpcyBwb3NzaWJsZSB0byBjcmVhdGUgdHVwbGVzIHdoaWNoIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzLCBzdWNoIGFzIGxpc3RzLlxcblxcblRob3VnaCB0dXBsZXMgbWF5IHNlZW0gc2ltaWxhciB0byBsaXN0cywgdGhleSBhcmUgb2Z0ZW4gdXNlZCBpbiBkaWZmZXJlbnQgc2l0dWF0aW9ucyBhbmQgZm9yIGRpZmZlcmVudCBwdXJwb3Nlcy4gVHVwbGVzIGFyZSBpbW11dGFibGUsIGFuZCB1c3VhbGx5IGNvbnRhaW4gYSBoZXRlcm9nZW5lb3VzIHNlcXVlbmNlIG9mIGVsZW1lbnRzIHRoYXQgYXJlIGFjY2Vzc2VkIHZpYSB1bnBhY2tpbmcgKHNlZSBsYXRlciBpbiB0aGlzIHNlY3Rpb24pIG9yIGluZGV4aW5nIChvciBldmVuIGJ5IGF0dHJpYnV0ZSBpbiB0aGUgY2FzZSBvZiBgbmFtZWR0dXBsZXNgKS4gTGlzdHMgYXJlIG11dGFibGUsIGFuZCB0aGVpciBlbGVtZW50cyBhcmUgdXN1YWxseSBob21vZ2VuZW91cyBhbmQgYXJlIGFjY2Vzc2VkIGJ5IGl0ZXJhdGluZyBvdmVyIHRoZSBsaXN0Llxcbi4uLlxcbiMjIDUuNC5cXG4uLi5cXG4uIFNldCBvYmplY3RzXFxuLi4uXFxuIyMgNS41LiBEaWN0aW9uYXJpZXNcXHUwMGI2XFxuXFxuQW5vdGhlciB1c2VmdWwgZGF0YSB0eXBlIGJ1aWx0IGludG8gUHl0aG9uIGlzIHRoZSBkaWN0aW9uYXJ5IChzZWUgTWFwcGluZyBUeXBlcyBcXHUyMDE0IGRpY3QpLiBEaWN0aW9uYXJpZXMgYXJlIHNvbWV0aW1lcyBmb3VuZCBpbiBvdGhlciBsYW5ndWFnZXMgYXMgXFx1MjAxY2Fzc29jaWF0aXZlIG1lbW9yaWVzXFx1MjAxZCBvciBcXHUyMDFjYXNzb2NpYXRpdmUgYXJyYXlzXFx1MjAxZC4gVW5saWtlIHNlcXVlbmNlcywgd2hpY2ggYXJlIGluZGV4ZWQgYnkgYSByYW5nZSBvZiBudW1iZXJzLCBkaWN0aW9uYXJpZXMgYXJlIGluZGV4ZWQgYnkga2V5cywgd2hpY2ggY2FuIGJlIGFueSBpbW11dGFibGUgdHlwZTsgc3RyaW5ncyBhbmQgbnVtYmVycyBjYW4gYWx3YXlzIGJlIGtleXMuIFR1cGxlcyBjYW4gYmUgdXNlZCBhcyBrZXlzIGlmIHRoZXkgY29udGFpbiBvbmx5IHN0cmluZ3MsIG51bWJlcnMsIG9yIHR1cGxlczsgaWYgYSB0dXBsZSBjb250YWlucyBhbnkgbXV0YWJsZSBvYmplY3QgZWl0aGVyIGRpcmVjdGx5IG9yIGluZGlyZWN0bHksIGl0IGNhbm5vdCBiZSB1c2VkIGFzIGEga2V5LiBZb3UgY2FuXFx1MjAxOXQgdXNlIGxpc3RzIGFzIGtleXMsIHNpbmNlIGxpc3RzIGNhbiBiZSBtb2RpZmllZCBpbiBwbGFjZSB1c2luZyBpbmRleCBhc3NpZ25tZW50cywgc2xpY2UgYXNzaWdubWVudHMsIG9yIG1ldGhvZHMgbGlrZSBgYXBwZW5kKClgIGFuZCBgZXh0ZW5kKClgLlwiXX1dIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJ0b29sIiwibmFtZSI6IndlYl9zZWFyY2giLCJpZCI6IjZlNTEyNWFjLTMxNjQtNGE3Ni05YzUyLWFjMzQ2MGNlMzZlNCIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsInN0YXR1cyI6InN1Y2Nlc3MifV19" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + } + ] + } + ] + }, + { + "resource": { + "attributes": [ + { + "key": "telemetry.sdk.language", + "value": { + "stringValue": "python" + } + }, + { + "key": "telemetry.sdk.name", + "value": { + "stringValue": "opentelemetry" + } + }, + { + "key": "telemetry.sdk.version", + "value": { + "stringValue": "1.45.0" + } + }, + { + "key": "service.instance.id", + "value": { + "stringValue": "b873df4c-5106-4097-a20b-b9380724f4d9" + } + }, + { + "key": "service.name", + "value": { + "stringValue": "deeplite" + } + } + ] + }, + "scopeSpans": [ + { + "scope": { + "name": "langsmith" + }, + "spans": [ + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "248062b114d67a66", + "parentSpanId": "08f0feca38b72d47", + "name": "tools", + "kind": 1, + "startTimeUnixNano": "1790968023765523968", + "endTimeUnixNano": "1790968023774028032", + "attributes": [ + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "tools" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "4" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "tools" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"__pregel_push\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_push\",0,false]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828|tools:ed0001c5-0402-bf5e-ceed-69e43e82ae5e" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "researcher" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "graph:step:4" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJpbnB1dCI6W3sibmFtZSI6InRyYW5zZmVyX3RvX3NrZXB0aWMiLCJhcmdzIjp7fSwiaWQiOiJ0b29sdV8wMVhqajNhUmRXeWhCSmZQbVRKQnhRYTkiLCJ0eXBlIjoidG9vbF9jYWxsIn1dfQ==" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "e30=" + } + } + ], + "events": [ + { + "timeUnixNano": "1790968023840499000", + "name": "exception", + "attributes": [ + { + "key": "exception.type", + "value": { + "stringValue": "Exception" + } + }, + { + "key": "exception.message", + "value": { + "stringValue": "ParentCommand(Command(graph='__parent__', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9')], 'active_agent': 'skeptic'}, goto='skeptic'))Traceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 709, in invoke\n input = step.invoke(input, config)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 447, in invoke\n ret = self.func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 84, in _write\n self.do_write(\n ~~~~~~~~~~~~~^\n config,\n ^^^^^^^\n writes,\n ^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 126, in do_write\n write(_assemble_writes(writes))\n ~~~~~~~~~~~~~~~~^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 181, in _assemble_writes\n if ww := w.mapper(w.value):\n ~~~~~~~~^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/graph/state.py\", line 1762, in _control_branch\n raise ParentCommand(command)\n\n\nlanggraph.errors.ParentCommand: Command(graph='__parent__', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9')], 'active_agent': 'skeptic'}, goto='skeptic')" + } + }, + { + "key": "exception.stacktrace", + "value": { + "stringValue": "Exception: ParentCommand(Command(graph='__parent__', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9')], 'active_agent': 'skeptic'}, goto='skeptic'))Traceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 709, in invoke\n input = step.invoke(input, config)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 447, in invoke\n ret = self.func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 84, in _write\n self.do_write(\n ~~~~~~~~~~~~~^\n config,\n ^^^^^^^\n writes,\n ^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 126, in do_write\n write(_assemble_writes(writes))\n ~~~~~~~~~~~~~~~~^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 181, in _assemble_writes\n if ww := w.mapper(w.value):\n ~~~~~~~~^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/graph/state.py\", line 1762, in _control_branch\n raise ParentCommand(command)\n\n\nlanggraph.errors.ParentCommand: Command(graph='__parent__', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9')], 'active_agent': 'skeptic'}, goto='skeptic')\n" + } + }, + { + "key": "exception.escaped", + "value": { + "stringValue": "False" + } + } + ] + } + ], + "status": { + "code": 2 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "1fd1c862b3a50355", + "parentSpanId": "248062b114d67a66", + "name": "FilesystemMiddleware.wrap_tool_call", + "kind": 1, + "startTimeUnixNano": "1790968023765999104", + "endTimeUnixNano": "1790968023766906112", + "attributes": [ + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "FilesystemMiddleware.wrap_tool_call" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "4" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "tools" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"__pregel_push\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_push\",0,false]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828|tools:ed0001c5-0402-bf5e-ceed-69e43e82ae5e" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "researcher" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828" + } + }, + { + "key": "langsmith.metadata.ls_method", + "value": { + "stringValue": "traceable" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "e30=" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJvdXRwdXQiOnsiZ3JhcGgiOiJfX3BhcmVudF9fIiwidXBkYXRlIjp7Im1lc3NhZ2VzIjpbeyJjb250ZW50IjoiRm9yIGEgc3ludGhldGljIHRyYWNpbmcgZml4dHVyZSwgZXhwbGFpbiBQeXRob24gdHVwbGUgdmVyc3VzIGxpc3QgaW1tdXRhYmlsaXR5IGluIGF0IG1vc3QgMTIwIHdvcmRzLiBVc2Ugb25seSBvZmZpY2lhbCBQeXRob24gZG9jdW1lbnRhdGlvbiBpZiB5b3Ugc2VhcmNoLiBIYXZlIHRoZSBza2VwdGljIGFuZCB2ZXJpZmllciBjaGVjayBpdCwgdGhlbiBoYXZlIHRoZSBlZGl0b3Igd3JpdGUgL2Fuc3dlci5tZC4gS2VlcCB0aGUgcnVuIHNob3J0IGFuZCBkbyBub3QgYXNrIGZvbGxvdy11cCBxdWVzdGlvbnMuIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJodW1hbiIsImlkIjoiNDYyMDQzMDUtZjhlZS00Mzg0LTljZTgtZmNkODBiZmNkZjc1In0seyJjb250ZW50IjpbeyJpZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7InF1ZXJ5IjoiZG9jcy5weXRob24ub3JnIHR1cGxlcyBpbW11dGFibGUgc2VxdWVuY2VzIGxpc3RzIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIHR1dG9yaWFsIn0sIm5hbWUiOiJ3ZWJfc2VhcmNoIiwidHlwZSI6InRvb2xfdXNlIiwidG9vbHNldF9uYW1lIjpudWxsfV0sImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2ZkdzlhVzlicmVuUHFneWJmTndQIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjI4NDUsIm91dHB1dF90b2tlbnMiOjczLCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjowfSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCIsIm1vZGVsX3Byb3ZpZGVyIjoiYW50aHJvcGljIn0sInR5cGUiOiJhaSIsIm5hbWUiOiJyZXNlYXJjaGVyIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTIxYTMtNzAwMC1hMzgxLTI4NGE2YTBjMDNmNy0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6IndlYl9zZWFyY2giLCJhcmdzIjp7InF1ZXJ5IjoiZG9jcy5weXRob24ub3JnIHR1cGxlcyBpbW11dGFibGUgc2VxdWVuY2VzIGxpc3RzIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIHR1dG9yaWFsIn0sImlkIjoidG9vbHVfMDFCMkxmSEdEallTRkpobWlYTmo0Q3VRIiwidHlwZSI6InRvb2xfY2FsbCJ9XSwiaW52YWxpZF90b29sX2NhbGxzIjpbXSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjoyODQ1LCJvdXRwdXRfdG9rZW5zIjo3MywidG90YWxfdG9rZW5zIjoyOTE4LCJpbnB1dF90b2tlbl9kZXRhaWxzIjp7ImNhY2hlX3JlYWQiOjAsImNhY2hlX2NyZWF0aW9uIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VuX2RldGFpbHMiOnsicmVhc29uaW5nIjowfX19LHsiY29udGVudCI6Ilt7XCJ0aXRsZVwiOiBcIjUuIERhdGEgU3RydWN0dXJlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiWW91IG1pZ2h0IGhhdmUgbm90aWNlZCB0aGF0IG1ldGhvZHMgbGlrZWBpbnNlcnRgLGByZW1vdmVgIG9yYHNvcnRgIHRoYXQgb25seSBtb2RpZnkgdGhlIGxpc3QgaGF2ZSBubyByZXR1cm4gdmFsdWUgcHJpbnRlZCBcXHUyMDEzIHRoZXkgcmV0dXJuIHRoZSBkZWZhdWx0YE5vbmVgLiBbMV0gVGhpcyBpcyBhIGRlc2lnbiBwcmluY2lwbGUgZm9yIGFsbCBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyBpbiBQeXRob24uXFxuLi4uXFxuIyMgNS4zLiBUdXBsZXMgYW5kIFNlcXVlbmNlc1xcdTAwYjZcXG5cXG5XZSBzYXcgdGhhdCBsaXN0cyBhbmQgc3RyaW5ncyBoYXZlIG1hbnkgY29tbW9uIHByb3BlcnRpZXMsIHN1Y2ggYXMgaW5kZXhpbmcgYW5kIHNsaWNpbmcgb3BlcmF0aW9ucy4gVGhleSBhcmUgdHdvIGV4YW1wbGVzIG9mIHNlcXVlbmNlIGRhdGEgdHlwZXMgKHNlZSBTZXF1ZW5jZSBUeXBlcyBcXHUyMDE0IGxpc3QsIHR1cGxlLCByYW5nZSkuIFNpbmNlIFB5dGhvbiBpcyBhbiBldm9sdmluZyBsYW5ndWFnZSwgb3RoZXIgc2VxdWVuY2UgZGF0YSB0eXBlcyBtYXkgYmUgYWRkZWQuIFRoZXJlIGlzIGFsc28gYW5vdGhlciBzdGFuZGFyZCBzZXF1ZW5jZSBkYXRhIHR5cGU6IHRoZSB0dXBsZS5cXG5cXG5BIHR1cGxlIGNvbnNpc3RzIG9mIGEgbnVtYmVyIG9mIHZhbHVlcyBzZXBhcmF0ZWQgYnkgY29tbWFzLCBmb3IgaW5zdGFuY2U6XFxuLi4uXFxuPj4+ICMgVHVwbGVzIGFyZSBpbW11dGFibGU6XFxuPj4+IHRbMF0gPSA4ODg4OFxcblRyYWNlYmFjayAobW9zdCByZWNlbnQgY2FsbCBsYXN0KTpcXG4gIEZpbGUgXFxcIjxzdGRpbj5cXFwiLCBsaW5lIDEsIGluIDxtb2R1bGU+XFxuVHlwZUVycm9yOiAndHVwbGUnIG9iamVjdCBkb2VzIG5vdCBzdXBwb3J0IGl0ZW0gYXNzaWdubWVudFxcbj4+PiAjIGJ1dCB0aGV5IGNhbiBjb250YWluIG11dGFibGUgb2JqZWN0czpcXG4+Pj4gdiA9IChbMSwgMiwgM10sIFszLCAyLCAxXSlcXG4+Pj4gdlxcbihbMSwgMiwgM10sIFszLCAyLCAxXSlcXG4uLi5cXG5BcyB5b3Ugc2VlLCBvbiBvdXRwdXQgdHVwbGVzIGFyZSBhbHdheXMgZW5jbG9zZWQgaW4gcGFyZW50aGVzZXMsIHNvIHRoYXQgbmVzdGVkIHR1cGxlcyBhcmUgaW50ZXJwcmV0ZWQgY29ycmVjdGx5OyB0aGV5IG1heSBiZSBpbnB1dCB3aXRoIG9yIHdpdGhvdXQgc3Vycm91bmRpbmcgcGFyZW50aGVzZXMsIGFsdGhvdWdoIG9mdGVuIHBhcmVudGhlc2VzIGFyZSBuZWNlc3NhcnkgYW55d2F5IChpZiB0aGUgdHVwbGUgaXMgcGFydCBvZiBhIGxhcmdlciBleHByZXNzaW9uKS4gSXQgaXMgbm90IHBvc3NpYmxlIHRvIGFzc2lnbiB0byB0aGUgaW5kaXZpZHVhbCBpdGVtcyBvZiBhIHR1cGxlLCBob3dldmVyIGl0IGlzIHBvc3NpYmxlIHRvIGNyZWF0ZSB0dXBsZXMgd2hpY2ggY29udGFpbiBtdXRhYmxlIG9iamVjdHMsIHN1Y2ggYXMgbGlzdHMuXFxuXFxuVGhvdWdoIHR1cGxlcyBtYXkgc2VlbSBzaW1pbGFyIHRvIGxpc3RzLCB0aGV5IGFyZSBvZnRlbiB1c2VkIGluIGRpZmZlcmVudCBzaXR1YXRpb25zIGFuZCBmb3IgZGlmZmVyZW50IHB1cnBvc2VzLiBUdXBsZXMgYXJlIGltbXV0YWJsZSwgYW5kIHVzdWFsbHkgY29udGFpbiBhIGhldGVyb2dlbmVvdXMgc2VxdWVuY2Ugb2YgZWxlbWVudHMgdGhhdCBhcmUgYWNjZXNzZWQgdmlhIHVucGFja2luZyAoc2VlIGxhdGVyIGluIHRoaXMgc2VjdGlvbikgb3IgaW5kZXhpbmcgKG9yIGV2ZW4gYnkgYXR0cmlidXRlIGluIHRoZSBjYXNlIG9mIG5hbWVkdHVwbGVzKS4gTGlzdHMgYXJlIG11dGFibGUsIGFuZCB0aGVpciBlbGVtZW50cyBhcmUgdXN1YWxseSBob21vZ2VuZW91cyBhbmQgYXJlIGFjY2Vzc2VkIGJ5IGl0ZXJhdGluZyBvdmVyIHRoZSBsaXN0Llxcbi4uLlxcbkFub3RoZXIgdXNlZnVsIGRhdGEgdHlwZSBidWlsdCBpbnRvIFB5dGhvbiBpcyB0aGUgZGljdGlvbmFyeSAoc2VlIE1hcHBpbmcgVHlwZXMgXFx1MjAxNCBkaWN0KS4gRGljdGlvbmFyaWVzIGFyZSBzb21ldGltZXMgZm91bmQgaW4gb3RoZXIgbGFuZ3VhZ2VzIGFzIFxcdTIwMWNhc3NvY2lhdGl2ZSBtZW1vcmllc1xcdTIwMWQgb3IgXFx1MjAxY2Fzc29jaWF0aXZlIGFycmF5c1xcdTIwMWQuIFVubGlrZSBzZXF1ZW5jZXMsIHdoaWNoIGFyZSBpbmRleGVkIGJ5IGEgcmFuZ2Ugb2YgbnVtYmVycywgZGljdGlvbmFyaWVzIGFyZSBpbmRleGVkIGJ5IGtleXMsIHdoaWNoIGNhbiBiZSBhbnkgaW1tdXRhYmxlIHR5cGU7IHN0cmluZ3MgYW5kIG51bWJlcnMgY2FuIGFsd2F5cyBiZSBrZXlzLiBUdXBsZXMgY2FuIGJlIHVzZWQgYXMga2V5cyBpZiB0aGV5IGNvbnRhaW4gb25seSBzdHJpbmdzLCBudW1iZXJzLCBvciB0dXBsZXM7IGlmIGEgdHVwbGUgY29udGFpbnMgYW55IG11dGFibGUgb2JqZWN0IGVpdGhlciBkaXJlY3RseSBvciBpbmRpcmVjdGx5LCBpdCBjYW5ub3QgYmUgdXNlZCBhcyBhIGtleS4gWW91IGNhblxcdTIwMTl0IHVzZSBsaXN0cyBhcyBrZXlzLCBzaW5jZSBsaXN0cyBjYW4gYmUgbW9kaWZpZWQgaW4gcGxhY2UgdXNpbmcgaW5kZXggYXNzaWdubWVudHMsIHNsaWNlIGFzc2lnbm1lbnRzLCBvciBtZXRob2RzIGxpa2UgYXBwZW5kKCkgYW5kIGV4dGVuZCgpLlwiXX0sIHtcInRpdGxlXCI6IFwiMy4gRGF0YSBtb2RlbCBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvcmVmZXJlbmNlL2RhdGFtb2RlbC5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJUaGUgdmFsdWUgb2Ygc29tZSBvYmplY3RzIGNhbiBjaGFuZ2UuIE9iamVjdHMgd2hvc2UgdmFsdWUgY2FuIGNoYW5nZSBhcmUgc2FpZCB0byBiZSBtdXRhYmxlOyBvYmplY3RzIHdob3NlIHZhbHVlIGlzIHVuY2hhbmdlYWJsZSBvbmNlIHRoZXkgYXJlIGNyZWF0ZWQgYXJlIGNhbGxlZCBpbW11dGFibGUuIChUaGUgdmFsdWUgb2YgYW4gaW1tdXRhYmxlIGNvbnRhaW5lciBvYmplY3QgdGhhdCBjb250YWlucyBhIHJlZmVyZW5jZSB0byBhIG11dGFibGUgb2JqZWN0IGNhbiBjaGFuZ2Ugd2hlbiB0aGUgbGF0dGVyXFx1MjAxOXMgdmFsdWUgaXMgY2hhbmdlZDsgaG93ZXZlciB0aGUgY29udGFpbmVyIGlzIHN0aWxsIGNvbnNpZGVyZWQgaW1tdXRhYmxlLCBiZWNhdXNlIHRoZSBjb2xsZWN0aW9uIG9mIG9iamVjdHMgaXQgY29udGFpbnMgY2Fubm90IGJlIGNoYW5nZWQuIFNvLCBpbW11dGFiaWxpdHkgaXMgbm90IHN0cmljdGx5IHRoZSBzYW1lIGFzIGhhdmluZyBhbiB1bmNoYW5nZWFibGUgdmFsdWUsIGl0IGlzIG1vcmUgc3VidGxlLikgQW4gb2JqZWN0XFx1MjAxOXMgbXV0YWJpbGl0eSBpcyBkZXRlcm1pbmVkIGJ5IGl0cyB0eXBlOyBmb3IgaW5zdGFuY2UsIG51bWJlcnMsIHN0cmluZ3MgYW5kIHR1cGxlcyBhcmUgaW1tdXRhYmxlLCB3aGlsZSBkaWN0aW9uYXJpZXMgYW5kIGxpc3RzIGFyZSBtdXRhYmxlLlxcbi4uLlxcblNvbWUgb2JqZWN0cyBjb250YWluIHJlZmVyZW5jZXMgdG8gb3RoZXIgb2JqZWN0czsgdGhlc2UgYXJlIGNhbGxlZCBjb250YWluZXJzLiBFeGFtcGxlcyBvZiBjb250YWluZXJzIGFyZSB0dXBsZXMsIGxpc3RzIGFuZCBkaWN0aW9uYXJpZXMuIFRoZSByZWZlcmVuY2VzIGFyZSBwYXJ0IG9mIGEgY29udGFpbmVyXFx1MjAxOXMgdmFsdWUuIEluIG1vc3QgY2FzZXMsIHdoZW4gd2UgdGFsayBhYm91dCB0aGUgdmFsdWUgb2YgYSBjb250YWluZXIsIHdlIGltcGx5IHRoZSB2YWx1ZXMsIG5vdCB0aGUgaWRlbnRpdGllcyBvZiB0aGUgY29udGFpbmVkIG9iamVjdHM7IGhvd2V2ZXIsIHdoZW4gd2UgdGFsayBhYm91dCB0aGUgbXV0YWJpbGl0eSBvZiBhIGNvbnRhaW5lciwgb25seSB0aGUgaWRlbnRpdGllcyBvZiB0aGUgaW1tZWRpYXRlbHkgY29udGFpbmVkIG9iamVjdHMgYXJlIGltcGxpZWQuIFNvLCBpZiBhbiBpbW11dGFibGUgY29udGFpbmVyIChsaWtlIGEgdHVwbGUpIGNvbnRhaW5zIGEgcmVmZXJlbmNlIHRvIGEgbXV0YWJsZSBvYmplY3QsIGl0cyB2YWx1ZSBjaGFuZ2VzIGlmIHRoYXQgbXV0YWJsZSBvYmplY3QgaXMgY2hhbmdlZC5cXG4uLi5cXG4jIyMgMy4yLjUuIFNlcXVlbmNlc1xcdTAwYjZcXG4uLi5cXG4jIyMjIDMuMi41LjEuIEltbXV0YWJsZSBzZXF1ZW5jZXNcXHUwMGI2XFxuXFxuQW4gb2JqZWN0IG9mIGFuIGltbXV0YWJsZSBzZXF1ZW5jZSB0eXBlIGNhbm5vdCBjaGFuZ2Ugb25jZSBpdCBpcyBjcmVhdGVkLiAoSWYgdGhlIG9iamVjdCBjb250YWlucyByZWZlcmVuY2VzIHRvIG90aGVyIG9iamVjdHMsIHRoZXNlIG90aGVyIG9iamVjdHMgbWF5IGJlIG11dGFibGUgYW5kIG1heSBiZSBjaGFuZ2VkOyBob3dldmVyLCB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGRpcmVjdGx5IHJlZmVyZW5jZWQgYnkgYW4gaW1tdXRhYmxlIG9iamVjdCBjYW5ub3QgY2hhbmdlLilcXG5cXG5UaGUgZm9sbG93aW5nIHR5cGVzIGFyZSBpbW11dGFibGUgc2VxdWVuY2VzOlxcbi4uLlxcblR1cGxlc1xcbjogVGhlIGl0ZW1zIG9mIGEgYHR1cGxlYCBhcmUgYXJiaXRyYXJ5IFB5dGhvbiBvYmplY3RzLiBUdXBsZXMgb2YgdHdvIG9yIG1vcmUgaXRlbXMgYXJlIGZvcm1lZCBieSBjb21tYS1zZXBhcmF0ZWQgbGlzdHMgb2YgZXhwcmVzc2lvbnMuIEEgdHVwbGUgb2Ygb25lIGl0ZW0gKGEgXFx1MjAxOHNpbmdsZXRvblxcdTIwMTkpIGNhbiBiZSBmb3JtZWQgYnkgYWZmaXhpbmcgYSBjb21tYSB0byBhbiBleHByZXNzaW9uIChhbiBleHByZXNzaW9uIGJ5IGl0c2VsZiBkb2VzIG5vdCBjcmVhdGUgYSB0dXBsZSwgc2luY2UgcGFyZW50aGVzZXMgbXVzdCBiZSB1c2FibGUgZm9yIGdyb3VwaW5nIG9mIGV4cHJlc3Npb25zKS4gQW4gZW1wdHkgdHVwbGUgY2FuIGJlIGZvcm1lZCBieSBhbiBlbXB0eSBwYWlyIG9mIHBhcmVudGhlc2VzLlxcbi4uLlxcbiMjIyMgMy4yLjUuMi4gTXV0YWJsZSBzZXF1ZW5jZXNcXHUwMGI2XFxuXFxuTXV0YWJsZSBzZXF1ZW5jZXMgY2FuIGJlIGNoYW5nZWQgYWZ0ZXIgdGhleSBhcmUgY3JlYXRlZC4gVGhlIHN1YnNjcmlwdGlvbiBhbmQgc2xpY2luZyBub3RhdGlvbnMgY2FuIGJlIHVzZWQgYXMgdGhlIHRhcmdldCBvZiBhc3NpZ25tZW50IGFuZCBgZGVsYCAoZGVsZXRlKSBzdGF0ZW1lbnRzLlxcbi4uLlxcblRoZXJlIGFyZSBjdXJyZW50bHkgdHdvIGludHJpbnNpYyBtdXRhYmxlIHNlcXVlbmNlIHR5cGVzOlxcblxcbkxpc3RzXFxuOiBUaGUgaXRlbXMgb2YgYSBsaXN0IGFyZSBhcmJpdHJhcnkgUHl0aG9uIG9iamVjdHMuIExpc3RzIGFyZSBmb3JtZWQgYnkgcGxhY2luZyBhIGNvbW1hLXNlcGFyYXRlZCBsaXN0IG9mIGV4cHJlc3Npb25zIGluIHNxdWFyZSBicmFja2V0cy4gKE5vdGUgdGhhdCB0aGVyZSBhcmUgbm8gc3BlY2lhbCBjYXNlcyBuZWVkZWQgdG8gZm9ybSBsaXN0cyBvZiBsZW5ndGggMCBvciAxLilcIl19LCB7XCJ0aXRsZVwiOiBcIkJ1aWx0LWluIFR5cGVzIFxcdTIwMTQgUHl0aG9uIDMuMTQuNyBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9idWlsdGlucy9zdGR0eXBlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJjb2xsZWN0aW9uIGNsYXNzZXMgYXJlIG11dGFibGUuIFRoZSBtZXRob2RzIHRoYXQgYWRkLCBzdWJ0cmFjdCwgb3IgcmVhcnJhbmdlIHRoZWlyIC4uLiBpbiBwbGFjZSwgYW5kIGRvblxcdTIwMTl0IHJldHVybiBhIC4uLiAsIG5ldmVyIHJldHVybiB0aGUgY29sbGVjdGlvbiBpbnN0YW5jZSBpdHNlbGYgYnV0IGBOb25lYC5cXG4uLi5cXG4jIyBTZXF1ZW5jZSBUeXBlcyBcXHUyMDE0IGBsaXN0YCwgYHR1cGxlYCwgYHJhbmdlYFxcdTAwYjZcXG5cXG5UaGVyZSBhcmUgdGhyZWUgYmFzaWMgc2VxdWVuY2UgdHlwZXM6IGxpc3RzLCB0dXBsZXMsIGFuZCByYW5nZSBvYmplY3RzLiBBZGRpdGlvbmFsIHNlcXVlbmNlIHR5cGVzIHRhaWxvcmVkIGZvciBwcm9jZXNzaW5nIG9mIGJpbmFyeSBkYXRhIGFuZCB0ZXh0IHN0cmluZ3MgYXJlIGRlc2NyaWJlZCBpbiBkZWRpY2F0ZWQgc2VjdGlvbnMuXFxuLi4uXFxuVGhlIG9wZXJhdGlvbnMgaW4gdGhlIGZvbGxvd2luZyB0YWJsZSAuLi4gbXV0YWJsZSBhbmQgaW1tdXRhYmxlLiBUaGUgYGNvbGxlY3Rpb25zLiAuLi4gaXMgcHJvdmlkZWQgdG8gbWFrZSAuLi4gZWFzaWVyIHRvIGNvcnJlY3RseSBpbXBsZW1lbnQgdGhlc2Ugb3BlcmF0aW9ucyBvbiBjdXN0b20gc2VxdWVuY2UgdHlwZXNcXG4uLi5cXG4jIyMgSW1tdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbm11dGFibGUgc2VxdWVuY2UgdHlwZXMgaXNcXG4uLi5cXG5zdXBwb3J0IGFsbG93cyBpbW11dGFibGUgc2VxdWVuY2VzLCAuLi4gLCB0byBiZSB1c2VkIGFzIGBkaWN0YCBrZXlzIGFuZCBzdG9yZWQgaW4gLi4uIGVuc2V0YCBpbnN0YW5jZXNcXG4uLi5cXG4jIyMgTXV0YWJsZSBTZXF1ZW5jZSBUeXBlc1xcdTAwYjZcXG4uLi5cXG4jIyMgTGlzdHNcXHUwMGI2XFxuXFxuTGlzdHMgYXJlIG11dGFibGUgc2VxdWVuY2VzLCB0eXBpY2FsbHkgdXNlZCB0byBzdG9yZSBjb2xsZWN0aW9ucyBvZiBob21vZ2VuZW91cyBpdGVtcyAod2hlcmUgdGhlIHByZWNpc2UgZGVncmVlIG9mIHNpbWlsYXJpdHkgd2lsbCB2YXJ5IGJ5IGFwcGxpY2F0aW9uKS5cXG4uLi5cXG4jIyMgVHVwbGVzXFx1MDBiNlxcblxcblR1cGxlcyBhcmUgaW1tdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaGV0ZXJvZ2VuZW91cyBkYXRhIChzdWNoIGFzIHRoZSAyLXR1cGxlcyBwcm9kdWNlZCBieSB0aGUgYGVudW1lcmF0ZSgpYCBidWlsdC1pbikuIFR1cGxlcyBhcmUgYWxzbyB1c2VkIGZvciBjYXNlcyB3aGVyZSBhbiBpbW11dGFibGUgc2VxdWVuY2Ugb2YgaG9tb2dlbmVvdXMgZGF0YSBpcyBuZWVkZWQgKHN1Y2ggYXMgYWxsb3dpbmcgc3RvcmFnZSBpbiBhIGBzZXRgIG9yIGBkaWN0YCBpbnN0YW5jZSkuXFxuLi4uXFxudHVwbGUoaXRlcmFibGU9XFxuLi4uXFxuVGhlIGNvbnN0cnVjdG9yIGJ1aWxkcyBhIHR1cGxlIHdob3NlIGl0ZW1zIGFyZSB0aGUgc2FtZSBhbmQgaW4gdGhlIHNhbWUgb3JkZXIgYXMgaXRlcmFibGVcXHUyMDE5cyBpdGVtcy4gaXRlcmFibGUgbWF5IGJlIGVpdGhlciBhIHNlcXVlbmNlLCBhIGNvbnRhaW5lciB0aGF0IHN1cHBvcnRzIGl0ZXJhdGlvbiwgb3IgYW4gaXRlcmF0b3Igb2JqZWN0LiBJZiBpdGVyYWJsZSBpcyBhbHJlYWR5IGEgdHVwbGUsIGl0IGlzIHJldHVybmVkIHVuY2hhbmdlZC4gRm9yIGV4YW1wbGUsIGB0dXBsZSgnYWJjJylgIHJldHVybnMgYCgnYScsICdiJywgJ2MnKWAgYW5kIGB0dXBsZSggWzEsIDIsIDNdIClgIHJldHVybnMgYCgxLCAyLCAzKWAuIElmIG5vIGFyZ3VtZW50IGlzIGdpdmVuLCB0aGUgY29uc3RydWN0b3IgY3JlYXRlcyBhIG5ldyBlbXB0eSB0dXBsZSwgYCgpYC5cXG4uLi5cXG5UdXBsZXMgaW1wbGVtZW50IGFsbCBvZiB0aGUgY29tbW9uIHNlcXVlbmNlIG9wZXJhdGlvbnMuXFxuLi4uXFxuRm9yIGhldGVyb2dlbmVvdXMgY29sbGVjdGlvbnMgb2YgZGF0YSB3aGVyZSBhY2Nlc3MgYnkgbmFtZSBpcyBjbGVhcmVyIHRoYW4gYWNjZXNzIGJ5IGluZGV4LCBgY29sbGVjdGlvbnMubmFtZWR0dXBsZSgpYCBtYXkgYmUgYSBtb3JlIGFwcHJvcHJpYXRlIGNob2ljZSB0aGFuIGEgc2ltcGxlIHR1cGxlIG9iamVjdC5cIl19LCB7XCJ0aXRsZVwiOiBcIkJ1aWx0LWluIFR5cGVzIFxcdTIwMTQgUHl0aG9uIDMuMTQuNSBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9saWJyYXJ5L3N0ZHR5cGVzLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcIlNvbWUgY29sbGVjdGlvbiBjbGFzc2VzIGFyZSBtdXRhYmxlLiBUaGUgbWV0aG9kcyB0aGF0IGFkZCwgc3VidHJhY3QsIG9yIHJlYXJyYW5nZSB0aGVpciAuLi4gaW4gcGxhY2UsIGFuZCBkb25cXHUyMDE5dCByZXR1cm4gYSAuLi4gLCBuZXZlciByZXR1cm4gdGhlIGNvbGxlY3Rpb24gaW5zdGFuY2UgaXRzZWxmIGJ1dCBgTm9uZWAuXFxuLi4uXFxuIyMgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBgbGlzdGAsIGB0dXBsZWAsIGByYW5nZWBcXHUwMGI2XFxuXFxuVGhlcmUgYXJlIHRocmVlIGJhc2ljIHNlcXVlbmNlIHR5cGVzOiBsaXN0cywgdHVwbGVzLCBhbmQgLi4uIG9iamVjdHMuIEFkZGl0aW9uYWwgc2VxdWVuY2UgdHlwZXMgdGFpbG9yZWQgZm9yIHByb2Nlc3Npbmcgb2YgYmluYXJ5IGRhdGEgYW5kIHRleHQgc3RyaW5ncyBhcmUgZGVzY3JpYmVkIGluIGRlZGljYXRlZCBzZWN0aW9ucy5cXG4uLi5cXG5UaGUgb3BlcmF0aW9ucyBpbiB0aGUgZm9sbG93aW5nIHRhYmxlIC4uLiBpbW11dGFibGUuIFRoZSBgIC4uLiBpcyBwcm92aWRlZCB0byBtYWtlIC4uLiBlYXNpZXIgdG8gY29ycmVjdGx5IGltcGxlbWVudCB0aGVzZSBvcGVyYXRpb25zIG9uIGN1c3RvbSBzZXF1ZW5jZSB0eXBlc1xcbi4uLlxcbiMjIyBJbW11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxub3BlcmF0aW9uIHRoYXQgaW1tdXRhYmxlIHNlcXVlbmNlIC4uLiBieSBtdXRhYmxlIHNlcXVlbmNlIHR5cGVzIGlzIC4uLiBgaGFzaCgpYFxcbi4uLlxcblRoaXMgc3VwcG9ydCBhbGxvd3MgaW1tdXRhYmxlIHNlcXVlbmNlcywgc3VjaCBhcyBgdHVwbGVgIGluc3RhbmNlcywgdG8gYmUgdXNlZCBhcyBgZGljdGAga2V5cyBhbmQgc3RvcmVkIGluIGBzZXRgIGFuZCBgZnJvemVuc2V0YCBpbnN0YW5jZXMuXFxuLi4uXFxuIyMjIE11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxuIyMjIExpc3RzXFx1MDBiNlxcblxcbkxpc3RzIGFyZSBtdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaG9tb2dlbmVvdXMgaXRlbXMgKHdoZXJlIHRoZSBwcmVjaXNlIGRlZ3JlZSBvZiBzaW1pbGFyaXR5IHdpbGwgdmFyeSBieSBhcHBsaWNhdGlvbikuXFxuLi4uXFxuIyMjIFR1cGxlc1xcdTAwYjZcXG5cXG5UdXBsZXMgYXJlIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhldGVyb2dlbmVvdXMgZGF0YSAoc3VjaCBhcyB0aGUgMi10dXBsZXMgcHJvZHVjZWQgYnkgdGhlIGBlbnVtZXJhdGUoKWAgYnVpbHQtaW4pLiBUdXBsZXMgYXJlIGFsc28gdXNlZCBmb3IgY2FzZXMgd2hlcmUgYW4gaW1tdXRhYmxlIHNlcXVlbmNlIG9mIGhvbW9nZW5lb3VzIGRhdGEgaXMgbmVlZGVkIChzdWNoIGFzIGFsbG93aW5nIHN0b3JhZ2UgaW4gYSBgc2V0YCBvciBgZGljdGAgaW5zdGFuY2UpLlxcbi4uLlxcblRoZSBjb25zdHJ1Y3RvciBidWlsZHMgYSB0dXBsZSB3aG9zZSBpdGVtcyBhcmUgdGhlIHNhbWUgYW5kIGluIHRoZSBzYW1lIG9yZGVyIGFzIGl0ZXJhYmxlXFx1MjAxOXMgaXRlbXMuIGl0ZXJhYmxlIG1heSBiZSBlaXRoZXIgYSBzZXF1ZW5jZSwgYSBjb250YWluZXIgdGhhdCBzdXBwb3J0cyBpdGVyYXRpb24sIG9yIGFuIGl0ZXJhdG9yIG9iamVjdC4gSWYgaXRlcmFibGUgaXMgYWxyZWFkeSBhIHR1cGxlLCBpdCBpcyByZXR1cm5lZCB1bmNoYW5nZWQuIEZvciBleGFtcGxlLCBgdHVwbGUoJ2FiYycpYCByZXR1cm5zIGAoJ2EnLCAnYicsICdjJylgIGFuZCBgdHVwbGUoIFsxLCAyLCAzXSApYCByZXR1cm5zIGAoMSwgMiwgMylgLiBJZiBubyBhcmd1bWVudCBpcyBnaXZlbiwgdGhlIGNvbnN0cnVjdG9yIGNyZWF0ZXMgYSBuZXcgZW1wdHkgdHVwbGUsIGAoKWAuXFxuLi4uXFxuVHVwbGVzIGltcGxlbWVudCBhbGwgb2YgdGhlIGNvbW1vbiBzZXF1ZW5jZSBvcGVyYXRpb25zLlxcbi4uLlxcbkZvciBoZXRlcm9nZW5lb3VzIGNvbGxlY3Rpb25zIG9mIGRhdGEgd2hlcmUgYWNjZXNzIGJ5IG5hbWUgaXMgY2xlYXJlciB0aGFuIGFjY2VzcyBieSBpbmRleCwgYGNvbGxlY3Rpb25zLm5hbWVkdHVwbGUoKWAgbWF5IGJlIGEgbW9yZSBhcHByb3ByaWF0ZSBjaG9pY2UgdGhhbiBhIHNpbXBsZSB0dXBsZSBvYmplY3QuXCJdfSwge1widGl0bGVcIjogXCI1LiBEYXRhIFN0cnVjdHVyZXMgXFx1MjAxNCBQeXRob24gMy4xMC4yMCBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy4xMC90dXRvcmlhbC9kYXRhc3RydWN0dXJlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJZb3UgbWlnaHQgaGF2ZSBub3RpY2VkIHRoYXQgbWV0aG9kcyBsaWtlIGBpbnNlcnRgLCBgcmVtb3ZlYCBvciBgc29ydGAgdGhhdCBvbmx5IG1vZGlmeSB0aGUgbGlzdCBoYXZlIG5vIHJldHVybiB2YWx1ZSBwcmludGVkIFxcdTIwMTMgdGhleSByZXR1cm4gdGhlIGRlZmF1bHQgYE5vbmVgLiAxIFRoaXMgaXMgYSBkZXNpZ24gcHJpbmNpcGxlIGZvciBhbGwgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgaW4gUHl0aG9uLlxcbi4uLlxcbiMjIDUuMy4gVHVwbGVzIGFuZCBTZXF1ZW5jZXNcXHUwMGI2XFxuXFxuV2Ugc2F3IHRoYXQgbGlzdHMgYW5kIHN0cmluZ3MgaGF2ZSBtYW55IGNvbW1vbiBwcm9wZXJ0aWVzLCBzdWNoIGFzIGluZGV4aW5nIGFuZCBzbGljaW5nIG9wZXJhdGlvbnMuIFRoZXkgYXJlIHR3byBleGFtcGxlcyBvZiBzZXF1ZW5jZSBkYXRhIHR5cGVzIChzZWUgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBsaXN0LCB0dXBsZSwgcmFuZ2UpLiBTaW5jZSBQeXRob24gaXMgYW4gZXZvbHZpbmcgbGFuZ3VhZ2UsIG90aGVyIHNlcXVlbmNlIGRhdGEgdHlwZXMgbWF5IGJlIGFkZGVkLiBUaGVyZSBpcyBhbHNvIGFub3RoZXIgc3RhbmRhcmQgc2VxdWVuY2UgZGF0YSB0eXBlOiB0aGUgdHVwbGUuXFxuXFxuQSB0dXBsZSBjb25zaXN0cyBvZiBhIG51bWJlciBvZiB2YWx1ZXMgc2VwYXJhdGVkIGJ5IGNvbW1hcywgZm9yIGluc3RhbmNlOlxcbi4uLlxcbj4+PiAjIFR1cGxlcyBhcmUgaW1tdXRhYmxlOlxcbi4uLiB0WzBdID0gODg4ODhcXG5UcmFjZWJhY2sgKG1vc3QgcmVjZW50IGNhbGwgbGFzdCk6XFxuICBGaWxlIFxcXCI8c3RkaW4+XFxcIiwgbGluZSAxLCBpbiA8bW9kdWxlPlxcblR5cGVFcnJvcjogJ3R1cGxlJyBvYmplY3QgZG9lcyBub3Qgc3VwcG9ydCBpdGVtIGFzc2lnbm1lbnRcXG4+Pj4gIyBidXQgdGhleSBjYW4gY29udGFpbiBtdXRhYmxlIG9iamVjdHM6XFxuLi4uIHYgPSAoWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuPj4+IHZcXG4oWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuLi4uXFxuQXMgeW91IHNlZSwgb24gb3V0cHV0IHR1cGxlcyBhcmUgYWx3YXlzIGVuY2xvc2VkIGluIHBhcmVudGhlc2VzLCBzbyB0aGF0IG5lc3RlZCB0dXBsZXMgYXJlIGludGVycHJldGVkIGNvcnJlY3RseTsgdGhleSBtYXkgYmUgaW5wdXQgd2l0aCBvciB3aXRob3V0IHN1cnJvdW5kaW5nIHBhcmVudGhlc2VzLCBhbHRob3VnaCBvZnRlbiBwYXJlbnRoZXNlcyBhcmUgbmVjZXNzYXJ5IGFueXdheSAoaWYgdGhlIHR1cGxlIGlzIHBhcnQgb2YgYSBsYXJnZXIgZXhwcmVzc2lvbikuIEl0IGlzIG5vdCBwb3NzaWJsZSB0byBhc3NpZ24gdG8gdGhlIGluZGl2aWR1YWwgaXRlbXMgb2YgYSB0dXBsZSwgaG93ZXZlciBpdCBpcyBwb3NzaWJsZSB0byBjcmVhdGUgdHVwbGVzIHdoaWNoIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzLCBzdWNoIGFzIGxpc3RzLlxcblxcblRob3VnaCB0dXBsZXMgbWF5IHNlZW0gc2ltaWxhciB0byBsaXN0cywgdGhleSBhcmUgb2Z0ZW4gdXNlZCBpbiBkaWZmZXJlbnQgc2l0dWF0aW9ucyBhbmQgZm9yIGRpZmZlcmVudCBwdXJwb3Nlcy4gVHVwbGVzIGFyZSBpbW11dGFibGUsIGFuZCB1c3VhbGx5IGNvbnRhaW4gYSBoZXRlcm9nZW5lb3VzIHNlcXVlbmNlIG9mIGVsZW1lbnRzIHRoYXQgYXJlIGFjY2Vzc2VkIHZpYSB1bnBhY2tpbmcgKHNlZSBsYXRlciBpbiB0aGlzIHNlY3Rpb24pIG9yIGluZGV4aW5nIChvciBldmVuIGJ5IGF0dHJpYnV0ZSBpbiB0aGUgY2FzZSBvZiBgbmFtZWR0dXBsZXNgKS4gTGlzdHMgYXJlIG11dGFibGUsIGFuZCB0aGVpciBlbGVtZW50cyBhcmUgdXN1YWxseSBob21vZ2VuZW91cyBhbmQgYXJlIGFjY2Vzc2VkIGJ5IGl0ZXJhdGluZyBvdmVyIHRoZSBsaXN0Llxcbi4uLlxcbiMjIDUuNC5cXG4uLi5cXG4uIFNldCBvYmplY3RzXFxuLi4uXFxuIyMgNS41LiBEaWN0aW9uYXJpZXNcXHUwMGI2XFxuXFxuQW5vdGhlciB1c2VmdWwgZGF0YSB0eXBlIGJ1aWx0IGludG8gUHl0aG9uIGlzIHRoZSBkaWN0aW9uYXJ5IChzZWUgTWFwcGluZyBUeXBlcyBcXHUyMDE0IGRpY3QpLiBEaWN0aW9uYXJpZXMgYXJlIHNvbWV0aW1lcyBmb3VuZCBpbiBvdGhlciBsYW5ndWFnZXMgYXMgXFx1MjAxY2Fzc29jaWF0aXZlIG1lbW9yaWVzXFx1MjAxZCBvciBcXHUyMDFjYXNzb2NpYXRpdmUgYXJyYXlzXFx1MjAxZC4gVW5saWtlIHNlcXVlbmNlcywgd2hpY2ggYXJlIGluZGV4ZWQgYnkgYSByYW5nZSBvZiBudW1iZXJzLCBkaWN0aW9uYXJpZXMgYXJlIGluZGV4ZWQgYnkga2V5cywgd2hpY2ggY2FuIGJlIGFueSBpbW11dGFibGUgdHlwZTsgc3RyaW5ncyBhbmQgbnVtYmVycyBjYW4gYWx3YXlzIGJlIGtleXMuIFR1cGxlcyBjYW4gYmUgdXNlZCBhcyBrZXlzIGlmIHRoZXkgY29udGFpbiBvbmx5IHN0cmluZ3MsIG51bWJlcnMsIG9yIHR1cGxlczsgaWYgYSB0dXBsZSBjb250YWlucyBhbnkgbXV0YWJsZSBvYmplY3QgZWl0aGVyIGRpcmVjdGx5IG9yIGluZGlyZWN0bHksIGl0IGNhbm5vdCBiZSB1c2VkIGFzIGEga2V5LiBZb3UgY2FuXFx1MjAxOXQgdXNlIGxpc3RzIGFzIGtleXMsIHNpbmNlIGxpc3RzIGNhbiBiZSBtb2RpZmllZCBpbiBwbGFjZSB1c2luZyBpbmRleCBhc3NpZ25tZW50cywgc2xpY2UgYXNzaWdubWVudHMsIG9yIG1ldGhvZHMgbGlrZSBgYXBwZW5kKClgIGFuZCBgZXh0ZW5kKClgLlwiXX1dIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJ0b29sIiwibmFtZSI6IndlYl9zZWFyY2giLCJpZCI6IjZlNTEyNWFjLTMxNjQtNGE3Ni05YzUyLWFjMzQ2MGNlMzZlNCIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsInN0YXR1cyI6InN1Y2Nlc3MifSx7ImNvbnRlbnQiOlt7ImlkIjoidG9vbHVfMDFYamozYVJkV3loQkpmUG1USkJ4UWE5IiwiY2FsbGVyIjp7InR5cGUiOiJkaXJlY3QifSwiaW5wdXQiOnt9LCJuYW1lIjoidHJhbnNmZXJfdG9fc2tlcHRpYyIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHdBNkY0Y0NONXloRDRDcnM5TSIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo3Mzc4LCJvdXRwdXRfdG9rZW5zIjozMywib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6MH0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoicmVzZWFyY2hlciIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy0zY2Q4LTc5ZjEtODhhMS1jZTZjZWUyOTRlNzktMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ0cmFuc2Zlcl90b19za2VwdGljIiwiYXJncyI6e30sImlkIjoidG9vbHVfMDFYamozYVJkV3loQkpmUG1USkJ4UWE5IiwidHlwZSI6InRvb2xfY2FsbCJ9XSwiaW52YWxpZF90b29sX2NhbGxzIjpbXSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjo3Mzc4LCJvdXRwdXRfdG9rZW5zIjozMywidG90YWxfdG9rZW5zIjo3NDExLCJpbnB1dF90b2tlbl9kZXRhaWxzIjp7ImNhY2hlX3JlYWQiOjAsImNhY2hlX2NyZWF0aW9uIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VuX2RldGFpbHMiOnsicmVhc29uaW5nIjowfX19LHsiY29udGVudCI6IlN1Y2Nlc3NmdWxseSB0cmFuc2ZlcnJlZCB0byBza2VwdGljIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJ0b29sIiwibmFtZSI6InRyYW5zZmVyX3RvX3NrZXB0aWMiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMVhqajNhUmRXeWhCSmZQbVRKQnhRYTkiLCJzdGF0dXMiOiJzdWNjZXNzIn1dLCJhY3RpdmVfYWdlbnQiOiJza2VwdGljIn0sInJlc3VtZSI6bnVsbCwiZ290byI6InNrZXB0aWMifX0=" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "d92a848281469e5a", + "parentSpanId": "1fd1c862b3a50355", + "name": "transfer_to_skeptic", + "kind": 1, + "startTimeUnixNano": "1790968023766263040", + "endTimeUnixNano": "1790968023766625792", + "attributes": [ + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "tool" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "transfer_to_skeptic" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "execute_tool" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "gen_ai.tool.name", + "value": { + "stringValue": "transfer_to_skeptic" + } + }, + { + "key": "gen_ai.tool.call.id", + "value": { + "stringValue": "toolu_01Xjj3aRdWyhBJfPmTJBxQa9" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "4" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "tools" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"__pregel_push\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_push\",0,false]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828|tools:ed0001c5-0402-bf5e-ceed-69e43e82ae5e" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "researcher" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828" + } + }, + { + "key": "langsmith.metadata.__handoff_destination", + "value": { + "stringValue": "skeptic" + } + }, + { + "key": "langsmith.metadata.ls_method", + "value": { + "stringValue": "traceable" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "seq:step:1" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "e30=" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJvdXRwdXQiOnsiZ3JhcGgiOiJfX3BhcmVudF9fIiwidXBkYXRlIjp7Im1lc3NhZ2VzIjpbeyJjb250ZW50IjoiRm9yIGEgc3ludGhldGljIHRyYWNpbmcgZml4dHVyZSwgZXhwbGFpbiBQeXRob24gdHVwbGUgdmVyc3VzIGxpc3QgaW1tdXRhYmlsaXR5IGluIGF0IG1vc3QgMTIwIHdvcmRzLiBVc2Ugb25seSBvZmZpY2lhbCBQeXRob24gZG9jdW1lbnRhdGlvbiBpZiB5b3Ugc2VhcmNoLiBIYXZlIHRoZSBza2VwdGljIGFuZCB2ZXJpZmllciBjaGVjayBpdCwgdGhlbiBoYXZlIHRoZSBlZGl0b3Igd3JpdGUgL2Fuc3dlci5tZC4gS2VlcCB0aGUgcnVuIHNob3J0IGFuZCBkbyBub3QgYXNrIGZvbGxvdy11cCBxdWVzdGlvbnMuIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJodW1hbiIsImlkIjoiNDYyMDQzMDUtZjhlZS00Mzg0LTljZTgtZmNkODBiZmNkZjc1In0seyJjb250ZW50IjpbeyJpZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7InF1ZXJ5IjoiZG9jcy5weXRob24ub3JnIHR1cGxlcyBpbW11dGFibGUgc2VxdWVuY2VzIGxpc3RzIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIHR1dG9yaWFsIn0sIm5hbWUiOiJ3ZWJfc2VhcmNoIiwidHlwZSI6InRvb2xfdXNlIiwidG9vbHNldF9uYW1lIjpudWxsfV0sImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2ZkdzlhVzlicmVuUHFneWJmTndQIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjI4NDUsIm91dHB1dF90b2tlbnMiOjczLCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjowfSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCIsIm1vZGVsX3Byb3ZpZGVyIjoiYW50aHJvcGljIn0sInR5cGUiOiJhaSIsIm5hbWUiOiJyZXNlYXJjaGVyIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTIxYTMtNzAwMC1hMzgxLTI4NGE2YTBjMDNmNy0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6IndlYl9zZWFyY2giLCJhcmdzIjp7InF1ZXJ5IjoiZG9jcy5weXRob24ub3JnIHR1cGxlcyBpbW11dGFibGUgc2VxdWVuY2VzIGxpc3RzIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIHR1dG9yaWFsIn0sImlkIjoidG9vbHVfMDFCMkxmSEdEallTRkpobWlYTmo0Q3VRIiwidHlwZSI6InRvb2xfY2FsbCJ9XSwiaW52YWxpZF90b29sX2NhbGxzIjpbXSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjoyODQ1LCJvdXRwdXRfdG9rZW5zIjo3MywidG90YWxfdG9rZW5zIjoyOTE4LCJpbnB1dF90b2tlbl9kZXRhaWxzIjp7ImNhY2hlX3JlYWQiOjAsImNhY2hlX2NyZWF0aW9uIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VuX2RldGFpbHMiOnsicmVhc29uaW5nIjowfX19LHsiY29udGVudCI6Ilt7XCJ0aXRsZVwiOiBcIjUuIERhdGEgU3RydWN0dXJlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiWW91IG1pZ2h0IGhhdmUgbm90aWNlZCB0aGF0IG1ldGhvZHMgbGlrZWBpbnNlcnRgLGByZW1vdmVgIG9yYHNvcnRgIHRoYXQgb25seSBtb2RpZnkgdGhlIGxpc3QgaGF2ZSBubyByZXR1cm4gdmFsdWUgcHJpbnRlZCBcXHUyMDEzIHRoZXkgcmV0dXJuIHRoZSBkZWZhdWx0YE5vbmVgLiBbMV0gVGhpcyBpcyBhIGRlc2lnbiBwcmluY2lwbGUgZm9yIGFsbCBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyBpbiBQeXRob24uXFxuLi4uXFxuIyMgNS4zLiBUdXBsZXMgYW5kIFNlcXVlbmNlc1xcdTAwYjZcXG5cXG5XZSBzYXcgdGhhdCBsaXN0cyBhbmQgc3RyaW5ncyBoYXZlIG1hbnkgY29tbW9uIHByb3BlcnRpZXMsIHN1Y2ggYXMgaW5kZXhpbmcgYW5kIHNsaWNpbmcgb3BlcmF0aW9ucy4gVGhleSBhcmUgdHdvIGV4YW1wbGVzIG9mIHNlcXVlbmNlIGRhdGEgdHlwZXMgKHNlZSBTZXF1ZW5jZSBUeXBlcyBcXHUyMDE0IGxpc3QsIHR1cGxlLCByYW5nZSkuIFNpbmNlIFB5dGhvbiBpcyBhbiBldm9sdmluZyBsYW5ndWFnZSwgb3RoZXIgc2VxdWVuY2UgZGF0YSB0eXBlcyBtYXkgYmUgYWRkZWQuIFRoZXJlIGlzIGFsc28gYW5vdGhlciBzdGFuZGFyZCBzZXF1ZW5jZSBkYXRhIHR5cGU6IHRoZSB0dXBsZS5cXG5cXG5BIHR1cGxlIGNvbnNpc3RzIG9mIGEgbnVtYmVyIG9mIHZhbHVlcyBzZXBhcmF0ZWQgYnkgY29tbWFzLCBmb3IgaW5zdGFuY2U6XFxuLi4uXFxuPj4+ICMgVHVwbGVzIGFyZSBpbW11dGFibGU6XFxuPj4+IHRbMF0gPSA4ODg4OFxcblRyYWNlYmFjayAobW9zdCByZWNlbnQgY2FsbCBsYXN0KTpcXG4gIEZpbGUgXFxcIjxzdGRpbj5cXFwiLCBsaW5lIDEsIGluIDxtb2R1bGU+XFxuVHlwZUVycm9yOiAndHVwbGUnIG9iamVjdCBkb2VzIG5vdCBzdXBwb3J0IGl0ZW0gYXNzaWdubWVudFxcbj4+PiAjIGJ1dCB0aGV5IGNhbiBjb250YWluIG11dGFibGUgb2JqZWN0czpcXG4+Pj4gdiA9IChbMSwgMiwgM10sIFszLCAyLCAxXSlcXG4+Pj4gdlxcbihbMSwgMiwgM10sIFszLCAyLCAxXSlcXG4uLi5cXG5BcyB5b3Ugc2VlLCBvbiBvdXRwdXQgdHVwbGVzIGFyZSBhbHdheXMgZW5jbG9zZWQgaW4gcGFyZW50aGVzZXMsIHNvIHRoYXQgbmVzdGVkIHR1cGxlcyBhcmUgaW50ZXJwcmV0ZWQgY29ycmVjdGx5OyB0aGV5IG1heSBiZSBpbnB1dCB3aXRoIG9yIHdpdGhvdXQgc3Vycm91bmRpbmcgcGFyZW50aGVzZXMsIGFsdGhvdWdoIG9mdGVuIHBhcmVudGhlc2VzIGFyZSBuZWNlc3NhcnkgYW55d2F5IChpZiB0aGUgdHVwbGUgaXMgcGFydCBvZiBhIGxhcmdlciBleHByZXNzaW9uKS4gSXQgaXMgbm90IHBvc3NpYmxlIHRvIGFzc2lnbiB0byB0aGUgaW5kaXZpZHVhbCBpdGVtcyBvZiBhIHR1cGxlLCBob3dldmVyIGl0IGlzIHBvc3NpYmxlIHRvIGNyZWF0ZSB0dXBsZXMgd2hpY2ggY29udGFpbiBtdXRhYmxlIG9iamVjdHMsIHN1Y2ggYXMgbGlzdHMuXFxuXFxuVGhvdWdoIHR1cGxlcyBtYXkgc2VlbSBzaW1pbGFyIHRvIGxpc3RzLCB0aGV5IGFyZSBvZnRlbiB1c2VkIGluIGRpZmZlcmVudCBzaXR1YXRpb25zIGFuZCBmb3IgZGlmZmVyZW50IHB1cnBvc2VzLiBUdXBsZXMgYXJlIGltbXV0YWJsZSwgYW5kIHVzdWFsbHkgY29udGFpbiBhIGhldGVyb2dlbmVvdXMgc2VxdWVuY2Ugb2YgZWxlbWVudHMgdGhhdCBhcmUgYWNjZXNzZWQgdmlhIHVucGFja2luZyAoc2VlIGxhdGVyIGluIHRoaXMgc2VjdGlvbikgb3IgaW5kZXhpbmcgKG9yIGV2ZW4gYnkgYXR0cmlidXRlIGluIHRoZSBjYXNlIG9mIG5hbWVkdHVwbGVzKS4gTGlzdHMgYXJlIG11dGFibGUsIGFuZCB0aGVpciBlbGVtZW50cyBhcmUgdXN1YWxseSBob21vZ2VuZW91cyBhbmQgYXJlIGFjY2Vzc2VkIGJ5IGl0ZXJhdGluZyBvdmVyIHRoZSBsaXN0Llxcbi4uLlxcbkFub3RoZXIgdXNlZnVsIGRhdGEgdHlwZSBidWlsdCBpbnRvIFB5dGhvbiBpcyB0aGUgZGljdGlvbmFyeSAoc2VlIE1hcHBpbmcgVHlwZXMgXFx1MjAxNCBkaWN0KS4gRGljdGlvbmFyaWVzIGFyZSBzb21ldGltZXMgZm91bmQgaW4gb3RoZXIgbGFuZ3VhZ2VzIGFzIFxcdTIwMWNhc3NvY2lhdGl2ZSBtZW1vcmllc1xcdTIwMWQgb3IgXFx1MjAxY2Fzc29jaWF0aXZlIGFycmF5c1xcdTIwMWQuIFVubGlrZSBzZXF1ZW5jZXMsIHdoaWNoIGFyZSBpbmRleGVkIGJ5IGEgcmFuZ2Ugb2YgbnVtYmVycywgZGljdGlvbmFyaWVzIGFyZSBpbmRleGVkIGJ5IGtleXMsIHdoaWNoIGNhbiBiZSBhbnkgaW1tdXRhYmxlIHR5cGU7IHN0cmluZ3MgYW5kIG51bWJlcnMgY2FuIGFsd2F5cyBiZSBrZXlzLiBUdXBsZXMgY2FuIGJlIHVzZWQgYXMga2V5cyBpZiB0aGV5IGNvbnRhaW4gb25seSBzdHJpbmdzLCBudW1iZXJzLCBvciB0dXBsZXM7IGlmIGEgdHVwbGUgY29udGFpbnMgYW55IG11dGFibGUgb2JqZWN0IGVpdGhlciBkaXJlY3RseSBvciBpbmRpcmVjdGx5LCBpdCBjYW5ub3QgYmUgdXNlZCBhcyBhIGtleS4gWW91IGNhblxcdTIwMTl0IHVzZSBsaXN0cyBhcyBrZXlzLCBzaW5jZSBsaXN0cyBjYW4gYmUgbW9kaWZpZWQgaW4gcGxhY2UgdXNpbmcgaW5kZXggYXNzaWdubWVudHMsIHNsaWNlIGFzc2lnbm1lbnRzLCBvciBtZXRob2RzIGxpa2UgYXBwZW5kKCkgYW5kIGV4dGVuZCgpLlwiXX0sIHtcInRpdGxlXCI6IFwiMy4gRGF0YSBtb2RlbCBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvcmVmZXJlbmNlL2RhdGFtb2RlbC5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJUaGUgdmFsdWUgb2Ygc29tZSBvYmplY3RzIGNhbiBjaGFuZ2UuIE9iamVjdHMgd2hvc2UgdmFsdWUgY2FuIGNoYW5nZSBhcmUgc2FpZCB0byBiZSBtdXRhYmxlOyBvYmplY3RzIHdob3NlIHZhbHVlIGlzIHVuY2hhbmdlYWJsZSBvbmNlIHRoZXkgYXJlIGNyZWF0ZWQgYXJlIGNhbGxlZCBpbW11dGFibGUuIChUaGUgdmFsdWUgb2YgYW4gaW1tdXRhYmxlIGNvbnRhaW5lciBvYmplY3QgdGhhdCBjb250YWlucyBhIHJlZmVyZW5jZSB0byBhIG11dGFibGUgb2JqZWN0IGNhbiBjaGFuZ2Ugd2hlbiB0aGUgbGF0dGVyXFx1MjAxOXMgdmFsdWUgaXMgY2hhbmdlZDsgaG93ZXZlciB0aGUgY29udGFpbmVyIGlzIHN0aWxsIGNvbnNpZGVyZWQgaW1tdXRhYmxlLCBiZWNhdXNlIHRoZSBjb2xsZWN0aW9uIG9mIG9iamVjdHMgaXQgY29udGFpbnMgY2Fubm90IGJlIGNoYW5nZWQuIFNvLCBpbW11dGFiaWxpdHkgaXMgbm90IHN0cmljdGx5IHRoZSBzYW1lIGFzIGhhdmluZyBhbiB1bmNoYW5nZWFibGUgdmFsdWUsIGl0IGlzIG1vcmUgc3VidGxlLikgQW4gb2JqZWN0XFx1MjAxOXMgbXV0YWJpbGl0eSBpcyBkZXRlcm1pbmVkIGJ5IGl0cyB0eXBlOyBmb3IgaW5zdGFuY2UsIG51bWJlcnMsIHN0cmluZ3MgYW5kIHR1cGxlcyBhcmUgaW1tdXRhYmxlLCB3aGlsZSBkaWN0aW9uYXJpZXMgYW5kIGxpc3RzIGFyZSBtdXRhYmxlLlxcbi4uLlxcblNvbWUgb2JqZWN0cyBjb250YWluIHJlZmVyZW5jZXMgdG8gb3RoZXIgb2JqZWN0czsgdGhlc2UgYXJlIGNhbGxlZCBjb250YWluZXJzLiBFeGFtcGxlcyBvZiBjb250YWluZXJzIGFyZSB0dXBsZXMsIGxpc3RzIGFuZCBkaWN0aW9uYXJpZXMuIFRoZSByZWZlcmVuY2VzIGFyZSBwYXJ0IG9mIGEgY29udGFpbmVyXFx1MjAxOXMgdmFsdWUuIEluIG1vc3QgY2FzZXMsIHdoZW4gd2UgdGFsayBhYm91dCB0aGUgdmFsdWUgb2YgYSBjb250YWluZXIsIHdlIGltcGx5IHRoZSB2YWx1ZXMsIG5vdCB0aGUgaWRlbnRpdGllcyBvZiB0aGUgY29udGFpbmVkIG9iamVjdHM7IGhvd2V2ZXIsIHdoZW4gd2UgdGFsayBhYm91dCB0aGUgbXV0YWJpbGl0eSBvZiBhIGNvbnRhaW5lciwgb25seSB0aGUgaWRlbnRpdGllcyBvZiB0aGUgaW1tZWRpYXRlbHkgY29udGFpbmVkIG9iamVjdHMgYXJlIGltcGxpZWQuIFNvLCBpZiBhbiBpbW11dGFibGUgY29udGFpbmVyIChsaWtlIGEgdHVwbGUpIGNvbnRhaW5zIGEgcmVmZXJlbmNlIHRvIGEgbXV0YWJsZSBvYmplY3QsIGl0cyB2YWx1ZSBjaGFuZ2VzIGlmIHRoYXQgbXV0YWJsZSBvYmplY3QgaXMgY2hhbmdlZC5cXG4uLi5cXG4jIyMgMy4yLjUuIFNlcXVlbmNlc1xcdTAwYjZcXG4uLi5cXG4jIyMjIDMuMi41LjEuIEltbXV0YWJsZSBzZXF1ZW5jZXNcXHUwMGI2XFxuXFxuQW4gb2JqZWN0IG9mIGFuIGltbXV0YWJsZSBzZXF1ZW5jZSB0eXBlIGNhbm5vdCBjaGFuZ2Ugb25jZSBpdCBpcyBjcmVhdGVkLiAoSWYgdGhlIG9iamVjdCBjb250YWlucyByZWZlcmVuY2VzIHRvIG90aGVyIG9iamVjdHMsIHRoZXNlIG90aGVyIG9iamVjdHMgbWF5IGJlIG11dGFibGUgYW5kIG1heSBiZSBjaGFuZ2VkOyBob3dldmVyLCB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGRpcmVjdGx5IHJlZmVyZW5jZWQgYnkgYW4gaW1tdXRhYmxlIG9iamVjdCBjYW5ub3QgY2hhbmdlLilcXG5cXG5UaGUgZm9sbG93aW5nIHR5cGVzIGFyZSBpbW11dGFibGUgc2VxdWVuY2VzOlxcbi4uLlxcblR1cGxlc1xcbjogVGhlIGl0ZW1zIG9mIGEgYHR1cGxlYCBhcmUgYXJiaXRyYXJ5IFB5dGhvbiBvYmplY3RzLiBUdXBsZXMgb2YgdHdvIG9yIG1vcmUgaXRlbXMgYXJlIGZvcm1lZCBieSBjb21tYS1zZXBhcmF0ZWQgbGlzdHMgb2YgZXhwcmVzc2lvbnMuIEEgdHVwbGUgb2Ygb25lIGl0ZW0gKGEgXFx1MjAxOHNpbmdsZXRvblxcdTIwMTkpIGNhbiBiZSBmb3JtZWQgYnkgYWZmaXhpbmcgYSBjb21tYSB0byBhbiBleHByZXNzaW9uIChhbiBleHByZXNzaW9uIGJ5IGl0c2VsZiBkb2VzIG5vdCBjcmVhdGUgYSB0dXBsZSwgc2luY2UgcGFyZW50aGVzZXMgbXVzdCBiZSB1c2FibGUgZm9yIGdyb3VwaW5nIG9mIGV4cHJlc3Npb25zKS4gQW4gZW1wdHkgdHVwbGUgY2FuIGJlIGZvcm1lZCBieSBhbiBlbXB0eSBwYWlyIG9mIHBhcmVudGhlc2VzLlxcbi4uLlxcbiMjIyMgMy4yLjUuMi4gTXV0YWJsZSBzZXF1ZW5jZXNcXHUwMGI2XFxuXFxuTXV0YWJsZSBzZXF1ZW5jZXMgY2FuIGJlIGNoYW5nZWQgYWZ0ZXIgdGhleSBhcmUgY3JlYXRlZC4gVGhlIHN1YnNjcmlwdGlvbiBhbmQgc2xpY2luZyBub3RhdGlvbnMgY2FuIGJlIHVzZWQgYXMgdGhlIHRhcmdldCBvZiBhc3NpZ25tZW50IGFuZCBgZGVsYCAoZGVsZXRlKSBzdGF0ZW1lbnRzLlxcbi4uLlxcblRoZXJlIGFyZSBjdXJyZW50bHkgdHdvIGludHJpbnNpYyBtdXRhYmxlIHNlcXVlbmNlIHR5cGVzOlxcblxcbkxpc3RzXFxuOiBUaGUgaXRlbXMgb2YgYSBsaXN0IGFyZSBhcmJpdHJhcnkgUHl0aG9uIG9iamVjdHMuIExpc3RzIGFyZSBmb3JtZWQgYnkgcGxhY2luZyBhIGNvbW1hLXNlcGFyYXRlZCBsaXN0IG9mIGV4cHJlc3Npb25zIGluIHNxdWFyZSBicmFja2V0cy4gKE5vdGUgdGhhdCB0aGVyZSBhcmUgbm8gc3BlY2lhbCBjYXNlcyBuZWVkZWQgdG8gZm9ybSBsaXN0cyBvZiBsZW5ndGggMCBvciAxLilcIl19LCB7XCJ0aXRsZVwiOiBcIkJ1aWx0LWluIFR5cGVzIFxcdTIwMTQgUHl0aG9uIDMuMTQuNyBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9idWlsdGlucy9zdGR0eXBlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJjb2xsZWN0aW9uIGNsYXNzZXMgYXJlIG11dGFibGUuIFRoZSBtZXRob2RzIHRoYXQgYWRkLCBzdWJ0cmFjdCwgb3IgcmVhcnJhbmdlIHRoZWlyIC4uLiBpbiBwbGFjZSwgYW5kIGRvblxcdTIwMTl0IHJldHVybiBhIC4uLiAsIG5ldmVyIHJldHVybiB0aGUgY29sbGVjdGlvbiBpbnN0YW5jZSBpdHNlbGYgYnV0IGBOb25lYC5cXG4uLi5cXG4jIyBTZXF1ZW5jZSBUeXBlcyBcXHUyMDE0IGBsaXN0YCwgYHR1cGxlYCwgYHJhbmdlYFxcdTAwYjZcXG5cXG5UaGVyZSBhcmUgdGhyZWUgYmFzaWMgc2VxdWVuY2UgdHlwZXM6IGxpc3RzLCB0dXBsZXMsIGFuZCByYW5nZSBvYmplY3RzLiBBZGRpdGlvbmFsIHNlcXVlbmNlIHR5cGVzIHRhaWxvcmVkIGZvciBwcm9jZXNzaW5nIG9mIGJpbmFyeSBkYXRhIGFuZCB0ZXh0IHN0cmluZ3MgYXJlIGRlc2NyaWJlZCBpbiBkZWRpY2F0ZWQgc2VjdGlvbnMuXFxuLi4uXFxuVGhlIG9wZXJhdGlvbnMgaW4gdGhlIGZvbGxvd2luZyB0YWJsZSAuLi4gbXV0YWJsZSBhbmQgaW1tdXRhYmxlLiBUaGUgYGNvbGxlY3Rpb25zLiAuLi4gaXMgcHJvdmlkZWQgdG8gbWFrZSAuLi4gZWFzaWVyIHRvIGNvcnJlY3RseSBpbXBsZW1lbnQgdGhlc2Ugb3BlcmF0aW9ucyBvbiBjdXN0b20gc2VxdWVuY2UgdHlwZXNcXG4uLi5cXG4jIyMgSW1tdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbm11dGFibGUgc2VxdWVuY2UgdHlwZXMgaXNcXG4uLi5cXG5zdXBwb3J0IGFsbG93cyBpbW11dGFibGUgc2VxdWVuY2VzLCAuLi4gLCB0byBiZSB1c2VkIGFzIGBkaWN0YCBrZXlzIGFuZCBzdG9yZWQgaW4gLi4uIGVuc2V0YCBpbnN0YW5jZXNcXG4uLi5cXG4jIyMgTXV0YWJsZSBTZXF1ZW5jZSBUeXBlc1xcdTAwYjZcXG4uLi5cXG4jIyMgTGlzdHNcXHUwMGI2XFxuXFxuTGlzdHMgYXJlIG11dGFibGUgc2VxdWVuY2VzLCB0eXBpY2FsbHkgdXNlZCB0byBzdG9yZSBjb2xsZWN0aW9ucyBvZiBob21vZ2VuZW91cyBpdGVtcyAod2hlcmUgdGhlIHByZWNpc2UgZGVncmVlIG9mIHNpbWlsYXJpdHkgd2lsbCB2YXJ5IGJ5IGFwcGxpY2F0aW9uKS5cXG4uLi5cXG4jIyMgVHVwbGVzXFx1MDBiNlxcblxcblR1cGxlcyBhcmUgaW1tdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaGV0ZXJvZ2VuZW91cyBkYXRhIChzdWNoIGFzIHRoZSAyLXR1cGxlcyBwcm9kdWNlZCBieSB0aGUgYGVudW1lcmF0ZSgpYCBidWlsdC1pbikuIFR1cGxlcyBhcmUgYWxzbyB1c2VkIGZvciBjYXNlcyB3aGVyZSBhbiBpbW11dGFibGUgc2VxdWVuY2Ugb2YgaG9tb2dlbmVvdXMgZGF0YSBpcyBuZWVkZWQgKHN1Y2ggYXMgYWxsb3dpbmcgc3RvcmFnZSBpbiBhIGBzZXRgIG9yIGBkaWN0YCBpbnN0YW5jZSkuXFxuLi4uXFxudHVwbGUoaXRlcmFibGU9XFxuLi4uXFxuVGhlIGNvbnN0cnVjdG9yIGJ1aWxkcyBhIHR1cGxlIHdob3NlIGl0ZW1zIGFyZSB0aGUgc2FtZSBhbmQgaW4gdGhlIHNhbWUgb3JkZXIgYXMgaXRlcmFibGVcXHUyMDE5cyBpdGVtcy4gaXRlcmFibGUgbWF5IGJlIGVpdGhlciBhIHNlcXVlbmNlLCBhIGNvbnRhaW5lciB0aGF0IHN1cHBvcnRzIGl0ZXJhdGlvbiwgb3IgYW4gaXRlcmF0b3Igb2JqZWN0LiBJZiBpdGVyYWJsZSBpcyBhbHJlYWR5IGEgdHVwbGUsIGl0IGlzIHJldHVybmVkIHVuY2hhbmdlZC4gRm9yIGV4YW1wbGUsIGB0dXBsZSgnYWJjJylgIHJldHVybnMgYCgnYScsICdiJywgJ2MnKWAgYW5kIGB0dXBsZSggWzEsIDIsIDNdIClgIHJldHVybnMgYCgxLCAyLCAzKWAuIElmIG5vIGFyZ3VtZW50IGlzIGdpdmVuLCB0aGUgY29uc3RydWN0b3IgY3JlYXRlcyBhIG5ldyBlbXB0eSB0dXBsZSwgYCgpYC5cXG4uLi5cXG5UdXBsZXMgaW1wbGVtZW50IGFsbCBvZiB0aGUgY29tbW9uIHNlcXVlbmNlIG9wZXJhdGlvbnMuXFxuLi4uXFxuRm9yIGhldGVyb2dlbmVvdXMgY29sbGVjdGlvbnMgb2YgZGF0YSB3aGVyZSBhY2Nlc3MgYnkgbmFtZSBpcyBjbGVhcmVyIHRoYW4gYWNjZXNzIGJ5IGluZGV4LCBgY29sbGVjdGlvbnMubmFtZWR0dXBsZSgpYCBtYXkgYmUgYSBtb3JlIGFwcHJvcHJpYXRlIGNob2ljZSB0aGFuIGEgc2ltcGxlIHR1cGxlIG9iamVjdC5cIl19LCB7XCJ0aXRsZVwiOiBcIkJ1aWx0LWluIFR5cGVzIFxcdTIwMTQgUHl0aG9uIDMuMTQuNSBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9saWJyYXJ5L3N0ZHR5cGVzLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcIlNvbWUgY29sbGVjdGlvbiBjbGFzc2VzIGFyZSBtdXRhYmxlLiBUaGUgbWV0aG9kcyB0aGF0IGFkZCwgc3VidHJhY3QsIG9yIHJlYXJyYW5nZSB0aGVpciAuLi4gaW4gcGxhY2UsIGFuZCBkb25cXHUyMDE5dCByZXR1cm4gYSAuLi4gLCBuZXZlciByZXR1cm4gdGhlIGNvbGxlY3Rpb24gaW5zdGFuY2UgaXRzZWxmIGJ1dCBgTm9uZWAuXFxuLi4uXFxuIyMgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBgbGlzdGAsIGB0dXBsZWAsIGByYW5nZWBcXHUwMGI2XFxuXFxuVGhlcmUgYXJlIHRocmVlIGJhc2ljIHNlcXVlbmNlIHR5cGVzOiBsaXN0cywgdHVwbGVzLCBhbmQgLi4uIG9iamVjdHMuIEFkZGl0aW9uYWwgc2VxdWVuY2UgdHlwZXMgdGFpbG9yZWQgZm9yIHByb2Nlc3Npbmcgb2YgYmluYXJ5IGRhdGEgYW5kIHRleHQgc3RyaW5ncyBhcmUgZGVzY3JpYmVkIGluIGRlZGljYXRlZCBzZWN0aW9ucy5cXG4uLi5cXG5UaGUgb3BlcmF0aW9ucyBpbiB0aGUgZm9sbG93aW5nIHRhYmxlIC4uLiBpbW11dGFibGUuIFRoZSBgIC4uLiBpcyBwcm92aWRlZCB0byBtYWtlIC4uLiBlYXNpZXIgdG8gY29ycmVjdGx5IGltcGxlbWVudCB0aGVzZSBvcGVyYXRpb25zIG9uIGN1c3RvbSBzZXF1ZW5jZSB0eXBlc1xcbi4uLlxcbiMjIyBJbW11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxub3BlcmF0aW9uIHRoYXQgaW1tdXRhYmxlIHNlcXVlbmNlIC4uLiBieSBtdXRhYmxlIHNlcXVlbmNlIHR5cGVzIGlzIC4uLiBgaGFzaCgpYFxcbi4uLlxcblRoaXMgc3VwcG9ydCBhbGxvd3MgaW1tdXRhYmxlIHNlcXVlbmNlcywgc3VjaCBhcyBgdHVwbGVgIGluc3RhbmNlcywgdG8gYmUgdXNlZCBhcyBgZGljdGAga2V5cyBhbmQgc3RvcmVkIGluIGBzZXRgIGFuZCBgZnJvemVuc2V0YCBpbnN0YW5jZXMuXFxuLi4uXFxuIyMjIE11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxuIyMjIExpc3RzXFx1MDBiNlxcblxcbkxpc3RzIGFyZSBtdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaG9tb2dlbmVvdXMgaXRlbXMgKHdoZXJlIHRoZSBwcmVjaXNlIGRlZ3JlZSBvZiBzaW1pbGFyaXR5IHdpbGwgdmFyeSBieSBhcHBsaWNhdGlvbikuXFxuLi4uXFxuIyMjIFR1cGxlc1xcdTAwYjZcXG5cXG5UdXBsZXMgYXJlIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhldGVyb2dlbmVvdXMgZGF0YSAoc3VjaCBhcyB0aGUgMi10dXBsZXMgcHJvZHVjZWQgYnkgdGhlIGBlbnVtZXJhdGUoKWAgYnVpbHQtaW4pLiBUdXBsZXMgYXJlIGFsc28gdXNlZCBmb3IgY2FzZXMgd2hlcmUgYW4gaW1tdXRhYmxlIHNlcXVlbmNlIG9mIGhvbW9nZW5lb3VzIGRhdGEgaXMgbmVlZGVkIChzdWNoIGFzIGFsbG93aW5nIHN0b3JhZ2UgaW4gYSBgc2V0YCBvciBgZGljdGAgaW5zdGFuY2UpLlxcbi4uLlxcblRoZSBjb25zdHJ1Y3RvciBidWlsZHMgYSB0dXBsZSB3aG9zZSBpdGVtcyBhcmUgdGhlIHNhbWUgYW5kIGluIHRoZSBzYW1lIG9yZGVyIGFzIGl0ZXJhYmxlXFx1MjAxOXMgaXRlbXMuIGl0ZXJhYmxlIG1heSBiZSBlaXRoZXIgYSBzZXF1ZW5jZSwgYSBjb250YWluZXIgdGhhdCBzdXBwb3J0cyBpdGVyYXRpb24sIG9yIGFuIGl0ZXJhdG9yIG9iamVjdC4gSWYgaXRlcmFibGUgaXMgYWxyZWFkeSBhIHR1cGxlLCBpdCBpcyByZXR1cm5lZCB1bmNoYW5nZWQuIEZvciBleGFtcGxlLCBgdHVwbGUoJ2FiYycpYCByZXR1cm5zIGAoJ2EnLCAnYicsICdjJylgIGFuZCBgdHVwbGUoIFsxLCAyLCAzXSApYCByZXR1cm5zIGAoMSwgMiwgMylgLiBJZiBubyBhcmd1bWVudCBpcyBnaXZlbiwgdGhlIGNvbnN0cnVjdG9yIGNyZWF0ZXMgYSBuZXcgZW1wdHkgdHVwbGUsIGAoKWAuXFxuLi4uXFxuVHVwbGVzIGltcGxlbWVudCBhbGwgb2YgdGhlIGNvbW1vbiBzZXF1ZW5jZSBvcGVyYXRpb25zLlxcbi4uLlxcbkZvciBoZXRlcm9nZW5lb3VzIGNvbGxlY3Rpb25zIG9mIGRhdGEgd2hlcmUgYWNjZXNzIGJ5IG5hbWUgaXMgY2xlYXJlciB0aGFuIGFjY2VzcyBieSBpbmRleCwgYGNvbGxlY3Rpb25zLm5hbWVkdHVwbGUoKWAgbWF5IGJlIGEgbW9yZSBhcHByb3ByaWF0ZSBjaG9pY2UgdGhhbiBhIHNpbXBsZSB0dXBsZSBvYmplY3QuXCJdfSwge1widGl0bGVcIjogXCI1LiBEYXRhIFN0cnVjdHVyZXMgXFx1MjAxNCBQeXRob24gMy4xMC4yMCBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy4xMC90dXRvcmlhbC9kYXRhc3RydWN0dXJlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJZb3UgbWlnaHQgaGF2ZSBub3RpY2VkIHRoYXQgbWV0aG9kcyBsaWtlIGBpbnNlcnRgLCBgcmVtb3ZlYCBvciBgc29ydGAgdGhhdCBvbmx5IG1vZGlmeSB0aGUgbGlzdCBoYXZlIG5vIHJldHVybiB2YWx1ZSBwcmludGVkIFxcdTIwMTMgdGhleSByZXR1cm4gdGhlIGRlZmF1bHQgYE5vbmVgLiAxIFRoaXMgaXMgYSBkZXNpZ24gcHJpbmNpcGxlIGZvciBhbGwgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgaW4gUHl0aG9uLlxcbi4uLlxcbiMjIDUuMy4gVHVwbGVzIGFuZCBTZXF1ZW5jZXNcXHUwMGI2XFxuXFxuV2Ugc2F3IHRoYXQgbGlzdHMgYW5kIHN0cmluZ3MgaGF2ZSBtYW55IGNvbW1vbiBwcm9wZXJ0aWVzLCBzdWNoIGFzIGluZGV4aW5nIGFuZCBzbGljaW5nIG9wZXJhdGlvbnMuIFRoZXkgYXJlIHR3byBleGFtcGxlcyBvZiBzZXF1ZW5jZSBkYXRhIHR5cGVzIChzZWUgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBsaXN0LCB0dXBsZSwgcmFuZ2UpLiBTaW5jZSBQeXRob24gaXMgYW4gZXZvbHZpbmcgbGFuZ3VhZ2UsIG90aGVyIHNlcXVlbmNlIGRhdGEgdHlwZXMgbWF5IGJlIGFkZGVkLiBUaGVyZSBpcyBhbHNvIGFub3RoZXIgc3RhbmRhcmQgc2VxdWVuY2UgZGF0YSB0eXBlOiB0aGUgdHVwbGUuXFxuXFxuQSB0dXBsZSBjb25zaXN0cyBvZiBhIG51bWJlciBvZiB2YWx1ZXMgc2VwYXJhdGVkIGJ5IGNvbW1hcywgZm9yIGluc3RhbmNlOlxcbi4uLlxcbj4+PiAjIFR1cGxlcyBhcmUgaW1tdXRhYmxlOlxcbi4uLiB0WzBdID0gODg4ODhcXG5UcmFjZWJhY2sgKG1vc3QgcmVjZW50IGNhbGwgbGFzdCk6XFxuICBGaWxlIFxcXCI8c3RkaW4+XFxcIiwgbGluZSAxLCBpbiA8bW9kdWxlPlxcblR5cGVFcnJvcjogJ3R1cGxlJyBvYmplY3QgZG9lcyBub3Qgc3VwcG9ydCBpdGVtIGFzc2lnbm1lbnRcXG4+Pj4gIyBidXQgdGhleSBjYW4gY29udGFpbiBtdXRhYmxlIG9iamVjdHM6XFxuLi4uIHYgPSAoWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuPj4+IHZcXG4oWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuLi4uXFxuQXMgeW91IHNlZSwgb24gb3V0cHV0IHR1cGxlcyBhcmUgYWx3YXlzIGVuY2xvc2VkIGluIHBhcmVudGhlc2VzLCBzbyB0aGF0IG5lc3RlZCB0dXBsZXMgYXJlIGludGVycHJldGVkIGNvcnJlY3RseTsgdGhleSBtYXkgYmUgaW5wdXQgd2l0aCBvciB3aXRob3V0IHN1cnJvdW5kaW5nIHBhcmVudGhlc2VzLCBhbHRob3VnaCBvZnRlbiBwYXJlbnRoZXNlcyBhcmUgbmVjZXNzYXJ5IGFueXdheSAoaWYgdGhlIHR1cGxlIGlzIHBhcnQgb2YgYSBsYXJnZXIgZXhwcmVzc2lvbikuIEl0IGlzIG5vdCBwb3NzaWJsZSB0byBhc3NpZ24gdG8gdGhlIGluZGl2aWR1YWwgaXRlbXMgb2YgYSB0dXBsZSwgaG93ZXZlciBpdCBpcyBwb3NzaWJsZSB0byBjcmVhdGUgdHVwbGVzIHdoaWNoIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzLCBzdWNoIGFzIGxpc3RzLlxcblxcblRob3VnaCB0dXBsZXMgbWF5IHNlZW0gc2ltaWxhciB0byBsaXN0cywgdGhleSBhcmUgb2Z0ZW4gdXNlZCBpbiBkaWZmZXJlbnQgc2l0dWF0aW9ucyBhbmQgZm9yIGRpZmZlcmVudCBwdXJwb3Nlcy4gVHVwbGVzIGFyZSBpbW11dGFibGUsIGFuZCB1c3VhbGx5IGNvbnRhaW4gYSBoZXRlcm9nZW5lb3VzIHNlcXVlbmNlIG9mIGVsZW1lbnRzIHRoYXQgYXJlIGFjY2Vzc2VkIHZpYSB1bnBhY2tpbmcgKHNlZSBsYXRlciBpbiB0aGlzIHNlY3Rpb24pIG9yIGluZGV4aW5nIChvciBldmVuIGJ5IGF0dHJpYnV0ZSBpbiB0aGUgY2FzZSBvZiBgbmFtZWR0dXBsZXNgKS4gTGlzdHMgYXJlIG11dGFibGUsIGFuZCB0aGVpciBlbGVtZW50cyBhcmUgdXN1YWxseSBob21vZ2VuZW91cyBhbmQgYXJlIGFjY2Vzc2VkIGJ5IGl0ZXJhdGluZyBvdmVyIHRoZSBsaXN0Llxcbi4uLlxcbiMjIDUuNC5cXG4uLi5cXG4uIFNldCBvYmplY3RzXFxuLi4uXFxuIyMgNS41LiBEaWN0aW9uYXJpZXNcXHUwMGI2XFxuXFxuQW5vdGhlciB1c2VmdWwgZGF0YSB0eXBlIGJ1aWx0IGludG8gUHl0aG9uIGlzIHRoZSBkaWN0aW9uYXJ5IChzZWUgTWFwcGluZyBUeXBlcyBcXHUyMDE0IGRpY3QpLiBEaWN0aW9uYXJpZXMgYXJlIHNvbWV0aW1lcyBmb3VuZCBpbiBvdGhlciBsYW5ndWFnZXMgYXMgXFx1MjAxY2Fzc29jaWF0aXZlIG1lbW9yaWVzXFx1MjAxZCBvciBcXHUyMDFjYXNzb2NpYXRpdmUgYXJyYXlzXFx1MjAxZC4gVW5saWtlIHNlcXVlbmNlcywgd2hpY2ggYXJlIGluZGV4ZWQgYnkgYSByYW5nZSBvZiBudW1iZXJzLCBkaWN0aW9uYXJpZXMgYXJlIGluZGV4ZWQgYnkga2V5cywgd2hpY2ggY2FuIGJlIGFueSBpbW11dGFibGUgdHlwZTsgc3RyaW5ncyBhbmQgbnVtYmVycyBjYW4gYWx3YXlzIGJlIGtleXMuIFR1cGxlcyBjYW4gYmUgdXNlZCBhcyBrZXlzIGlmIHRoZXkgY29udGFpbiBvbmx5IHN0cmluZ3MsIG51bWJlcnMsIG9yIHR1cGxlczsgaWYgYSB0dXBsZSBjb250YWlucyBhbnkgbXV0YWJsZSBvYmplY3QgZWl0aGVyIGRpcmVjdGx5IG9yIGluZGlyZWN0bHksIGl0IGNhbm5vdCBiZSB1c2VkIGFzIGEga2V5LiBZb3UgY2FuXFx1MjAxOXQgdXNlIGxpc3RzIGFzIGtleXMsIHNpbmNlIGxpc3RzIGNhbiBiZSBtb2RpZmllZCBpbiBwbGFjZSB1c2luZyBpbmRleCBhc3NpZ25tZW50cywgc2xpY2UgYXNzaWdubWVudHMsIG9yIG1ldGhvZHMgbGlrZSBgYXBwZW5kKClgIGFuZCBgZXh0ZW5kKClgLlwiXX1dIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJ0b29sIiwibmFtZSI6IndlYl9zZWFyY2giLCJpZCI6IjZlNTEyNWFjLTMxNjQtNGE3Ni05YzUyLWFjMzQ2MGNlMzZlNCIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsInN0YXR1cyI6InN1Y2Nlc3MifSx7ImNvbnRlbnQiOlt7ImlkIjoidG9vbHVfMDFYamozYVJkV3loQkpmUG1USkJ4UWE5IiwiY2FsbGVyIjp7InR5cGUiOiJkaXJlY3QifSwiaW5wdXQiOnt9LCJuYW1lIjoidHJhbnNmZXJfdG9fc2tlcHRpYyIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHdBNkY0Y0NONXloRDRDcnM5TSIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo3Mzc4LCJvdXRwdXRfdG9rZW5zIjozMywib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6MH0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoicmVzZWFyY2hlciIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy0zY2Q4LTc5ZjEtODhhMS1jZTZjZWUyOTRlNzktMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ0cmFuc2Zlcl90b19za2VwdGljIiwiYXJncyI6e30sImlkIjoidG9vbHVfMDFYamozYVJkV3loQkpmUG1USkJ4UWE5IiwidHlwZSI6InRvb2xfY2FsbCJ9XSwiaW52YWxpZF90b29sX2NhbGxzIjpbXSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjo3Mzc4LCJvdXRwdXRfdG9rZW5zIjozMywidG90YWxfdG9rZW5zIjo3NDExLCJpbnB1dF90b2tlbl9kZXRhaWxzIjp7ImNhY2hlX3JlYWQiOjAsImNhY2hlX2NyZWF0aW9uIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VuX2RldGFpbHMiOnsicmVhc29uaW5nIjowfX19LHsiY29udGVudCI6IlN1Y2Nlc3NmdWxseSB0cmFuc2ZlcnJlZCB0byBza2VwdGljIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJ0b29sIiwibmFtZSI6InRyYW5zZmVyX3RvX3NrZXB0aWMiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMVhqajNhUmRXeWhCSmZQbVRKQnhRYTkiLCJzdGF0dXMiOiJzdWNjZXNzIn1dLCJhY3RpdmVfYWdlbnQiOiJza2VwdGljIn0sInJlc3VtZSI6bnVsbCwiZ290byI6InNrZXB0aWMifX0=" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "9cc6e8991d091c19", + "parentSpanId": "08f0feca38b72d47", + "name": "model", + "kind": 1, + "startTimeUnixNano": "1790968020180871936", + "endTimeUnixNano": "1790968023765219072", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "3" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828|model:c92c732a-60d5-e72e-21dd-29893acbe69a" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "researcher" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "graph:step:3" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W3siY29udGVudCI6IkZvciBhIHN5bnRoZXRpYyB0cmFjaW5nIGZpeHR1cmUsIGV4cGxhaW4gUHl0aG9uIHR1cGxlIHZlcnN1cyBsaXN0IGltbXV0YWJpbGl0eSBpbiBhdCBtb3N0IDEyMCB3b3Jkcy4gVXNlIG9ubHkgb2ZmaWNpYWwgUHl0aG9uIGRvY3VtZW50YXRpb24gaWYgeW91IHNlYXJjaC4gSGF2ZSB0aGUgc2tlcHRpYyBhbmQgdmVyaWZpZXIgY2hlY2sgaXQsIHRoZW4gaGF2ZSB0aGUgZWRpdG9yIHdyaXRlIC9hbnN3ZXIubWQuIEtlZXAgdGhlIHJ1biBzaG9ydCBhbmQgZG8gbm90IGFzayBmb2xsb3ctdXAgcXVlc3Rpb25zLiIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoiaHVtYW4iLCJpZCI6IjQ2MjA0MzA1LWY4ZWUtNDM4NC05Y2U4LWZjZDgwYmZjZGY3NSJ9LHsiY29udGVudCI6W3siaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9LCJuYW1lIjoid2ViX3NlYXJjaCIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHc5YVc5YnJlblBxZ3liZk53UCIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjoyODQ1LCJvdXRwdXRfdG9rZW5zIjo3Mywib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6MH0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoicmVzZWFyY2hlciIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy0yMWEzLTcwMDAtYTM4MS0yODRhNmEwYzAzZjctMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ3ZWJfc2VhcmNoIiwiYXJncyI6eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9LCJpZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6Mjg0NSwib3V0cHV0X3Rva2VucyI6NzMsInRvdGFsX3Rva2VucyI6MjkxOCwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fSx7ImNvbnRlbnQiOiJbe1widGl0bGVcIjogXCI1LiBEYXRhIFN0cnVjdHVyZXMgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3R1dG9yaWFsL2RhdGFzdHJ1Y3R1cmVzLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcIllvdSBtaWdodCBoYXZlIG5vdGljZWQgdGhhdCBtZXRob2RzIGxpa2VgaW5zZXJ0YCxgcmVtb3ZlYCBvcmBzb3J0YCB0aGF0IG9ubHkgbW9kaWZ5IHRoZSBsaXN0IGhhdmUgbm8gcmV0dXJuIHZhbHVlIHByaW50ZWQgXFx1MjAxMyB0aGV5IHJldHVybiB0aGUgZGVmYXVsdGBOb25lYC4gWzFdIFRoaXMgaXMgYSBkZXNpZ24gcHJpbmNpcGxlIGZvciBhbGwgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgaW4gUHl0aG9uLlxcbi4uLlxcbiMjIDUuMy4gVHVwbGVzIGFuZCBTZXF1ZW5jZXNcXHUwMGI2XFxuXFxuV2Ugc2F3IHRoYXQgbGlzdHMgYW5kIHN0cmluZ3MgaGF2ZSBtYW55IGNvbW1vbiBwcm9wZXJ0aWVzLCBzdWNoIGFzIGluZGV4aW5nIGFuZCBzbGljaW5nIG9wZXJhdGlvbnMuIFRoZXkgYXJlIHR3byBleGFtcGxlcyBvZiBzZXF1ZW5jZSBkYXRhIHR5cGVzIChzZWUgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBsaXN0LCB0dXBsZSwgcmFuZ2UpLiBTaW5jZSBQeXRob24gaXMgYW4gZXZvbHZpbmcgbGFuZ3VhZ2UsIG90aGVyIHNlcXVlbmNlIGRhdGEgdHlwZXMgbWF5IGJlIGFkZGVkLiBUaGVyZSBpcyBhbHNvIGFub3RoZXIgc3RhbmRhcmQgc2VxdWVuY2UgZGF0YSB0eXBlOiB0aGUgdHVwbGUuXFxuXFxuQSB0dXBsZSBjb25zaXN0cyBvZiBhIG51bWJlciBvZiB2YWx1ZXMgc2VwYXJhdGVkIGJ5IGNvbW1hcywgZm9yIGluc3RhbmNlOlxcbi4uLlxcbj4+PiAjIFR1cGxlcyBhcmUgaW1tdXRhYmxlOlxcbj4+PiB0WzBdID0gODg4ODhcXG5UcmFjZWJhY2sgKG1vc3QgcmVjZW50IGNhbGwgbGFzdCk6XFxuICBGaWxlIFxcXCI8c3RkaW4+XFxcIiwgbGluZSAxLCBpbiA8bW9kdWxlPlxcblR5cGVFcnJvcjogJ3R1cGxlJyBvYmplY3QgZG9lcyBub3Qgc3VwcG9ydCBpdGVtIGFzc2lnbm1lbnRcXG4+Pj4gIyBidXQgdGhleSBjYW4gY29udGFpbiBtdXRhYmxlIG9iamVjdHM6XFxuPj4+IHYgPSAoWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuPj4+IHZcXG4oWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuLi4uXFxuQXMgeW91IHNlZSwgb24gb3V0cHV0IHR1cGxlcyBhcmUgYWx3YXlzIGVuY2xvc2VkIGluIHBhcmVudGhlc2VzLCBzbyB0aGF0IG5lc3RlZCB0dXBsZXMgYXJlIGludGVycHJldGVkIGNvcnJlY3RseTsgdGhleSBtYXkgYmUgaW5wdXQgd2l0aCBvciB3aXRob3V0IHN1cnJvdW5kaW5nIHBhcmVudGhlc2VzLCBhbHRob3VnaCBvZnRlbiBwYXJlbnRoZXNlcyBhcmUgbmVjZXNzYXJ5IGFueXdheSAoaWYgdGhlIHR1cGxlIGlzIHBhcnQgb2YgYSBsYXJnZXIgZXhwcmVzc2lvbikuIEl0IGlzIG5vdCBwb3NzaWJsZSB0byBhc3NpZ24gdG8gdGhlIGluZGl2aWR1YWwgaXRlbXMgb2YgYSB0dXBsZSwgaG93ZXZlciBpdCBpcyBwb3NzaWJsZSB0byBjcmVhdGUgdHVwbGVzIHdoaWNoIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzLCBzdWNoIGFzIGxpc3RzLlxcblxcblRob3VnaCB0dXBsZXMgbWF5IHNlZW0gc2ltaWxhciB0byBsaXN0cywgdGhleSBhcmUgb2Z0ZW4gdXNlZCBpbiBkaWZmZXJlbnQgc2l0dWF0aW9ucyBhbmQgZm9yIGRpZmZlcmVudCBwdXJwb3Nlcy4gVHVwbGVzIGFyZSBpbW11dGFibGUsIGFuZCB1c3VhbGx5IGNvbnRhaW4gYSBoZXRlcm9nZW5lb3VzIHNlcXVlbmNlIG9mIGVsZW1lbnRzIHRoYXQgYXJlIGFjY2Vzc2VkIHZpYSB1bnBhY2tpbmcgKHNlZSBsYXRlciBpbiB0aGlzIHNlY3Rpb24pIG9yIGluZGV4aW5nIChvciBldmVuIGJ5IGF0dHJpYnV0ZSBpbiB0aGUgY2FzZSBvZiBuYW1lZHR1cGxlcykuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG5Bbm90aGVyIHVzZWZ1bCBkYXRhIHR5cGUgYnVpbHQgaW50byBQeXRob24gaXMgdGhlIGRpY3Rpb25hcnkgKHNlZSBNYXBwaW5nIFR5cGVzIFxcdTIwMTQgZGljdCkuIERpY3Rpb25hcmllcyBhcmUgc29tZXRpbWVzIGZvdW5kIGluIG90aGVyIGxhbmd1YWdlcyBhcyBcXHUyMDFjYXNzb2NpYXRpdmUgbWVtb3JpZXNcXHUyMDFkIG9yIFxcdTIwMWNhc3NvY2lhdGl2ZSBhcnJheXNcXHUyMDFkLiBVbmxpa2Ugc2VxdWVuY2VzLCB3aGljaCBhcmUgaW5kZXhlZCBieSBhIHJhbmdlIG9mIG51bWJlcnMsIGRpY3Rpb25hcmllcyBhcmUgaW5kZXhlZCBieSBrZXlzLCB3aGljaCBjYW4gYmUgYW55IGltbXV0YWJsZSB0eXBlOyBzdHJpbmdzIGFuZCBudW1iZXJzIGNhbiBhbHdheXMgYmUga2V5cy4gVHVwbGVzIGNhbiBiZSB1c2VkIGFzIGtleXMgaWYgdGhleSBjb250YWluIG9ubHkgc3RyaW5ncywgbnVtYmVycywgb3IgdHVwbGVzOyBpZiBhIHR1cGxlIGNvbnRhaW5zIGFueSBtdXRhYmxlIG9iamVjdCBlaXRoZXIgZGlyZWN0bHkgb3IgaW5kaXJlY3RseSwgaXQgY2Fubm90IGJlIHVzZWQgYXMgYSBrZXkuIFlvdSBjYW5cXHUyMDE5dCB1c2UgbGlzdHMgYXMga2V5cywgc2luY2UgbGlzdHMgY2FuIGJlIG1vZGlmaWVkIGluIHBsYWNlIHVzaW5nIGluZGV4IGFzc2lnbm1lbnRzLCBzbGljZSBhc3NpZ25tZW50cywgb3IgbWV0aG9kcyBsaWtlIGFwcGVuZCgpIGFuZCBleHRlbmQoKS5cIl19LCB7XCJ0aXRsZVwiOiBcIjMuIERhdGEgbW9kZWwgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3JlZmVyZW5jZS9kYXRhbW9kZWwuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiVGhlIHZhbHVlIG9mIHNvbWUgb2JqZWN0cyBjYW4gY2hhbmdlLiBPYmplY3RzIHdob3NlIHZhbHVlIGNhbiBjaGFuZ2UgYXJlIHNhaWQgdG8gYmUgbXV0YWJsZTsgb2JqZWN0cyB3aG9zZSB2YWx1ZSBpcyB1bmNoYW5nZWFibGUgb25jZSB0aGV5IGFyZSBjcmVhdGVkIGFyZSBjYWxsZWQgaW1tdXRhYmxlLiAoVGhlIHZhbHVlIG9mIGFuIGltbXV0YWJsZSBjb250YWluZXIgb2JqZWN0IHRoYXQgY29udGFpbnMgYSByZWZlcmVuY2UgdG8gYSBtdXRhYmxlIG9iamVjdCBjYW4gY2hhbmdlIHdoZW4gdGhlIGxhdHRlclxcdTIwMTlzIHZhbHVlIGlzIGNoYW5nZWQ7IGhvd2V2ZXIgdGhlIGNvbnRhaW5lciBpcyBzdGlsbCBjb25zaWRlcmVkIGltbXV0YWJsZSwgYmVjYXVzZSB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGl0IGNvbnRhaW5zIGNhbm5vdCBiZSBjaGFuZ2VkLiBTbywgaW1tdXRhYmlsaXR5IGlzIG5vdCBzdHJpY3RseSB0aGUgc2FtZSBhcyBoYXZpbmcgYW4gdW5jaGFuZ2VhYmxlIHZhbHVlLCBpdCBpcyBtb3JlIHN1YnRsZS4pIEFuIG9iamVjdFxcdTIwMTlzIG11dGFiaWxpdHkgaXMgZGV0ZXJtaW5lZCBieSBpdHMgdHlwZTsgZm9yIGluc3RhbmNlLCBudW1iZXJzLCBzdHJpbmdzIGFuZCB0dXBsZXMgYXJlIGltbXV0YWJsZSwgd2hpbGUgZGljdGlvbmFyaWVzIGFuZCBsaXN0cyBhcmUgbXV0YWJsZS5cXG4uLi5cXG5Tb21lIG9iamVjdHMgY29udGFpbiByZWZlcmVuY2VzIHRvIG90aGVyIG9iamVjdHM7IHRoZXNlIGFyZSBjYWxsZWQgY29udGFpbmVycy4gRXhhbXBsZXMgb2YgY29udGFpbmVycyBhcmUgdHVwbGVzLCBsaXN0cyBhbmQgZGljdGlvbmFyaWVzLiBUaGUgcmVmZXJlbmNlcyBhcmUgcGFydCBvZiBhIGNvbnRhaW5lclxcdTIwMTlzIHZhbHVlLiBJbiBtb3N0IGNhc2VzLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIHZhbHVlIG9mIGEgY29udGFpbmVyLCB3ZSBpbXBseSB0aGUgdmFsdWVzLCBub3QgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGNvbnRhaW5lZCBvYmplY3RzOyBob3dldmVyLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIG11dGFiaWxpdHkgb2YgYSBjb250YWluZXIsIG9ubHkgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGltbWVkaWF0ZWx5IGNvbnRhaW5lZCBvYmplY3RzIGFyZSBpbXBsaWVkLiBTbywgaWYgYW4gaW1tdXRhYmxlIGNvbnRhaW5lciAobGlrZSBhIHR1cGxlKSBjb250YWlucyBhIHJlZmVyZW5jZSB0byBhIG11dGFibGUgb2JqZWN0LCBpdHMgdmFsdWUgY2hhbmdlcyBpZiB0aGF0IG11dGFibGUgb2JqZWN0IGlzIGNoYW5nZWQuXFxuLi4uXFxuIyMjIDMuMi41LiBTZXF1ZW5jZXNcXHUwMGI2XFxuLi4uXFxuIyMjIyAzLjIuNS4xLiBJbW11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbkFuIG9iamVjdCBvZiBhbiBpbW11dGFibGUgc2VxdWVuY2UgdHlwZSBjYW5ub3QgY2hhbmdlIG9uY2UgaXQgaXMgY3JlYXRlZC4gKElmIHRoZSBvYmplY3QgY29udGFpbnMgcmVmZXJlbmNlcyB0byBvdGhlciBvYmplY3RzLCB0aGVzZSBvdGhlciBvYmplY3RzIG1heSBiZSBtdXRhYmxlIGFuZCBtYXkgYmUgY2hhbmdlZDsgaG93ZXZlciwgdGhlIGNvbGxlY3Rpb24gb2Ygb2JqZWN0cyBkaXJlY3RseSByZWZlcmVuY2VkIGJ5IGFuIGltbXV0YWJsZSBvYmplY3QgY2Fubm90IGNoYW5nZS4pXFxuXFxuVGhlIGZvbGxvd2luZyB0eXBlcyBhcmUgaW1tdXRhYmxlIHNlcXVlbmNlczpcXG4uLi5cXG5UdXBsZXNcXG46IFRoZSBpdGVtcyBvZiBhIGB0dXBsZWAgYXJlIGFyYml0cmFyeSBQeXRob24gb2JqZWN0cy4gVHVwbGVzIG9mIHR3byBvciBtb3JlIGl0ZW1zIGFyZSBmb3JtZWQgYnkgY29tbWEtc2VwYXJhdGVkIGxpc3RzIG9mIGV4cHJlc3Npb25zLiBBIHR1cGxlIG9mIG9uZSBpdGVtIChhIFxcdTIwMThzaW5nbGV0b25cXHUyMDE5KSBjYW4gYmUgZm9ybWVkIGJ5IGFmZml4aW5nIGEgY29tbWEgdG8gYW4gZXhwcmVzc2lvbiAoYW4gZXhwcmVzc2lvbiBieSBpdHNlbGYgZG9lcyBub3QgY3JlYXRlIGEgdHVwbGUsIHNpbmNlIHBhcmVudGhlc2VzIG11c3QgYmUgdXNhYmxlIGZvciBncm91cGluZyBvZiBleHByZXNzaW9ucykuIEFuIGVtcHR5IHR1cGxlIGNhbiBiZSBmb3JtZWQgYnkgYW4gZW1wdHkgcGFpciBvZiBwYXJlbnRoZXNlcy5cXG4uLi5cXG4jIyMjIDMuMi41LjIuIE11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbk11dGFibGUgc2VxdWVuY2VzIGNhbiBiZSBjaGFuZ2VkIGFmdGVyIHRoZXkgYXJlIGNyZWF0ZWQuIFRoZSBzdWJzY3JpcHRpb24gYW5kIHNsaWNpbmcgbm90YXRpb25zIGNhbiBiZSB1c2VkIGFzIHRoZSB0YXJnZXQgb2YgYXNzaWdubWVudCBhbmQgYGRlbGAgKGRlbGV0ZSkgc3RhdGVtZW50cy5cXG4uLi5cXG5UaGVyZSBhcmUgY3VycmVudGx5IHR3byBpbnRyaW5zaWMgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlczpcXG5cXG5MaXN0c1xcbjogVGhlIGl0ZW1zIG9mIGEgbGlzdCBhcmUgYXJiaXRyYXJ5IFB5dGhvbiBvYmplY3RzLiBMaXN0cyBhcmUgZm9ybWVkIGJ5IHBsYWNpbmcgYSBjb21tYS1zZXBhcmF0ZWQgbGlzdCBvZiBleHByZXNzaW9ucyBpbiBzcXVhcmUgYnJhY2tldHMuIChOb3RlIHRoYXQgdGhlcmUgYXJlIG5vIHNwZWNpYWwgY2FzZXMgbmVlZGVkIHRvIGZvcm0gbGlzdHMgb2YgbGVuZ3RoIDAgb3IgMS4pXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjcgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvYnVpbHRpbnMvc3RkdHlwZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiY29sbGVjdGlvbiBjbGFzc2VzIGFyZSBtdXRhYmxlLiBUaGUgbWV0aG9kcyB0aGF0IGFkZCwgc3VidHJhY3QsIG9yIHJlYXJyYW5nZSB0aGVpciAuLi4gaW4gcGxhY2UsIGFuZCBkb25cXHUyMDE5dCByZXR1cm4gYSAuLi4gLCBuZXZlciByZXR1cm4gdGhlIGNvbGxlY3Rpb24gaW5zdGFuY2UgaXRzZWxmIGJ1dCBgTm9uZWAuXFxuLi4uXFxuIyMgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBgbGlzdGAsIGB0dXBsZWAsIGByYW5nZWBcXHUwMGI2XFxuXFxuVGhlcmUgYXJlIHRocmVlIGJhc2ljIHNlcXVlbmNlIHR5cGVzOiBsaXN0cywgdHVwbGVzLCBhbmQgcmFuZ2Ugb2JqZWN0cy4gQWRkaXRpb25hbCBzZXF1ZW5jZSB0eXBlcyB0YWlsb3JlZCBmb3IgcHJvY2Vzc2luZyBvZiBiaW5hcnkgZGF0YSBhbmQgdGV4dCBzdHJpbmdzIGFyZSBkZXNjcmliZWQgaW4gZGVkaWNhdGVkIHNlY3Rpb25zLlxcbi4uLlxcblRoZSBvcGVyYXRpb25zIGluIHRoZSBmb2xsb3dpbmcgdGFibGUgLi4uIG11dGFibGUgYW5kIGltbXV0YWJsZS4gVGhlIGBjb2xsZWN0aW9ucy4gLi4uIGlzIHByb3ZpZGVkIHRvIG1ha2UgLi4uIGVhc2llciB0byBjb3JyZWN0bHkgaW1wbGVtZW50IHRoZXNlIG9wZXJhdGlvbnMgb24gY3VzdG9tIHNlcXVlbmNlIHR5cGVzXFxuLi4uXFxuIyMjIEltbXV0YWJsZSBTZXF1ZW5jZSBUeXBlc1xcdTAwYjZcXG4uLi5cXG5tdXRhYmxlIHNlcXVlbmNlIHR5cGVzIGlzXFxuLi4uXFxuc3VwcG9ydCBhbGxvd3MgaW1tdXRhYmxlIHNlcXVlbmNlcywgLi4uICwgdG8gYmUgdXNlZCBhcyBgZGljdGAga2V5cyBhbmQgc3RvcmVkIGluIC4uLiBlbnNldGAgaW5zdGFuY2VzXFxuLi4uXFxuIyMjIE11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxuIyMjIExpc3RzXFx1MDBiNlxcblxcbkxpc3RzIGFyZSBtdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaG9tb2dlbmVvdXMgaXRlbXMgKHdoZXJlIHRoZSBwcmVjaXNlIGRlZ3JlZSBvZiBzaW1pbGFyaXR5IHdpbGwgdmFyeSBieSBhcHBsaWNhdGlvbikuXFxuLi4uXFxuIyMjIFR1cGxlc1xcdTAwYjZcXG5cXG5UdXBsZXMgYXJlIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhldGVyb2dlbmVvdXMgZGF0YSAoc3VjaCBhcyB0aGUgMi10dXBsZXMgcHJvZHVjZWQgYnkgdGhlIGBlbnVtZXJhdGUoKWAgYnVpbHQtaW4pLiBUdXBsZXMgYXJlIGFsc28gdXNlZCBmb3IgY2FzZXMgd2hlcmUgYW4gaW1tdXRhYmxlIHNlcXVlbmNlIG9mIGhvbW9nZW5lb3VzIGRhdGEgaXMgbmVlZGVkIChzdWNoIGFzIGFsbG93aW5nIHN0b3JhZ2UgaW4gYSBgc2V0YCBvciBgZGljdGAgaW5zdGFuY2UpLlxcbi4uLlxcbnR1cGxlKGl0ZXJhYmxlPVxcbi4uLlxcblRoZSBjb25zdHJ1Y3RvciBidWlsZHMgYSB0dXBsZSB3aG9zZSBpdGVtcyBhcmUgdGhlIHNhbWUgYW5kIGluIHRoZSBzYW1lIG9yZGVyIGFzIGl0ZXJhYmxlXFx1MjAxOXMgaXRlbXMuIGl0ZXJhYmxlIG1heSBiZSBlaXRoZXIgYSBzZXF1ZW5jZSwgYSBjb250YWluZXIgdGhhdCBzdXBwb3J0cyBpdGVyYXRpb24sIG9yIGFuIGl0ZXJhdG9yIG9iamVjdC4gSWYgaXRlcmFibGUgaXMgYWxyZWFkeSBhIHR1cGxlLCBpdCBpcyByZXR1cm5lZCB1bmNoYW5nZWQuIEZvciBleGFtcGxlLCBgdHVwbGUoJ2FiYycpYCByZXR1cm5zIGAoJ2EnLCAnYicsICdjJylgIGFuZCBgdHVwbGUoIFsxLCAyLCAzXSApYCByZXR1cm5zIGAoMSwgMiwgMylgLiBJZiBubyBhcmd1bWVudCBpcyBnaXZlbiwgdGhlIGNvbnN0cnVjdG9yIGNyZWF0ZXMgYSBuZXcgZW1wdHkgdHVwbGUsIGAoKWAuXFxuLi4uXFxuVHVwbGVzIGltcGxlbWVudCBhbGwgb2YgdGhlIGNvbW1vbiBzZXF1ZW5jZSBvcGVyYXRpb25zLlxcbi4uLlxcbkZvciBoZXRlcm9nZW5lb3VzIGNvbGxlY3Rpb25zIG9mIGRhdGEgd2hlcmUgYWNjZXNzIGJ5IG5hbWUgaXMgY2xlYXJlciB0aGFuIGFjY2VzcyBieSBpbmRleCwgYGNvbGxlY3Rpb25zLm5hbWVkdHVwbGUoKWAgbWF5IGJlIGEgbW9yZSBhcHByb3ByaWF0ZSBjaG9pY2UgdGhhbiBhIHNpbXBsZSB0dXBsZSBvYmplY3QuXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvbGlicmFyeS9zdGR0eXBlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJTb21lIGNvbGxlY3Rpb24gY2xhc3NlcyBhcmUgbXV0YWJsZS4gVGhlIG1ldGhvZHMgdGhhdCBhZGQsIHN1YnRyYWN0LCBvciByZWFycmFuZ2UgdGhlaXIgLi4uIGluIHBsYWNlLCBhbmQgZG9uXFx1MjAxOXQgcmV0dXJuIGEgLi4uICwgbmV2ZXIgcmV0dXJuIHRoZSBjb2xsZWN0aW9uIGluc3RhbmNlIGl0c2VsZiBidXQgYE5vbmVgLlxcbi4uLlxcbiMjIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgYGxpc3RgLCBgdHVwbGVgLCBgcmFuZ2VgXFx1MDBiNlxcblxcblRoZXJlIGFyZSB0aHJlZSBiYXNpYyBzZXF1ZW5jZSB0eXBlczogbGlzdHMsIHR1cGxlcywgYW5kIC4uLiBvYmplY3RzLiBBZGRpdGlvbmFsIHNlcXVlbmNlIHR5cGVzIHRhaWxvcmVkIGZvciBwcm9jZXNzaW5nIG9mIGJpbmFyeSBkYXRhIGFuZCB0ZXh0IHN0cmluZ3MgYXJlIGRlc2NyaWJlZCBpbiBkZWRpY2F0ZWQgc2VjdGlvbnMuXFxuLi4uXFxuVGhlIG9wZXJhdGlvbnMgaW4gdGhlIGZvbGxvd2luZyB0YWJsZSAuLi4gaW1tdXRhYmxlLiBUaGUgYCAuLi4gaXMgcHJvdmlkZWQgdG8gbWFrZSAuLi4gZWFzaWVyIHRvIGNvcnJlY3RseSBpbXBsZW1lbnQgdGhlc2Ugb3BlcmF0aW9ucyBvbiBjdXN0b20gc2VxdWVuY2UgdHlwZXNcXG4uLi5cXG4jIyMgSW1tdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbm9wZXJhdGlvbiB0aGF0IGltbXV0YWJsZSBzZXF1ZW5jZSAuLi4gYnkgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlcyBpcyAuLi4gYGhhc2goKWBcXG4uLi5cXG5UaGlzIHN1cHBvcnQgYWxsb3dzIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHN1Y2ggYXMgYHR1cGxlYCBpbnN0YW5jZXMsIHRvIGJlIHVzZWQgYXMgYGRpY3RgIGtleXMgYW5kIHN0b3JlZCBpbiBgc2V0YCBhbmQgYGZyb3plbnNldGAgaW5zdGFuY2VzLlxcbi4uLlxcbiMjIyBNdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbiMjIyBMaXN0c1xcdTAwYjZcXG5cXG5MaXN0cyBhcmUgbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhvbW9nZW5lb3VzIGl0ZW1zICh3aGVyZSB0aGUgcHJlY2lzZSBkZWdyZWUgb2Ygc2ltaWxhcml0eSB3aWxsIHZhcnkgYnkgYXBwbGljYXRpb24pLlxcbi4uLlxcbiMjIyBUdXBsZXNcXHUwMGI2XFxuXFxuVHVwbGVzIGFyZSBpbW11dGFibGUgc2VxdWVuY2VzLCB0eXBpY2FsbHkgdXNlZCB0byBzdG9yZSBjb2xsZWN0aW9ucyBvZiBoZXRlcm9nZW5lb3VzIGRhdGEgKHN1Y2ggYXMgdGhlIDItdHVwbGVzIHByb2R1Y2VkIGJ5IHRoZSBgZW51bWVyYXRlKClgIGJ1aWx0LWluKS4gVHVwbGVzIGFyZSBhbHNvIHVzZWQgZm9yIGNhc2VzIHdoZXJlIGFuIGltbXV0YWJsZSBzZXF1ZW5jZSBvZiBob21vZ2VuZW91cyBkYXRhIGlzIG5lZWRlZCAoc3VjaCBhcyBhbGxvd2luZyBzdG9yYWdlIGluIGEgYHNldGAgb3IgYGRpY3RgIGluc3RhbmNlKS5cXG4uLi5cXG5UaGUgY29uc3RydWN0b3IgYnVpbGRzIGEgdHVwbGUgd2hvc2UgaXRlbXMgYXJlIHRoZSBzYW1lIGFuZCBpbiB0aGUgc2FtZSBvcmRlciBhcyBpdGVyYWJsZVxcdTIwMTlzIGl0ZW1zLiBpdGVyYWJsZSBtYXkgYmUgZWl0aGVyIGEgc2VxdWVuY2UsIGEgY29udGFpbmVyIHRoYXQgc3VwcG9ydHMgaXRlcmF0aW9uLCBvciBhbiBpdGVyYXRvciBvYmplY3QuIElmIGl0ZXJhYmxlIGlzIGFscmVhZHkgYSB0dXBsZSwgaXQgaXMgcmV0dXJuZWQgdW5jaGFuZ2VkLiBGb3IgZXhhbXBsZSwgYHR1cGxlKCdhYmMnKWAgcmV0dXJucyBgKCdhJywgJ2InLCAnYycpYCBhbmQgYHR1cGxlKCBbMSwgMiwgM10gKWAgcmV0dXJucyBgKDEsIDIsIDMpYC4gSWYgbm8gYXJndW1lbnQgaXMgZ2l2ZW4sIHRoZSBjb25zdHJ1Y3RvciBjcmVhdGVzIGEgbmV3IGVtcHR5IHR1cGxlLCBgKClgLlxcbi4uLlxcblR1cGxlcyBpbXBsZW1lbnQgYWxsIG9mIHRoZSBjb21tb24gc2VxdWVuY2Ugb3BlcmF0aW9ucy5cXG4uLi5cXG5Gb3IgaGV0ZXJvZ2VuZW91cyBjb2xsZWN0aW9ucyBvZiBkYXRhIHdoZXJlIGFjY2VzcyBieSBuYW1lIGlzIGNsZWFyZXIgdGhhbiBhY2Nlc3MgYnkgaW5kZXgsIGBjb2xsZWN0aW9ucy5uYW1lZHR1cGxlKClgIG1heSBiZSBhIG1vcmUgYXBwcm9wcmlhdGUgY2hvaWNlIHRoYW4gYSBzaW1wbGUgdHVwbGUgb2JqZWN0LlwiXX0sIHtcInRpdGxlXCI6IFwiNS4gRGF0YSBTdHJ1Y3R1cmVzIFxcdTIwMTQgUHl0aG9uIDMuMTAuMjAgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMuMTAvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiWW91IG1pZ2h0IGhhdmUgbm90aWNlZCB0aGF0IG1ldGhvZHMgbGlrZSBgaW5zZXJ0YCwgYHJlbW92ZWAgb3IgYHNvcnRgIHRoYXQgb25seSBtb2RpZnkgdGhlIGxpc3QgaGF2ZSBubyByZXR1cm4gdmFsdWUgcHJpbnRlZCBcXHUyMDEzIHRoZXkgcmV0dXJuIHRoZSBkZWZhdWx0IGBOb25lYC4gMSBUaGlzIGlzIGEgZGVzaWduIHByaW5jaXBsZSBmb3IgYWxsIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIGluIFB5dGhvbi5cXG4uLi5cXG4jIyA1LjMuIFR1cGxlcyBhbmQgU2VxdWVuY2VzXFx1MDBiNlxcblxcbldlIHNhdyB0aGF0IGxpc3RzIGFuZCBzdHJpbmdzIGhhdmUgbWFueSBjb21tb24gcHJvcGVydGllcywgc3VjaCBhcyBpbmRleGluZyBhbmQgc2xpY2luZyBvcGVyYXRpb25zLiBUaGV5IGFyZSB0d28gZXhhbXBsZXMgb2Ygc2VxdWVuY2UgZGF0YSB0eXBlcyAoc2VlIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgbGlzdCwgdHVwbGUsIHJhbmdlKS4gU2luY2UgUHl0aG9uIGlzIGFuIGV2b2x2aW5nIGxhbmd1YWdlLCBvdGhlciBzZXF1ZW5jZSBkYXRhIHR5cGVzIG1heSBiZSBhZGRlZC4gVGhlcmUgaXMgYWxzbyBhbm90aGVyIHN0YW5kYXJkIHNlcXVlbmNlIGRhdGEgdHlwZTogdGhlIHR1cGxlLlxcblxcbkEgdHVwbGUgY29uc2lzdHMgb2YgYSBudW1iZXIgb2YgdmFsdWVzIHNlcGFyYXRlZCBieSBjb21tYXMsIGZvciBpbnN0YW5jZTpcXG4uLi5cXG4+Pj4gIyBUdXBsZXMgYXJlIGltbXV0YWJsZTpcXG4uLi4gdFswXSA9IDg4ODg4XFxuVHJhY2ViYWNrIChtb3N0IHJlY2VudCBjYWxsIGxhc3QpOlxcbiAgRmlsZSBcXFwiPHN0ZGluPlxcXCIsIGxpbmUgMSwgaW4gPG1vZHVsZT5cXG5UeXBlRXJyb3I6ICd0dXBsZScgb2JqZWN0IGRvZXMgbm90IHN1cHBvcnQgaXRlbSBhc3NpZ25tZW50XFxuPj4+ICMgYnV0IHRoZXkgY2FuIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzOlxcbi4uLiB2ID0gKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbj4+PiB2XFxuKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbi4uLlxcbkFzIHlvdSBzZWUsIG9uIG91dHB1dCB0dXBsZXMgYXJlIGFsd2F5cyBlbmNsb3NlZCBpbiBwYXJlbnRoZXNlcywgc28gdGhhdCBuZXN0ZWQgdHVwbGVzIGFyZSBpbnRlcnByZXRlZCBjb3JyZWN0bHk7IHRoZXkgbWF5IGJlIGlucHV0IHdpdGggb3Igd2l0aG91dCBzdXJyb3VuZGluZyBwYXJlbnRoZXNlcywgYWx0aG91Z2ggb2Z0ZW4gcGFyZW50aGVzZXMgYXJlIG5lY2Vzc2FyeSBhbnl3YXkgKGlmIHRoZSB0dXBsZSBpcyBwYXJ0IG9mIGEgbGFyZ2VyIGV4cHJlc3Npb24pLiBJdCBpcyBub3QgcG9zc2libGUgdG8gYXNzaWduIHRvIHRoZSBpbmRpdmlkdWFsIGl0ZW1zIG9mIGEgdHVwbGUsIGhvd2V2ZXIgaXQgaXMgcG9zc2libGUgdG8gY3JlYXRlIHR1cGxlcyB3aGljaCBjb250YWluIG11dGFibGUgb2JqZWN0cywgc3VjaCBhcyBsaXN0cy5cXG5cXG5UaG91Z2ggdHVwbGVzIG1heSBzZWVtIHNpbWlsYXIgdG8gbGlzdHMsIHRoZXkgYXJlIG9mdGVuIHVzZWQgaW4gZGlmZmVyZW50IHNpdHVhdGlvbnMgYW5kIGZvciBkaWZmZXJlbnQgcHVycG9zZXMuIFR1cGxlcyBhcmUgaW1tdXRhYmxlLCBhbmQgdXN1YWxseSBjb250YWluIGEgaGV0ZXJvZ2VuZW91cyBzZXF1ZW5jZSBvZiBlbGVtZW50cyB0aGF0IGFyZSBhY2Nlc3NlZCB2aWEgdW5wYWNraW5nIChzZWUgbGF0ZXIgaW4gdGhpcyBzZWN0aW9uKSBvciBpbmRleGluZyAob3IgZXZlbiBieSBhdHRyaWJ1dGUgaW4gdGhlIGNhc2Ugb2YgYG5hbWVkdHVwbGVzYCkuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG4jIyA1LjQuXFxuLi4uXFxuLiBTZXQgb2JqZWN0c1xcbi4uLlxcbiMjIDUuNS4gRGljdGlvbmFyaWVzXFx1MDBiNlxcblxcbkFub3RoZXIgdXNlZnVsIGRhdGEgdHlwZSBidWlsdCBpbnRvIFB5dGhvbiBpcyB0aGUgZGljdGlvbmFyeSAoc2VlIE1hcHBpbmcgVHlwZXMgXFx1MjAxNCBkaWN0KS4gRGljdGlvbmFyaWVzIGFyZSBzb21ldGltZXMgZm91bmQgaW4gb3RoZXIgbGFuZ3VhZ2VzIGFzIFxcdTIwMWNhc3NvY2lhdGl2ZSBtZW1vcmllc1xcdTIwMWQgb3IgXFx1MjAxY2Fzc29jaWF0aXZlIGFycmF5c1xcdTIwMWQuIFVubGlrZSBzZXF1ZW5jZXMsIHdoaWNoIGFyZSBpbmRleGVkIGJ5IGEgcmFuZ2Ugb2YgbnVtYmVycywgZGljdGlvbmFyaWVzIGFyZSBpbmRleGVkIGJ5IGtleXMsIHdoaWNoIGNhbiBiZSBhbnkgaW1tdXRhYmxlIHR5cGU7IHN0cmluZ3MgYW5kIG51bWJlcnMgY2FuIGFsd2F5cyBiZSBrZXlzLiBUdXBsZXMgY2FuIGJlIHVzZWQgYXMga2V5cyBpZiB0aGV5IGNvbnRhaW4gb25seSBzdHJpbmdzLCBudW1iZXJzLCBvciB0dXBsZXM7IGlmIGEgdHVwbGUgY29udGFpbnMgYW55IG11dGFibGUgb2JqZWN0IGVpdGhlciBkaXJlY3RseSBvciBpbmRpcmVjdGx5LCBpdCBjYW5ub3QgYmUgdXNlZCBhcyBhIGtleS4gWW91IGNhblxcdTIwMTl0IHVzZSBsaXN0cyBhcyBrZXlzLCBzaW5jZSBsaXN0cyBjYW4gYmUgbW9kaWZpZWQgaW4gcGxhY2UgdXNpbmcgaW5kZXggYXNzaWdubWVudHMsIHNsaWNlIGFzc2lnbm1lbnRzLCBvciBtZXRob2RzIGxpa2UgYGFwcGVuZCgpYCBhbmQgYGV4dGVuZCgpYC5cIl19XSIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ3ZWJfc2VhcmNoIiwiaWQiOiI2ZTUxMjVhYy0zMTY0LTRhNzYtOWM1Mi1hYzM0NjBjZTM2ZTQiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJzdGF0dXMiOiJzdWNjZXNzIn1dfQ==" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJvdXRwdXQiOlt7ImdyYXBoIjpudWxsLCJ1cGRhdGUiOnsibWVzc2FnZXMiOlt7ImNvbnRlbnQiOlt7ImlkIjoidG9vbHVfMDFYamozYVJkV3loQkpmUG1USkJ4UWE5IiwiY2FsbGVyIjp7InR5cGUiOiJkaXJlY3QifSwiaW5wdXQiOnt9LCJuYW1lIjoidHJhbnNmZXJfdG9fc2tlcHRpYyIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHdBNkY0Y0NONXloRDRDcnM5TSIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo3Mzc4LCJvdXRwdXRfdG9rZW5zIjozMywib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6MH0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoicmVzZWFyY2hlciIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy0zY2Q4LTc5ZjEtODhhMS1jZTZjZWUyOTRlNzktMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ0cmFuc2Zlcl90b19za2VwdGljIiwiYXJncyI6e30sImlkIjoidG9vbHVfMDFYamozYVJkV3loQkpmUG1USkJ4UWE5IiwidHlwZSI6InRvb2xfY2FsbCJ9XSwiaW52YWxpZF90b29sX2NhbGxzIjpbXSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjo3Mzc4LCJvdXRwdXRfdG9rZW5zIjozMywidG90YWxfdG9rZW5zIjo3NDExLCJpbnB1dF90b2tlbl9kZXRhaWxzIjp7ImNhY2hlX3JlYWQiOjAsImNhY2hlX2NyZWF0aW9uIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VuX2RldGFpbHMiOnsicmVhc29uaW5nIjowfX19XX0sInJlc3VtZSI6bnVsbCwiZ290byI6W119XX0=" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "95614a9848c5f7d5", + "parentSpanId": "9cc6e8991d091c19", + "name": "FilesystemMiddleware.wrap_model_call", + "kind": 1, + "startTimeUnixNano": "1790968020181624832", + "endTimeUnixNano": "1790968023765062144", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "e30=" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "FilesystemMiddleware.wrap_model_call" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "3" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828|model:c92c732a-60d5-e72e-21dd-29893acbe69a" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "researcher" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828" + } + }, + { + "key": "langsmith.metadata.ls_method", + "value": { + "stringValue": "traceable" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJvdXRwdXQiOnsicmVzdWx0IjpbeyJjb250ZW50IjpbeyJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3NrZXB0aWMiLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QTZGNGNDTjV5aEQ0Q3JzOU0iLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjB9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InJlc2VhcmNoZXIiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtM2NkOC03OWYxLTg4YTEtY2U2Y2VlMjk0ZTc5LTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoidHJhbnNmZXJfdG9fc2tlcHRpYyIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsInRvdGFsX3Rva2VucyI6NzQxMSwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fV0sInN0cnVjdHVyZWRfcmVzcG9uc2UiOm51bGx9fQ==" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "25a7cd951117a4e0", + "parentSpanId": "95614a9848c5f7d5", + "name": "UnsupportedContentMiddleware.wrap_model_call", + "kind": 1, + "startTimeUnixNano": "1790968020182045952", + "endTimeUnixNano": "1790968023764993024", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "e30=" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "UnsupportedContentMiddleware.wrap_model_call" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "3" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828|model:c92c732a-60d5-e72e-21dd-29893acbe69a" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "researcher" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828" + } + }, + { + "key": "langsmith.metadata.ls_method", + "value": { + "stringValue": "traceable" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJvdXRwdXQiOnsicmVzdWx0IjpbeyJjb250ZW50IjpbeyJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3NrZXB0aWMiLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QTZGNGNDTjV5aEQ0Q3JzOU0iLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjB9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InJlc2VhcmNoZXIiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtM2NkOC03OWYxLTg4YTEtY2U2Y2VlMjk0ZTc5LTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoidHJhbnNmZXJfdG9fc2tlcHRpYyIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsInRvdGFsX3Rva2VucyI6NzQxMSwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fV0sInN0cnVjdHVyZWRfcmVzcG9uc2UiOm51bGx9fQ==" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "0d31577e2d5562cc", + "parentSpanId": "25a7cd951117a4e0", + "name": "ChatAnthropic", + "kind": 1, + "startTimeUnixNano": "1790968020184427008", + "endTimeUnixNano": "1790968023764837120", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chat" + } + }, + { + "key": "gen_ai.serialized.name", + "value": { + "stringValue": "ChatAnthropic" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "llm" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "ChatAnthropic" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "anthropic" + } + }, + { + "key": "gen_ai.request.model", + "value": { + "stringValue": "claude-sonnet" + } + }, + { + "key": "gen_ai.tool.definitions", + "value": { + "stringValue": "[{\"name\":\"ls\",\"input_schema\":{\"properties\":{\"path\":{\"description\":\"Absolute path to the directory to list. Must be absolute, not relative.\",\"type\":\"string\"}},\"required\":[\"path\"],\"type\":\"object\"},\"description\":\"Lists all files in a directory.\\n\\nThis is useful for exploring the filesystem and finding the right file to read or edit.\\nYou should almost ALWAYS use this tool before using the read_file or edit_file tools.\"},{\"name\":\"read_file\",\"input_schema\":{\"properties\":{\"file_path\":{\"description\":\"Absolute path to the file to read. Must be absolute, not relative.\",\"type\":\"string\"},\"offset\":{\"default\":0,\"description\":\"Line number to start reading from (0-indexed). Use for pagination of large files.\",\"type\":\"integer\"},\"limit\":{\"default\":100,\"description\":\"Maximum number of lines to read. Use for pagination of large files.\",\"type\":\"integer\"}},\"required\":[\"file_path\"],\"type\":\"object\"},\"description\":\"Reads a file from the filesystem. Assume any path the user provides is valid; reading a missing file returns an error.\\n\\nUsage:\\n- By default, it reads up to 100 lines starting from the beginning of the file. Use `offset`/`limit` to page through large files instead of reading them whole.\\n- A status header, `@@ field | field | ... @@`, sits above the file content, and every line after it is verbatim file content. When content is truncated, there may be an explanation before the header. Never include the header when editing.\\n- Speculatively batch multiple `read_file` calls in one response when several files may be useful.\\n- An empty file returns a system-reminder warning in place of contents.\\n- Large tool results may be offloaded to a file; the tool message gives the path. Read that path here, paging with `offset`/`limit`.\\n- Images (`.png`, `.jpg`, etc.), audio, video, and PDFs return multimodal content blocks (https://docs.langchain.com/oss/python/langchain/messages#multimodal).\\n- For images and PDFs, pagination via `offset`/`limit` is text-only - supply `file_path` only\\n- Always read a file before editing it.\"},{\"name\":\"glob\",\"input_schema\":{\"properties\":{\"pattern\":{\"description\":\"Glob pattern to match files (e.g., '*.py', '**/*.py', '/subdir/**/*.md'). A pattern without '/' matches the file name at any depth; a pattern containing '/' matches the search-root-relative path; a leading '/' anchors to the search root ('/*.py' matches only top-level files). Leading-dot names are excluded unless the pattern segment starts with '.', so prefer the bare form '*.py' over '**/*.py' -- '**' will not descend into dot-directories like '.github'.\",\"type\":\"string\"},\"path\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"null\"}],\"default\":null,\"description\":\"Base directory to search from. Defaults to the backend's default root.\"}},\"required\":[\"pattern\"],\"type\":\"object\"},\"description\":\"Find files matching a glob pattern, returning absolute paths.\\n\\nSupports `*` (any characters within a path segment), `**` (any directories), `?` (single character), `[abc]` (one character from a set), and `{a,b}` (alternatives), e.g. `*.py`, `src/**/*.py`, `*.{yml,yaml}`.\\n\\nA pattern without `/` matches the file name at any depth under the search root (`*.py` matches `src/app/main.py`). A pattern containing `/` matches the search-root-relative path (`src/**/*.py`). A leading `/` anchors to the search root (`/*.py` matches only top-level Python files).\\n\\nLeading-dot names are only matched when the pattern segment itself starts with `.` (use `.env`, or `.github/**/*.yml`). Because `**` will not descend into dot-directories, the bare form `*.yml` is *broader* than `**/*.yml` and is usually what you want.\"},{\"name\":\"grep\",\"input_schema\":{\"properties\":{\"pattern\":{\"description\":\"Text pattern to search for (literal string, not regex).\",\"type\":\"string\"},\"path\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"null\"}],\"default\":null,\"description\":\"Directory to search in. Defaults to current working directory.\"},\"glob\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"null\"}],\"default\":null,\"description\":\"Glob pattern (NOT regex) limiting which files are searched (e.g. '*.py', '*.ts'). A pattern without '/' matches the file name at any depth; a pattern containing '/' matches the search-root-relative path (e.g. 'src/**/*.py'). This is an in-tool file filter, not a call to the separate glob tool. Brace expansion (e.g. '*.{ts,tsx}') is not supported on all backends; run a separate search per extension for reliable results.\"},\"output_mode\":{\"default\":\"files_with_matches\",\"description\":\"Shape of the returned text. 'files_with_matches' (default): newline-separated matching file paths. 'content': matching lines grouped by file under a ':' header, each line indented and formatted ': ' (only the matched line, no surrounding context). 'count': one ': ' line per file.\",\"enum\":[\"files_with_matches\",\"content\",\"count\"],\"type\":\"string\"},\"max_count\":{\"anyOf\":[{\"exclusiveMinimum\":0,\"type\":\"integer\"},{\"type\":\"null\"}],\"default\":null,\"description\":\"Optional cap on the total number of matches returned across all files. Leave unset to use the configured default. When the cap is hit, results are truncated and a note says so; narrow the pattern or path to see the rest.\"}},\"required\":[\"pattern\"],\"type\":\"object\"},\"description\":\"Search for a LITERAL text pattern across files (NOT regex).\\n\\nThe pattern is matched verbatim: regex metacharacters are ordinary characters, not operators. To match any of several strings, run a separate grep for each; `grep(pattern=\\\"foo|bar\\\")` searches for the literal text \\\"foo|bar\\\", and `.*` or `\\\\.` match those characters literally.\\n\\nReturns matching files or content per `output_mode`. Offloaded large tool results live under the artifacts root (`/large_tool_results/` by default); grep that directory to search them when you do not know the exact path.\"},{\"name\":\"web_search\",\"input_schema\":{\"properties\":{\"query\":{\"type\":\"string\"}},\"required\":[\"query\"],\"type\":\"object\"},\"description\":\"Search the web with Exa for current facts and return excerpts with source URLs\"},{\"name\":\"transfer_to_skeptic\",\"input_schema\":{\"properties\":{},\"type\":\"object\"},\"description\":\"Send findings to the skeptic for critique\"},{\"name\":\"transfer_to_verifier\",\"input_schema\":{\"properties\":{},\"type\":\"object\"},\"description\":\"Send revised findings for source verification\"}]" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_chat_model" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "3" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828|model:c92c732a-60d5-e72e-21dd-29893acbe69a" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "researcher" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828" + } + }, + { + "key": "langsmith.metadata.ls_provider", + "value": { + "stringValue": "anthropic" + } + }, + { + "key": "langsmith.metadata.ls_model_name", + "value": { + "stringValue": "claude-sonnet" + } + }, + { + "key": "langsmith.metadata.ls_model_type", + "value": { + "stringValue": "chat" + } + }, + { + "key": "langsmith.metadata.ls_max_tokens", + "value": { + "intValue": "4096" + } + }, + { + "key": "langsmith.metadata.lc_versions", + "value": { + "stringValue": "{\"langchain-core\":\"1.6.6\",\"langchain\":\"1.4.3\",\"langchain-anthropic\":\"1.7.5\"}" + } + }, + { + "key": "langsmith.metadata.ls_method", + "value": { + "stringValue": "traceable" + } + }, + { + "key": "langsmith.metadata.model", + "value": { + "stringValue": "claude-sonnet" + } + }, + { + "key": "langsmith.metadata.max_tokens", + "value": { + "intValue": "4096" + } + }, + { + "key": "langsmith.metadata.model_kwargs", + "value": { + "stringValue": "{\"extra_body\":{\"extra_headers\":{\"anthropic-workspace-id\":\"[redacted workspace]\"}}}" + } + }, + { + "key": "langsmith.metadata.streaming", + "value": { + "boolValue": false + } + }, + { + "key": "langsmith.metadata.max_retries", + "value": { + "intValue": "2" + } + }, + { + "key": "langsmith.metadata._type", + "value": { + "stringValue": "anthropic-chat" + } + }, + { + "key": "langsmith.metadata.usage_metadata", + "value": { + "stringValue": "{\"input_tokens\":7378,\"output_tokens\":33,\"total_tokens\":7411,\"input_token_details\":{\"cache_read\":0,\"cache_creation\":0,\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_token_details\":{\"reasoning\":0}}" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "seq:step:1" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W1t7ImxjIjoxLCJ0eXBlIjoiY29uc3RydWN0b3IiLCJpZCI6WyJsYW5nY2hhaW4iLCJzY2hlbWEiLCJtZXNzYWdlcyIsIlN5c3RlbU1lc3NhZ2UiXSwia3dhcmdzIjp7ImNvbnRlbnQiOiJZb3UgYXJlIHRoZSByZXNlYXJjaGVyLiBTZWFyY2ggZm9yIGV2aWRlbmNlIGFuZCBzaGFyZSBzb3VyY2UgVVJMcy4gWW91IG1heSByZWFkIHNoYXJlZCB2aXJ0dWFsIGZpbGVzLCBidXQgb25seSB0aGUgZWRpdG9yIHdyaXRlcyB0aGVtLiBTZW5kIGluaXRpYWwgZmluZGluZ3MgdG8gdGhlIHNrZXB0aWMuIElmIGFub3RoZXIgYWdlbnQgcmV0dXJucyB3aXRoIGNvcnJlY3Rpb25zLCByZXZpc2UgeW91ciBmaW5kaW5ncyBhbmQgc2VuZCB0aGVtIHRvIHRoZSB2ZXJpZmllci4gRG8gbm90IGdpdmUgdGhlIGZpbmFsIGFuc3dlci4iLCJ0eXBlIjoic3lzdGVtIn19LHsibGMiOjEsInR5cGUiOiJjb25zdHJ1Y3RvciIsImlkIjpbImxhbmdjaGFpbiIsInNjaGVtYSIsIm1lc3NhZ2VzIiwiSHVtYW5NZXNzYWdlIl0sImt3YXJncyI6eyJjb250ZW50IjoiRm9yIGEgc3ludGhldGljIHRyYWNpbmcgZml4dHVyZSwgZXhwbGFpbiBQeXRob24gdHVwbGUgdmVyc3VzIGxpc3QgaW1tdXRhYmlsaXR5IGluIGF0IG1vc3QgMTIwIHdvcmRzLiBVc2Ugb25seSBvZmZpY2lhbCBQeXRob24gZG9jdW1lbnRhdGlvbiBpZiB5b3Ugc2VhcmNoLiBIYXZlIHRoZSBza2VwdGljIGFuZCB2ZXJpZmllciBjaGVjayBpdCwgdGhlbiBoYXZlIHRoZSBlZGl0b3Igd3JpdGUgL2Fuc3dlci5tZC4gS2VlcCB0aGUgcnVuIHNob3J0IGFuZCBkbyBub3QgYXNrIGZvbGxvdy11cCBxdWVzdGlvbnMuIiwidHlwZSI6Imh1bWFuIiwiaWQiOiI0NjIwNDMwNS1mOGVlLTQzODQtOWNlOC1mY2Q4MGJmY2RmNzUifX0seyJsYyI6MSwidHlwZSI6ImNvbnN0cnVjdG9yIiwiaWQiOlsibGFuZ2NoYWluIiwic2NoZW1hIiwibWVzc2FnZXMiLCJBSU1lc3NhZ2UiXSwia3dhcmdzIjp7ImNvbnRlbnQiOlt7ImlkIjoidG9vbHVfMDFCMkxmSEdEallTRkpobWlYTmo0Q3VRIiwiY2FsbGVyIjp7InR5cGUiOiJkaXJlY3QifSwiaW5wdXQiOnsicXVlcnkiOiJkb2NzLnB5dGhvbi5vcmcgdHVwbGVzIGltbXV0YWJsZSBzZXF1ZW5jZXMgbGlzdHMgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgdHV0b3JpYWwifSwibmFtZSI6IndlYl9zZWFyY2giLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2ZkdzlhVzlicmVuUHFneWJmTndQIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjI4NDUsIm91dHB1dF90b2tlbnMiOjczLCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjowfSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCIsIm1vZGVsX3Byb3ZpZGVyIjoiYW50aHJvcGljIn0sInR5cGUiOiJhaSIsIm5hbWUiOiJyZXNlYXJjaGVyIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTIxYTMtNzAwMC1hMzgxLTI4NGE2YTBjMDNmNy0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6IndlYl9zZWFyY2giLCJhcmdzIjp7InF1ZXJ5IjoiZG9jcy5weXRob24ub3JnIHR1cGxlcyBpbW11dGFibGUgc2VxdWVuY2VzIGxpc3RzIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIHR1dG9yaWFsIn0sImlkIjoidG9vbHVfMDFCMkxmSEdEallTRkpobWlYTmo0Q3VRIiwidHlwZSI6InRvb2xfY2FsbCJ9XSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjoyODQ1LCJvdXRwdXRfdG9rZW5zIjo3MywidG90YWxfdG9rZW5zIjoyOTE4LCJpbnB1dF90b2tlbl9kZXRhaWxzIjp7ImNhY2hlX3JlYWQiOjAsImNhY2hlX2NyZWF0aW9uIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VuX2RldGFpbHMiOnsicmVhc29uaW5nIjowfX0sImludmFsaWRfdG9vbF9jYWxscyI6W119fSx7ImxjIjoxLCJ0eXBlIjoiY29uc3RydWN0b3IiLCJpZCI6WyJsYW5nY2hhaW4iLCJzY2hlbWEiLCJtZXNzYWdlcyIsIlRvb2xNZXNzYWdlIl0sImt3YXJncyI6eyJjb250ZW50IjoiW3tcInRpdGxlXCI6IFwiNS4gRGF0YSBTdHJ1Y3R1cmVzIFxcdTIwMTQgUHl0aG9uIDMuMTQuNSBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy90dXRvcmlhbC9kYXRhc3RydWN0dXJlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJZb3UgbWlnaHQgaGF2ZSBub3RpY2VkIHRoYXQgbWV0aG9kcyBsaWtlYGluc2VydGAsYHJlbW92ZWAgb3Jgc29ydGAgdGhhdCBvbmx5IG1vZGlmeSB0aGUgbGlzdCBoYXZlIG5vIHJldHVybiB2YWx1ZSBwcmludGVkIFxcdTIwMTMgdGhleSByZXR1cm4gdGhlIGRlZmF1bHRgTm9uZWAuIFsxXSBUaGlzIGlzIGEgZGVzaWduIHByaW5jaXBsZSBmb3IgYWxsIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIGluIFB5dGhvbi5cXG4uLi5cXG4jIyA1LjMuIFR1cGxlcyBhbmQgU2VxdWVuY2VzXFx1MDBiNlxcblxcbldlIHNhdyB0aGF0IGxpc3RzIGFuZCBzdHJpbmdzIGhhdmUgbWFueSBjb21tb24gcHJvcGVydGllcywgc3VjaCBhcyBpbmRleGluZyBhbmQgc2xpY2luZyBvcGVyYXRpb25zLiBUaGV5IGFyZSB0d28gZXhhbXBsZXMgb2Ygc2VxdWVuY2UgZGF0YSB0eXBlcyAoc2VlIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgbGlzdCwgdHVwbGUsIHJhbmdlKS4gU2luY2UgUHl0aG9uIGlzIGFuIGV2b2x2aW5nIGxhbmd1YWdlLCBvdGhlciBzZXF1ZW5jZSBkYXRhIHR5cGVzIG1heSBiZSBhZGRlZC4gVGhlcmUgaXMgYWxzbyBhbm90aGVyIHN0YW5kYXJkIHNlcXVlbmNlIGRhdGEgdHlwZTogdGhlIHR1cGxlLlxcblxcbkEgdHVwbGUgY29uc2lzdHMgb2YgYSBudW1iZXIgb2YgdmFsdWVzIHNlcGFyYXRlZCBieSBjb21tYXMsIGZvciBpbnN0YW5jZTpcXG4uLi5cXG4+Pj4gIyBUdXBsZXMgYXJlIGltbXV0YWJsZTpcXG4+Pj4gdFswXSA9IDg4ODg4XFxuVHJhY2ViYWNrIChtb3N0IHJlY2VudCBjYWxsIGxhc3QpOlxcbiAgRmlsZSBcXFwiPHN0ZGluPlxcXCIsIGxpbmUgMSwgaW4gPG1vZHVsZT5cXG5UeXBlRXJyb3I6ICd0dXBsZScgb2JqZWN0IGRvZXMgbm90IHN1cHBvcnQgaXRlbSBhc3NpZ25tZW50XFxuPj4+ICMgYnV0IHRoZXkgY2FuIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzOlxcbj4+PiB2ID0gKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbj4+PiB2XFxuKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbi4uLlxcbkFzIHlvdSBzZWUsIG9uIG91dHB1dCB0dXBsZXMgYXJlIGFsd2F5cyBlbmNsb3NlZCBpbiBwYXJlbnRoZXNlcywgc28gdGhhdCBuZXN0ZWQgdHVwbGVzIGFyZSBpbnRlcnByZXRlZCBjb3JyZWN0bHk7IHRoZXkgbWF5IGJlIGlucHV0IHdpdGggb3Igd2l0aG91dCBzdXJyb3VuZGluZyBwYXJlbnRoZXNlcywgYWx0aG91Z2ggb2Z0ZW4gcGFyZW50aGVzZXMgYXJlIG5lY2Vzc2FyeSBhbnl3YXkgKGlmIHRoZSB0dXBsZSBpcyBwYXJ0IG9mIGEgbGFyZ2VyIGV4cHJlc3Npb24pLiBJdCBpcyBub3QgcG9zc2libGUgdG8gYXNzaWduIHRvIHRoZSBpbmRpdmlkdWFsIGl0ZW1zIG9mIGEgdHVwbGUsIGhvd2V2ZXIgaXQgaXMgcG9zc2libGUgdG8gY3JlYXRlIHR1cGxlcyB3aGljaCBjb250YWluIG11dGFibGUgb2JqZWN0cywgc3VjaCBhcyBsaXN0cy5cXG5cXG5UaG91Z2ggdHVwbGVzIG1heSBzZWVtIHNpbWlsYXIgdG8gbGlzdHMsIHRoZXkgYXJlIG9mdGVuIHVzZWQgaW4gZGlmZmVyZW50IHNpdHVhdGlvbnMgYW5kIGZvciBkaWZmZXJlbnQgcHVycG9zZXMuIFR1cGxlcyBhcmUgaW1tdXRhYmxlLCBhbmQgdXN1YWxseSBjb250YWluIGEgaGV0ZXJvZ2VuZW91cyBzZXF1ZW5jZSBvZiBlbGVtZW50cyB0aGF0IGFyZSBhY2Nlc3NlZCB2aWEgdW5wYWNraW5nIChzZWUgbGF0ZXIgaW4gdGhpcyBzZWN0aW9uKSBvciBpbmRleGluZyAob3IgZXZlbiBieSBhdHRyaWJ1dGUgaW4gdGhlIGNhc2Ugb2YgbmFtZWR0dXBsZXMpLiBMaXN0cyBhcmUgbXV0YWJsZSwgYW5kIHRoZWlyIGVsZW1lbnRzIGFyZSB1c3VhbGx5IGhvbW9nZW5lb3VzIGFuZCBhcmUgYWNjZXNzZWQgYnkgaXRlcmF0aW5nIG92ZXIgdGhlIGxpc3QuXFxuLi4uXFxuQW5vdGhlciB1c2VmdWwgZGF0YSB0eXBlIGJ1aWx0IGludG8gUHl0aG9uIGlzIHRoZSBkaWN0aW9uYXJ5IChzZWUgTWFwcGluZyBUeXBlcyBcXHUyMDE0IGRpY3QpLiBEaWN0aW9uYXJpZXMgYXJlIHNvbWV0aW1lcyBmb3VuZCBpbiBvdGhlciBsYW5ndWFnZXMgYXMgXFx1MjAxY2Fzc29jaWF0aXZlIG1lbW9yaWVzXFx1MjAxZCBvciBcXHUyMDFjYXNzb2NpYXRpdmUgYXJyYXlzXFx1MjAxZC4gVW5saWtlIHNlcXVlbmNlcywgd2hpY2ggYXJlIGluZGV4ZWQgYnkgYSByYW5nZSBvZiBudW1iZXJzLCBkaWN0aW9uYXJpZXMgYXJlIGluZGV4ZWQgYnkga2V5cywgd2hpY2ggY2FuIGJlIGFueSBpbW11dGFibGUgdHlwZTsgc3RyaW5ncyBhbmQgbnVtYmVycyBjYW4gYWx3YXlzIGJlIGtleXMuIFR1cGxlcyBjYW4gYmUgdXNlZCBhcyBrZXlzIGlmIHRoZXkgY29udGFpbiBvbmx5IHN0cmluZ3MsIG51bWJlcnMsIG9yIHR1cGxlczsgaWYgYSB0dXBsZSBjb250YWlucyBhbnkgbXV0YWJsZSBvYmplY3QgZWl0aGVyIGRpcmVjdGx5IG9yIGluZGlyZWN0bHksIGl0IGNhbm5vdCBiZSB1c2VkIGFzIGEga2V5LiBZb3UgY2FuXFx1MjAxOXQgdXNlIGxpc3RzIGFzIGtleXMsIHNpbmNlIGxpc3RzIGNhbiBiZSBtb2RpZmllZCBpbiBwbGFjZSB1c2luZyBpbmRleCBhc3NpZ25tZW50cywgc2xpY2UgYXNzaWdubWVudHMsIG9yIG1ldGhvZHMgbGlrZSBhcHBlbmQoKSBhbmQgZXh0ZW5kKCkuXCJdfSwge1widGl0bGVcIjogXCIzLiBEYXRhIG1vZGVsIFxcdTIwMTQgUHl0aG9uIDMuMTQuNSBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9yZWZlcmVuY2UvZGF0YW1vZGVsLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcIlRoZSB2YWx1ZSBvZiBzb21lIG9iamVjdHMgY2FuIGNoYW5nZS4gT2JqZWN0cyB3aG9zZSB2YWx1ZSBjYW4gY2hhbmdlIGFyZSBzYWlkIHRvIGJlIG11dGFibGU7IG9iamVjdHMgd2hvc2UgdmFsdWUgaXMgdW5jaGFuZ2VhYmxlIG9uY2UgdGhleSBhcmUgY3JlYXRlZCBhcmUgY2FsbGVkIGltbXV0YWJsZS4gKFRoZSB2YWx1ZSBvZiBhbiBpbW11dGFibGUgY29udGFpbmVyIG9iamVjdCB0aGF0IGNvbnRhaW5zIGEgcmVmZXJlbmNlIHRvIGEgbXV0YWJsZSBvYmplY3QgY2FuIGNoYW5nZSB3aGVuIHRoZSBsYXR0ZXJcXHUyMDE5cyB2YWx1ZSBpcyBjaGFuZ2VkOyBob3dldmVyIHRoZSBjb250YWluZXIgaXMgc3RpbGwgY29uc2lkZXJlZCBpbW11dGFibGUsIGJlY2F1c2UgdGhlIGNvbGxlY3Rpb24gb2Ygb2JqZWN0cyBpdCBjb250YWlucyBjYW5ub3QgYmUgY2hhbmdlZC4gU28sIGltbXV0YWJpbGl0eSBpcyBub3Qgc3RyaWN0bHkgdGhlIHNhbWUgYXMgaGF2aW5nIGFuIHVuY2hhbmdlYWJsZSB2YWx1ZSwgaXQgaXMgbW9yZSBzdWJ0bGUuKSBBbiBvYmplY3RcXHUyMDE5cyBtdXRhYmlsaXR5IGlzIGRldGVybWluZWQgYnkgaXRzIHR5cGU7IGZvciBpbnN0YW5jZSwgbnVtYmVycywgc3RyaW5ncyBhbmQgdHVwbGVzIGFyZSBpbW11dGFibGUsIHdoaWxlIGRpY3Rpb25hcmllcyBhbmQgbGlzdHMgYXJlIG11dGFibGUuXFxuLi4uXFxuU29tZSBvYmplY3RzIGNvbnRhaW4gcmVmZXJlbmNlcyB0byBvdGhlciBvYmplY3RzOyB0aGVzZSBhcmUgY2FsbGVkIGNvbnRhaW5lcnMuIEV4YW1wbGVzIG9mIGNvbnRhaW5lcnMgYXJlIHR1cGxlcywgbGlzdHMgYW5kIGRpY3Rpb25hcmllcy4gVGhlIHJlZmVyZW5jZXMgYXJlIHBhcnQgb2YgYSBjb250YWluZXJcXHUyMDE5cyB2YWx1ZS4gSW4gbW9zdCBjYXNlcywgd2hlbiB3ZSB0YWxrIGFib3V0IHRoZSB2YWx1ZSBvZiBhIGNvbnRhaW5lciwgd2UgaW1wbHkgdGhlIHZhbHVlcywgbm90IHRoZSBpZGVudGl0aWVzIG9mIHRoZSBjb250YWluZWQgb2JqZWN0czsgaG93ZXZlciwgd2hlbiB3ZSB0YWxrIGFib3V0IHRoZSBtdXRhYmlsaXR5IG9mIGEgY29udGFpbmVyLCBvbmx5IHRoZSBpZGVudGl0aWVzIG9mIHRoZSBpbW1lZGlhdGVseSBjb250YWluZWQgb2JqZWN0cyBhcmUgaW1wbGllZC4gU28sIGlmIGFuIGltbXV0YWJsZSBjb250YWluZXIgKGxpa2UgYSB0dXBsZSkgY29udGFpbnMgYSByZWZlcmVuY2UgdG8gYSBtdXRhYmxlIG9iamVjdCwgaXRzIHZhbHVlIGNoYW5nZXMgaWYgdGhhdCBtdXRhYmxlIG9iamVjdCBpcyBjaGFuZ2VkLlxcbi4uLlxcbiMjIyAzLjIuNS4gU2VxdWVuY2VzXFx1MDBiNlxcbi4uLlxcbiMjIyMgMy4yLjUuMS4gSW1tdXRhYmxlIHNlcXVlbmNlc1xcdTAwYjZcXG5cXG5BbiBvYmplY3Qgb2YgYW4gaW1tdXRhYmxlIHNlcXVlbmNlIHR5cGUgY2Fubm90IGNoYW5nZSBvbmNlIGl0IGlzIGNyZWF0ZWQuIChJZiB0aGUgb2JqZWN0IGNvbnRhaW5zIHJlZmVyZW5jZXMgdG8gb3RoZXIgb2JqZWN0cywgdGhlc2Ugb3RoZXIgb2JqZWN0cyBtYXkgYmUgbXV0YWJsZSBhbmQgbWF5IGJlIGNoYW5nZWQ7IGhvd2V2ZXIsIHRoZSBjb2xsZWN0aW9uIG9mIG9iamVjdHMgZGlyZWN0bHkgcmVmZXJlbmNlZCBieSBhbiBpbW11dGFibGUgb2JqZWN0IGNhbm5vdCBjaGFuZ2UuKVxcblxcblRoZSBmb2xsb3dpbmcgdHlwZXMgYXJlIGltbXV0YWJsZSBzZXF1ZW5jZXM6XFxuLi4uXFxuVHVwbGVzXFxuOiBUaGUgaXRlbXMgb2YgYSBgdHVwbGVgIGFyZSBhcmJpdHJhcnkgUHl0aG9uIG9iamVjdHMuIFR1cGxlcyBvZiB0d28gb3IgbW9yZSBpdGVtcyBhcmUgZm9ybWVkIGJ5IGNvbW1hLXNlcGFyYXRlZCBsaXN0cyBvZiBleHByZXNzaW9ucy4gQSB0dXBsZSBvZiBvbmUgaXRlbSAoYSBcXHUyMDE4c2luZ2xldG9uXFx1MjAxOSkgY2FuIGJlIGZvcm1lZCBieSBhZmZpeGluZyBhIGNvbW1hIHRvIGFuIGV4cHJlc3Npb24gKGFuIGV4cHJlc3Npb24gYnkgaXRzZWxmIGRvZXMgbm90IGNyZWF0ZSBhIHR1cGxlLCBzaW5jZSBwYXJlbnRoZXNlcyBtdXN0IGJlIHVzYWJsZSBmb3IgZ3JvdXBpbmcgb2YgZXhwcmVzc2lvbnMpLiBBbiBlbXB0eSB0dXBsZSBjYW4gYmUgZm9ybWVkIGJ5IGFuIGVtcHR5IHBhaXIgb2YgcGFyZW50aGVzZXMuXFxuLi4uXFxuIyMjIyAzLjIuNS4yLiBNdXRhYmxlIHNlcXVlbmNlc1xcdTAwYjZcXG5cXG5NdXRhYmxlIHNlcXVlbmNlcyBjYW4gYmUgY2hhbmdlZCBhZnRlciB0aGV5IGFyZSBjcmVhdGVkLiBUaGUgc3Vic2NyaXB0aW9uIGFuZCBzbGljaW5nIG5vdGF0aW9ucyBjYW4gYmUgdXNlZCBhcyB0aGUgdGFyZ2V0IG9mIGFzc2lnbm1lbnQgYW5kIGBkZWxgIChkZWxldGUpIHN0YXRlbWVudHMuXFxuLi4uXFxuVGhlcmUgYXJlIGN1cnJlbnRseSB0d28gaW50cmluc2ljIG11dGFibGUgc2VxdWVuY2UgdHlwZXM6XFxuXFxuTGlzdHNcXG46IFRoZSBpdGVtcyBvZiBhIGxpc3QgYXJlIGFyYml0cmFyeSBQeXRob24gb2JqZWN0cy4gTGlzdHMgYXJlIGZvcm1lZCBieSBwbGFjaW5nIGEgY29tbWEtc2VwYXJhdGVkIGxpc3Qgb2YgZXhwcmVzc2lvbnMgaW4gc3F1YXJlIGJyYWNrZXRzLiAoTm90ZSB0aGF0IHRoZXJlIGFyZSBubyBzcGVjaWFsIGNhc2VzIG5lZWRlZCB0byBmb3JtIGxpc3RzIG9mIGxlbmd0aCAwIG9yIDEuKVwiXX0sIHtcInRpdGxlXCI6IFwiQnVpbHQtaW4gVHlwZXMgXFx1MjAxNCBQeXRob24gMy4xNC43IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL2J1aWx0aW5zL3N0ZHR5cGVzLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcImNvbGxlY3Rpb24gY2xhc3NlcyBhcmUgbXV0YWJsZS4gVGhlIG1ldGhvZHMgdGhhdCBhZGQsIHN1YnRyYWN0LCBvciByZWFycmFuZ2UgdGhlaXIgLi4uIGluIHBsYWNlLCBhbmQgZG9uXFx1MjAxOXQgcmV0dXJuIGEgLi4uICwgbmV2ZXIgcmV0dXJuIHRoZSBjb2xsZWN0aW9uIGluc3RhbmNlIGl0c2VsZiBidXQgYE5vbmVgLlxcbi4uLlxcbiMjIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgYGxpc3RgLCBgdHVwbGVgLCBgcmFuZ2VgXFx1MDBiNlxcblxcblRoZXJlIGFyZSB0aHJlZSBiYXNpYyBzZXF1ZW5jZSB0eXBlczogbGlzdHMsIHR1cGxlcywgYW5kIHJhbmdlIG9iamVjdHMuIEFkZGl0aW9uYWwgc2VxdWVuY2UgdHlwZXMgdGFpbG9yZWQgZm9yIHByb2Nlc3Npbmcgb2YgYmluYXJ5IGRhdGEgYW5kIHRleHQgc3RyaW5ncyBhcmUgZGVzY3JpYmVkIGluIGRlZGljYXRlZCBzZWN0aW9ucy5cXG4uLi5cXG5UaGUgb3BlcmF0aW9ucyBpbiB0aGUgZm9sbG93aW5nIHRhYmxlIC4uLiBtdXRhYmxlIGFuZCBpbW11dGFibGUuIFRoZSBgY29sbGVjdGlvbnMuIC4uLiBpcyBwcm92aWRlZCB0byBtYWtlIC4uLiBlYXNpZXIgdG8gY29ycmVjdGx5IGltcGxlbWVudCB0aGVzZSBvcGVyYXRpb25zIG9uIGN1c3RvbSBzZXF1ZW5jZSB0eXBlc1xcbi4uLlxcbiMjIyBJbW11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxubXV0YWJsZSBzZXF1ZW5jZSB0eXBlcyBpc1xcbi4uLlxcbnN1cHBvcnQgYWxsb3dzIGltbXV0YWJsZSBzZXF1ZW5jZXMsIC4uLiAsIHRvIGJlIHVzZWQgYXMgYGRpY3RgIGtleXMgYW5kIHN0b3JlZCBpbiAuLi4gZW5zZXRgIGluc3RhbmNlc1xcbi4uLlxcbiMjIyBNdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbiMjIyBMaXN0c1xcdTAwYjZcXG5cXG5MaXN0cyBhcmUgbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhvbW9nZW5lb3VzIGl0ZW1zICh3aGVyZSB0aGUgcHJlY2lzZSBkZWdyZWUgb2Ygc2ltaWxhcml0eSB3aWxsIHZhcnkgYnkgYXBwbGljYXRpb24pLlxcbi4uLlxcbiMjIyBUdXBsZXNcXHUwMGI2XFxuXFxuVHVwbGVzIGFyZSBpbW11dGFibGUgc2VxdWVuY2VzLCB0eXBpY2FsbHkgdXNlZCB0byBzdG9yZSBjb2xsZWN0aW9ucyBvZiBoZXRlcm9nZW5lb3VzIGRhdGEgKHN1Y2ggYXMgdGhlIDItdHVwbGVzIHByb2R1Y2VkIGJ5IHRoZSBgZW51bWVyYXRlKClgIGJ1aWx0LWluKS4gVHVwbGVzIGFyZSBhbHNvIHVzZWQgZm9yIGNhc2VzIHdoZXJlIGFuIGltbXV0YWJsZSBzZXF1ZW5jZSBvZiBob21vZ2VuZW91cyBkYXRhIGlzIG5lZWRlZCAoc3VjaCBhcyBhbGxvd2luZyBzdG9yYWdlIGluIGEgYHNldGAgb3IgYGRpY3RgIGluc3RhbmNlKS5cXG4uLi5cXG50dXBsZShpdGVyYWJsZT1cXG4uLi5cXG5UaGUgY29uc3RydWN0b3IgYnVpbGRzIGEgdHVwbGUgd2hvc2UgaXRlbXMgYXJlIHRoZSBzYW1lIGFuZCBpbiB0aGUgc2FtZSBvcmRlciBhcyBpdGVyYWJsZVxcdTIwMTlzIGl0ZW1zLiBpdGVyYWJsZSBtYXkgYmUgZWl0aGVyIGEgc2VxdWVuY2UsIGEgY29udGFpbmVyIHRoYXQgc3VwcG9ydHMgaXRlcmF0aW9uLCBvciBhbiBpdGVyYXRvciBvYmplY3QuIElmIGl0ZXJhYmxlIGlzIGFscmVhZHkgYSB0dXBsZSwgaXQgaXMgcmV0dXJuZWQgdW5jaGFuZ2VkLiBGb3IgZXhhbXBsZSwgYHR1cGxlKCdhYmMnKWAgcmV0dXJucyBgKCdhJywgJ2InLCAnYycpYCBhbmQgYHR1cGxlKCBbMSwgMiwgM10gKWAgcmV0dXJucyBgKDEsIDIsIDMpYC4gSWYgbm8gYXJndW1lbnQgaXMgZ2l2ZW4sIHRoZSBjb25zdHJ1Y3RvciBjcmVhdGVzIGEgbmV3IGVtcHR5IHR1cGxlLCBgKClgLlxcbi4uLlxcblR1cGxlcyBpbXBsZW1lbnQgYWxsIG9mIHRoZSBjb21tb24gc2VxdWVuY2Ugb3BlcmF0aW9ucy5cXG4uLi5cXG5Gb3IgaGV0ZXJvZ2VuZW91cyBjb2xsZWN0aW9ucyBvZiBkYXRhIHdoZXJlIGFjY2VzcyBieSBuYW1lIGlzIGNsZWFyZXIgdGhhbiBhY2Nlc3MgYnkgaW5kZXgsIGBjb2xsZWN0aW9ucy5uYW1lZHR1cGxlKClgIG1heSBiZSBhIG1vcmUgYXBwcm9wcmlhdGUgY2hvaWNlIHRoYW4gYSBzaW1wbGUgdHVwbGUgb2JqZWN0LlwiXX0sIHtcInRpdGxlXCI6IFwiQnVpbHQtaW4gVHlwZXMgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL2xpYnJhcnkvc3RkdHlwZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiU29tZSBjb2xsZWN0aW9uIGNsYXNzZXMgYXJlIG11dGFibGUuIFRoZSBtZXRob2RzIHRoYXQgYWRkLCBzdWJ0cmFjdCwgb3IgcmVhcnJhbmdlIHRoZWlyIC4uLiBpbiBwbGFjZSwgYW5kIGRvblxcdTIwMTl0IHJldHVybiBhIC4uLiAsIG5ldmVyIHJldHVybiB0aGUgY29sbGVjdGlvbiBpbnN0YW5jZSBpdHNlbGYgYnV0IGBOb25lYC5cXG4uLi5cXG4jIyBTZXF1ZW5jZSBUeXBlcyBcXHUyMDE0IGBsaXN0YCwgYHR1cGxlYCwgYHJhbmdlYFxcdTAwYjZcXG5cXG5UaGVyZSBhcmUgdGhyZWUgYmFzaWMgc2VxdWVuY2UgdHlwZXM6IGxpc3RzLCB0dXBsZXMsIGFuZCAuLi4gb2JqZWN0cy4gQWRkaXRpb25hbCBzZXF1ZW5jZSB0eXBlcyB0YWlsb3JlZCBmb3IgcHJvY2Vzc2luZyBvZiBiaW5hcnkgZGF0YSBhbmQgdGV4dCBzdHJpbmdzIGFyZSBkZXNjcmliZWQgaW4gZGVkaWNhdGVkIHNlY3Rpb25zLlxcbi4uLlxcblRoZSBvcGVyYXRpb25zIGluIHRoZSBmb2xsb3dpbmcgdGFibGUgLi4uIGltbXV0YWJsZS4gVGhlIGAgLi4uIGlzIHByb3ZpZGVkIHRvIG1ha2UgLi4uIGVhc2llciB0byBjb3JyZWN0bHkgaW1wbGVtZW50IHRoZXNlIG9wZXJhdGlvbnMgb24gY3VzdG9tIHNlcXVlbmNlIHR5cGVzXFxuLi4uXFxuIyMjIEltbXV0YWJsZSBTZXF1ZW5jZSBUeXBlc1xcdTAwYjZcXG4uLi5cXG5vcGVyYXRpb24gdGhhdCBpbW11dGFibGUgc2VxdWVuY2UgLi4uIGJ5IG11dGFibGUgc2VxdWVuY2UgdHlwZXMgaXMgLi4uIGBoYXNoKClgXFxuLi4uXFxuVGhpcyBzdXBwb3J0IGFsbG93cyBpbW11dGFibGUgc2VxdWVuY2VzLCBzdWNoIGFzIGB0dXBsZWAgaW5zdGFuY2VzLCB0byBiZSB1c2VkIGFzIGBkaWN0YCBrZXlzIGFuZCBzdG9yZWQgaW4gYHNldGAgYW5kIGBmcm96ZW5zZXRgIGluc3RhbmNlcy5cXG4uLi5cXG4jIyMgTXV0YWJsZSBTZXF1ZW5jZSBUeXBlc1xcdTAwYjZcXG4uLi5cXG4jIyMgTGlzdHNcXHUwMGI2XFxuXFxuTGlzdHMgYXJlIG11dGFibGUgc2VxdWVuY2VzLCB0eXBpY2FsbHkgdXNlZCB0byBzdG9yZSBjb2xsZWN0aW9ucyBvZiBob21vZ2VuZW91cyBpdGVtcyAod2hlcmUgdGhlIHByZWNpc2UgZGVncmVlIG9mIHNpbWlsYXJpdHkgd2lsbCB2YXJ5IGJ5IGFwcGxpY2F0aW9uKS5cXG4uLi5cXG4jIyMgVHVwbGVzXFx1MDBiNlxcblxcblR1cGxlcyBhcmUgaW1tdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaGV0ZXJvZ2VuZW91cyBkYXRhIChzdWNoIGFzIHRoZSAyLXR1cGxlcyBwcm9kdWNlZCBieSB0aGUgYGVudW1lcmF0ZSgpYCBidWlsdC1pbikuIFR1cGxlcyBhcmUgYWxzbyB1c2VkIGZvciBjYXNlcyB3aGVyZSBhbiBpbW11dGFibGUgc2VxdWVuY2Ugb2YgaG9tb2dlbmVvdXMgZGF0YSBpcyBuZWVkZWQgKHN1Y2ggYXMgYWxsb3dpbmcgc3RvcmFnZSBpbiBhIGBzZXRgIG9yIGBkaWN0YCBpbnN0YW5jZSkuXFxuLi4uXFxuVGhlIGNvbnN0cnVjdG9yIGJ1aWxkcyBhIHR1cGxlIHdob3NlIGl0ZW1zIGFyZSB0aGUgc2FtZSBhbmQgaW4gdGhlIHNhbWUgb3JkZXIgYXMgaXRlcmFibGVcXHUyMDE5cyBpdGVtcy4gaXRlcmFibGUgbWF5IGJlIGVpdGhlciBhIHNlcXVlbmNlLCBhIGNvbnRhaW5lciB0aGF0IHN1cHBvcnRzIGl0ZXJhdGlvbiwgb3IgYW4gaXRlcmF0b3Igb2JqZWN0LiBJZiBpdGVyYWJsZSBpcyBhbHJlYWR5IGEgdHVwbGUsIGl0IGlzIHJldHVybmVkIHVuY2hhbmdlZC4gRm9yIGV4YW1wbGUsIGB0dXBsZSgnYWJjJylgIHJldHVybnMgYCgnYScsICdiJywgJ2MnKWAgYW5kIGB0dXBsZSggWzEsIDIsIDNdIClgIHJldHVybnMgYCgxLCAyLCAzKWAuIElmIG5vIGFyZ3VtZW50IGlzIGdpdmVuLCB0aGUgY29uc3RydWN0b3IgY3JlYXRlcyBhIG5ldyBlbXB0eSB0dXBsZSwgYCgpYC5cXG4uLi5cXG5UdXBsZXMgaW1wbGVtZW50IGFsbCBvZiB0aGUgY29tbW9uIHNlcXVlbmNlIG9wZXJhdGlvbnMuXFxuLi4uXFxuRm9yIGhldGVyb2dlbmVvdXMgY29sbGVjdGlvbnMgb2YgZGF0YSB3aGVyZSBhY2Nlc3MgYnkgbmFtZSBpcyBjbGVhcmVyIHRoYW4gYWNjZXNzIGJ5IGluZGV4LCBgY29sbGVjdGlvbnMubmFtZWR0dXBsZSgpYCBtYXkgYmUgYSBtb3JlIGFwcHJvcHJpYXRlIGNob2ljZSB0aGFuIGEgc2ltcGxlIHR1cGxlIG9iamVjdC5cIl19LCB7XCJ0aXRsZVwiOiBcIjUuIERhdGEgU3RydWN0dXJlcyBcXHUyMDE0IFB5dGhvbiAzLjEwLjIwIGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zLjEwL3R1dG9yaWFsL2RhdGFzdHJ1Y3R1cmVzLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcIllvdSBtaWdodCBoYXZlIG5vdGljZWQgdGhhdCBtZXRob2RzIGxpa2UgYGluc2VydGAsIGByZW1vdmVgIG9yIGBzb3J0YCB0aGF0IG9ubHkgbW9kaWZ5IHRoZSBsaXN0IGhhdmUgbm8gcmV0dXJuIHZhbHVlIHByaW50ZWQgXFx1MjAxMyB0aGV5IHJldHVybiB0aGUgZGVmYXVsdCBgTm9uZWAuIDEgVGhpcyBpcyBhIGRlc2lnbiBwcmluY2lwbGUgZm9yIGFsbCBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyBpbiBQeXRob24uXFxuLi4uXFxuIyMgNS4zLiBUdXBsZXMgYW5kIFNlcXVlbmNlc1xcdTAwYjZcXG5cXG5XZSBzYXcgdGhhdCBsaXN0cyBhbmQgc3RyaW5ncyBoYXZlIG1hbnkgY29tbW9uIHByb3BlcnRpZXMsIHN1Y2ggYXMgaW5kZXhpbmcgYW5kIHNsaWNpbmcgb3BlcmF0aW9ucy4gVGhleSBhcmUgdHdvIGV4YW1wbGVzIG9mIHNlcXVlbmNlIGRhdGEgdHlwZXMgKHNlZSBTZXF1ZW5jZSBUeXBlcyBcXHUyMDE0IGxpc3QsIHR1cGxlLCByYW5nZSkuIFNpbmNlIFB5dGhvbiBpcyBhbiBldm9sdmluZyBsYW5ndWFnZSwgb3RoZXIgc2VxdWVuY2UgZGF0YSB0eXBlcyBtYXkgYmUgYWRkZWQuIFRoZXJlIGlzIGFsc28gYW5vdGhlciBzdGFuZGFyZCBzZXF1ZW5jZSBkYXRhIHR5cGU6IHRoZSB0dXBsZS5cXG5cXG5BIHR1cGxlIGNvbnNpc3RzIG9mIGEgbnVtYmVyIG9mIHZhbHVlcyBzZXBhcmF0ZWQgYnkgY29tbWFzLCBmb3IgaW5zdGFuY2U6XFxuLi4uXFxuPj4+ICMgVHVwbGVzIGFyZSBpbW11dGFibGU6XFxuLi4uIHRbMF0gPSA4ODg4OFxcblRyYWNlYmFjayAobW9zdCByZWNlbnQgY2FsbCBsYXN0KTpcXG4gIEZpbGUgXFxcIjxzdGRpbj5cXFwiLCBsaW5lIDEsIGluIDxtb2R1bGU+XFxuVHlwZUVycm9yOiAndHVwbGUnIG9iamVjdCBkb2VzIG5vdCBzdXBwb3J0IGl0ZW0gYXNzaWdubWVudFxcbj4+PiAjIGJ1dCB0aGV5IGNhbiBjb250YWluIG11dGFibGUgb2JqZWN0czpcXG4uLi4gdiA9IChbMSwgMiwgM10sIFszLCAyLCAxXSlcXG4+Pj4gdlxcbihbMSwgMiwgM10sIFszLCAyLCAxXSlcXG4uLi5cXG5BcyB5b3Ugc2VlLCBvbiBvdXRwdXQgdHVwbGVzIGFyZSBhbHdheXMgZW5jbG9zZWQgaW4gcGFyZW50aGVzZXMsIHNvIHRoYXQgbmVzdGVkIHR1cGxlcyBhcmUgaW50ZXJwcmV0ZWQgY29ycmVjdGx5OyB0aGV5IG1heSBiZSBpbnB1dCB3aXRoIG9yIHdpdGhvdXQgc3Vycm91bmRpbmcgcGFyZW50aGVzZXMsIGFsdGhvdWdoIG9mdGVuIHBhcmVudGhlc2VzIGFyZSBuZWNlc3NhcnkgYW55d2F5IChpZiB0aGUgdHVwbGUgaXMgcGFydCBvZiBhIGxhcmdlciBleHByZXNzaW9uKS4gSXQgaXMgbm90IHBvc3NpYmxlIHRvIGFzc2lnbiB0byB0aGUgaW5kaXZpZHVhbCBpdGVtcyBvZiBhIHR1cGxlLCBob3dldmVyIGl0IGlzIHBvc3NpYmxlIHRvIGNyZWF0ZSB0dXBsZXMgd2hpY2ggY29udGFpbiBtdXRhYmxlIG9iamVjdHMsIHN1Y2ggYXMgbGlzdHMuXFxuXFxuVGhvdWdoIHR1cGxlcyBtYXkgc2VlbSBzaW1pbGFyIHRvIGxpc3RzLCB0aGV5IGFyZSBvZnRlbiB1c2VkIGluIGRpZmZlcmVudCBzaXR1YXRpb25zIGFuZCBmb3IgZGlmZmVyZW50IHB1cnBvc2VzLiBUdXBsZXMgYXJlIGltbXV0YWJsZSwgYW5kIHVzdWFsbHkgY29udGFpbiBhIGhldGVyb2dlbmVvdXMgc2VxdWVuY2Ugb2YgZWxlbWVudHMgdGhhdCBhcmUgYWNjZXNzZWQgdmlhIHVucGFja2luZyAoc2VlIGxhdGVyIGluIHRoaXMgc2VjdGlvbikgb3IgaW5kZXhpbmcgKG9yIGV2ZW4gYnkgYXR0cmlidXRlIGluIHRoZSBjYXNlIG9mIGBuYW1lZHR1cGxlc2ApLiBMaXN0cyBhcmUgbXV0YWJsZSwgYW5kIHRoZWlyIGVsZW1lbnRzIGFyZSB1c3VhbGx5IGhvbW9nZW5lb3VzIGFuZCBhcmUgYWNjZXNzZWQgYnkgaXRlcmF0aW5nIG92ZXIgdGhlIGxpc3QuXFxuLi4uXFxuIyMgNS40Llxcbi4uLlxcbi4gU2V0IG9iamVjdHNcXG4uLi5cXG4jIyA1LjUuIERpY3Rpb25hcmllc1xcdTAwYjZcXG5cXG5Bbm90aGVyIHVzZWZ1bCBkYXRhIHR5cGUgYnVpbHQgaW50byBQeXRob24gaXMgdGhlIGRpY3Rpb25hcnkgKHNlZSBNYXBwaW5nIFR5cGVzIFxcdTIwMTQgZGljdCkuIERpY3Rpb25hcmllcyBhcmUgc29tZXRpbWVzIGZvdW5kIGluIG90aGVyIGxhbmd1YWdlcyBhcyBcXHUyMDFjYXNzb2NpYXRpdmUgbWVtb3JpZXNcXHUyMDFkIG9yIFxcdTIwMWNhc3NvY2lhdGl2ZSBhcnJheXNcXHUyMDFkLiBVbmxpa2Ugc2VxdWVuY2VzLCB3aGljaCBhcmUgaW5kZXhlZCBieSBhIHJhbmdlIG9mIG51bWJlcnMsIGRpY3Rpb25hcmllcyBhcmUgaW5kZXhlZCBieSBrZXlzLCB3aGljaCBjYW4gYmUgYW55IGltbXV0YWJsZSB0eXBlOyBzdHJpbmdzIGFuZCBudW1iZXJzIGNhbiBhbHdheXMgYmUga2V5cy4gVHVwbGVzIGNhbiBiZSB1c2VkIGFzIGtleXMgaWYgdGhleSBjb250YWluIG9ubHkgc3RyaW5ncywgbnVtYmVycywgb3IgdHVwbGVzOyBpZiBhIHR1cGxlIGNvbnRhaW5zIGFueSBtdXRhYmxlIG9iamVjdCBlaXRoZXIgZGlyZWN0bHkgb3IgaW5kaXJlY3RseSwgaXQgY2Fubm90IGJlIHVzZWQgYXMgYSBrZXkuIFlvdSBjYW5cXHUyMDE5dCB1c2UgbGlzdHMgYXMga2V5cywgc2luY2UgbGlzdHMgY2FuIGJlIG1vZGlmaWVkIGluIHBsYWNlIHVzaW5nIGluZGV4IGFzc2lnbm1lbnRzLCBzbGljZSBhc3NpZ25tZW50cywgb3IgbWV0aG9kcyBsaWtlIGBhcHBlbmQoKWAgYW5kIGBleHRlbmQoKWAuXCJdfV0iLCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ3ZWJfc2VhcmNoIiwiaWQiOiI2ZTUxMjVhYy0zMTY0LTRhNzYtOWM1Mi1hYzM0NjBjZTM2ZTQiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJzdGF0dXMiOiJzdWNjZXNzIn19XV19" + } + }, + { + "key": "gen_ai.usage.input_tokens", + "value": { + "intValue": "7378" + } + }, + { + "key": "gen_ai.usage.output_tokens", + "value": { + "intValue": "33" + } + }, + { + "key": "gen_ai.usage.total_tokens", + "value": { + "intValue": "7411" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJnZW5lcmF0aW9ucyI6W1t7InRleHQiOiIiLCJnZW5lcmF0aW9uX2luZm8iOm51bGwsInR5cGUiOiJDaGF0R2VuZXJhdGlvbiIsIm1lc3NhZ2UiOnsibGMiOjEsInR5cGUiOiJjb25zdHJ1Y3RvciIsImlkIjpbImxhbmdjaGFpbiIsInNjaGVtYSIsIm1lc3NhZ2VzIiwiQUlNZXNzYWdlIl0sImt3YXJncyI6eyJjb250ZW50IjpbeyJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3NrZXB0aWMiLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2Zkd0E2RjRjQ041eWhENENyczlNIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjczNzgsIm91dHB1dF90b2tlbnMiOjMzLCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjowfSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCIsIm1vZGVsX3Byb3ZpZGVyIjoiYW50aHJvcGljIn0sInR5cGUiOiJhaSIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy0zY2Q4LTc5ZjEtODhhMS1jZTZjZWUyOTRlNzktMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ0cmFuc2Zlcl90b19za2VwdGljIiwiYXJncyI6e30sImlkIjoidG9vbHVfMDFYamozYVJkV3loQkpmUG1USkJ4UWE5IiwidHlwZSI6InRvb2xfY2FsbCJ9XSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjo3Mzc4LCJvdXRwdXRfdG9rZW5zIjozMywidG90YWxfdG9rZW5zIjo3NDExLCJpbnB1dF90b2tlbl9kZXRhaWxzIjp7ImNhY2hlX3JlYWQiOjAsImNhY2hlX2NyZWF0aW9uIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VuX2RldGFpbHMiOnsicmVhc29uaW5nIjowfX0sImludmFsaWRfdG9vbF9jYWxscyI6W119fX1dXSwibGxtX291dHB1dCI6eyJpZCI6Im1zZ18wMTFDZmR3QTZGNGNDTjV5aEQ0Q3JzOU0iLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjB9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0In0sInJ1biI6bnVsbCwidHlwZSI6IkxMTVJlc3VsdCJ9" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "08f0feca38b72d47", + "parentSpanId": "e2830d2d0406001a", + "name": "researcher", + "kind": 1, + "startTimeUnixNano": "1790968013212771840", + "endTimeUnixNano": "1790968023777713152", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "e30=" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "researcher" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "1" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "researcher" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:researcher\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"researcher\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "researcher" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "seq:step:1" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W3siY29udGVudCI6IkZvciBhIHN5bnRoZXRpYyB0cmFjaW5nIGZpeHR1cmUsIGV4cGxhaW4gUHl0aG9uIHR1cGxlIHZlcnN1cyBsaXN0IGltbXV0YWJpbGl0eSBpbiBhdCBtb3N0IDEyMCB3b3Jkcy4gVXNlIG9ubHkgb2ZmaWNpYWwgUHl0aG9uIGRvY3VtZW50YXRpb24gaWYgeW91IHNlYXJjaC4gSGF2ZSB0aGUgc2tlcHRpYyBhbmQgdmVyaWZpZXIgY2hlY2sgaXQsIHRoZW4gaGF2ZSB0aGUgZWRpdG9yIHdyaXRlIC9hbnN3ZXIubWQuIEtlZXAgdGhlIHJ1biBzaG9ydCBhbmQgZG8gbm90IGFzayBmb2xsb3ctdXAgcXVlc3Rpb25zLiIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoiaHVtYW4iLCJpZCI6IjQ2MjA0MzA1LWY4ZWUtNDM4NC05Y2U4LWZjZDgwYmZjZGY3NSJ9XX0=" + } + } + ], + "events": [ + { + "timeUnixNano": "1790968023841631000", + "name": "exception", + "attributes": [ + { + "key": "exception.type", + "value": { + "stringValue": "Exception" + } + }, + { + "key": "exception.message", + "value": { + "stringValue": "ParentCommand(Command(graph='researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9')], 'active_agent': 'skeptic'}, goto='skeptic'))Traceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 2967, in stream\n for _ in runner.tick(\n ~~~~~~~~~~~^\n [t for t in loop.tasks.values() if not t.writes],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<2 lines>...\n schedule_task=loop.accept_push,\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ):\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_runner.py\", line 207, in tick\n run_with_retry(\n ~~~~~~~~~~~~~~^\n t,\n ^^\n ...<10 lines>...\n },\n ^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_retry.py\", line 617, in run_with_retry\n return task.proc.invoke(task.input, config)\n ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 709, in invoke\n input = step.invoke(input, config)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 447, in invoke\n ret = self.func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 84, in _write\n self.do_write(\n ~~~~~~~~~~~~~^\n config,\n ^^^^^^^\n writes,\n ^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 126, in do_write\n write(_assemble_writes(writes))\n ~~~~~~~~~~~~~~~~^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 181, in _assemble_writes\n if ww := w.mapper(w.value):\n ~~~~~~~~^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/graph/state.py\", line 1762, in _control_branch\n raise ParentCommand(command)\n\n\nlanggraph.errors.ParentCommand: Command(graph='researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9')], 'active_agent': 'skeptic'}, goto='skeptic')" + } + }, + { + "key": "exception.stacktrace", + "value": { + "stringValue": "Exception: ParentCommand(Command(graph='researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9')], 'active_agent': 'skeptic'}, goto='skeptic'))Traceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 2967, in stream\n for _ in runner.tick(\n ~~~~~~~~~~~^\n [t for t in loop.tasks.values() if not t.writes],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<2 lines>...\n schedule_task=loop.accept_push,\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ):\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_runner.py\", line 207, in tick\n run_with_retry(\n ~~~~~~~~~~~~~~^\n t,\n ^^\n ...<10 lines>...\n },\n ^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_retry.py\", line 617, in run_with_retry\n return task.proc.invoke(task.input, config)\n ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 709, in invoke\n input = step.invoke(input, config)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 447, in invoke\n ret = self.func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 84, in _write\n self.do_write(\n ~~~~~~~~~~~~~^\n config,\n ^^^^^^^\n writes,\n ^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 126, in do_write\n write(_assemble_writes(writes))\n ~~~~~~~~~~~~~~~~^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 181, in _assemble_writes\n if ww := w.mapper(w.value):\n ~~~~~~~~^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/graph/state.py\", line 1762, in _control_branch\n raise ParentCommand(command)\n\n\nlanggraph.errors.ParentCommand: Command(graph='researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9')], 'active_agent': 'skeptic'}, goto='skeptic')\n" + } + }, + { + "key": "exception.escaped", + "value": { + "stringValue": "False" + } + } + ] + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "e2830d2d0406001a", + "parentSpanId": "04ffc0db9ce0f358", + "name": "researcher", + "kind": 1, + "startTimeUnixNano": "1790968013212549888", + "endTimeUnixNano": "1790968023779422976", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "e30=" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "researcher" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langgraph" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "1" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "researcher" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:researcher\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"researcher\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "graph:step:1" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W3siY29udGVudCI6IkZvciBhIHN5bnRoZXRpYyB0cmFjaW5nIGZpeHR1cmUsIGV4cGxhaW4gUHl0aG9uIHR1cGxlIHZlcnN1cyBsaXN0IGltbXV0YWJpbGl0eSBpbiBhdCBtb3N0IDEyMCB3b3Jkcy4gVXNlIG9ubHkgb2ZmaWNpYWwgUHl0aG9uIGRvY3VtZW50YXRpb24gaWYgeW91IHNlYXJjaC4gSGF2ZSB0aGUgc2tlcHRpYyBhbmQgdmVyaWZpZXIgY2hlY2sgaXQsIHRoZW4gaGF2ZSB0aGUgZWRpdG9yIHdyaXRlIC9hbnN3ZXIubWQuIEtlZXAgdGhlIHJ1biBzaG9ydCBhbmQgZG8gbm90IGFzayBmb2xsb3ctdXAgcXVlc3Rpb25zLiIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoiaHVtYW4iLCJpZCI6IjQ2MjA0MzA1LWY4ZWUtNDM4NC05Y2U4LWZjZDgwYmZjZGY3NSJ9XX0=" + } + } + ], + "events": [ + { + "timeUnixNano": "1790968023841736000", + "name": "exception", + "attributes": [ + { + "key": "exception.type", + "value": { + "stringValue": "Exception" + } + }, + { + "key": "exception.message", + "value": { + "stringValue": "ParentCommand(Command(graph='researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9')], 'active_agent': 'skeptic'}, goto='skeptic'))Traceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 707, in invoke\n input = context.run(step.invoke, input, config, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 3913, in invoke\n for chunk in self.stream(\n ~~~~~~~~~~~^\n input,\n ^^^^^^\n ...<11 lines>...\n **kwargs,\n ^^^^^^^^^\n ):\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 2967, in stream\n for _ in runner.tick(\n ~~~~~~~~~~~^\n [t for t in loop.tasks.values() if not t.writes],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<2 lines>...\n schedule_task=loop.accept_push,\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ):\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_runner.py\", line 207, in tick\n run_with_retry(\n ~~~~~~~~~~~~~~^\n t,\n ^^\n ...<10 lines>...\n },\n ^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_retry.py\", line 617, in run_with_retry\n return task.proc.invoke(task.input, config)\n ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 709, in invoke\n input = step.invoke(input, config)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 447, in invoke\n ret = self.func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 84, in _write\n self.do_write(\n ~~~~~~~~~~~~~^\n config,\n ^^^^^^^\n writes,\n ^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 126, in do_write\n write(_assemble_writes(writes))\n ~~~~~~~~~~~~~~~~^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 181, in _assemble_writes\n if ww := w.mapper(w.value):\n ~~~~~~~~^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/graph/state.py\", line 1762, in _control_branch\n raise ParentCommand(command)\n\n\nlanggraph.errors.ParentCommand: Command(graph='researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9')], 'active_agent': 'skeptic'}, goto='skeptic')" + } + }, + { + "key": "exception.stacktrace", + "value": { + "stringValue": "Exception: ParentCommand(Command(graph='researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9')], 'active_agent': 'skeptic'}, goto='skeptic'))Traceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 707, in invoke\n input = context.run(step.invoke, input, config, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 3913, in invoke\n for chunk in self.stream(\n ~~~~~~~~~~~^\n input,\n ^^^^^^\n ...<11 lines>...\n **kwargs,\n ^^^^^^^^^\n ):\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 2967, in stream\n for _ in runner.tick(\n ~~~~~~~~~~~^\n [t for t in loop.tasks.values() if not t.writes],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<2 lines>...\n schedule_task=loop.accept_push,\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ):\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_runner.py\", line 207, in tick\n run_with_retry(\n ~~~~~~~~~~~~~~^\n t,\n ^^\n ...<10 lines>...\n },\n ^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_retry.py\", line 617, in run_with_retry\n return task.proc.invoke(task.input, config)\n ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 709, in invoke\n input = step.invoke(input, config)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 447, in invoke\n ret = self.func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 84, in _write\n self.do_write(\n ~~~~~~~~~~~~~^\n config,\n ^^^^^^^\n writes,\n ^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 126, in do_write\n write(_assemble_writes(writes))\n ~~~~~~~~~~~~~~~~^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 181, in _assemble_writes\n if ww := w.mapper(w.value):\n ~~~~~~~~^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/graph/state.py\", line 1762, in _control_branch\n raise ParentCommand(command)\n\n\nlanggraph.errors.ParentCommand: Command(graph='researcher:9227aa30-2ab0-7e70-70d0-d760f3ff5828', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9')], 'active_agent': 'skeptic'}, goto='skeptic')\n" + } + }, + { + "key": "exception.escaped", + "value": { + "stringValue": "False" + } + } + ] + } + ], + "status": { + "code": 1 + }, + "flags": 256 + } + ] + } + ] + }, + { + "resource": { + "attributes": [ + { + "key": "telemetry.sdk.language", + "value": { + "stringValue": "python" + } + }, + { + "key": "telemetry.sdk.name", + "value": { + "stringValue": "opentelemetry" + } + }, + { + "key": "telemetry.sdk.version", + "value": { + "stringValue": "1.45.0" + } + }, + { + "key": "service.instance.id", + "value": { + "stringValue": "b873df4c-5106-4097-a20b-b9380724f4d9" + } + }, + { + "key": "service.name", + "value": { + "stringValue": "deeplite" + } + } + ] + }, + "scopeSpans": [ + { + "scope": { + "name": "langsmith" + }, + "spans": [ + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "3049adb48476557c", + "parentSpanId": "419c86d999598365", + "name": "tools", + "kind": 1, + "startTimeUnixNano": "1790968030835418880", + "endTimeUnixNano": "1790968030840750848", + "attributes": [ + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "tools" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "2" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "tools" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"__pregel_push\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_push\",0,false]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "skeptic:3dfc4328-0d19-e1ea-bcda-3d3d36d7cda8|tools:e52223df-38bd-4e1b-4faf-e72de38488d4" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "skeptic" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "skeptic:3dfc4328-0d19-e1ea-bcda-3d3d36d7cda8" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "graph:step:2" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJpbnB1dCI6W3sibmFtZSI6InRyYW5zZmVyX3RvX3ZlcmlmaWVyIiwiYXJncyI6e30sImlkIjoidG9vbHVfMDFINmZWOEp2UkpOdlh5NDdlUm5kM2pIIiwidHlwZSI6InRvb2xfY2FsbCJ9XX0=" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "e30=" + } + } + ], + "events": [ + { + "timeUnixNano": "1790968030907778000", + "name": "exception", + "attributes": [ + { + "key": "exception.type", + "value": { + "stringValue": "Exception" + } + }, + { + "key": "exception.message", + "value": { + "stringValue": "ParentCommand(Command(graph='__parent__', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', id='389058d1-083f-4632-bbc5-09bfd3a0ffd2', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9'), AIMessage(content=[{'signature': 'CAQS1QgKEAgSGAI4AUIIdGhpbmtpbmcSDN6TfU6NDM971lEbzxoMvRUA1J9ox3fJQYT9IjArCQQGHBP4pRHPavXzRlbMbaYHC57Yj+Gbxgr0helvd2OIL/o6Rbt4PFA3FaIFUlgq8gc7KQI9FtGnvSAkLBRzIsUp/2d5Rd2zgEsfkFVeXsn8WYYZkinAQVVy6A7B3LaEJW0xb/x88Di67fedwx0Q+FlZtwcmT3Dcaa11JveyHRr3dm+zGSZP6CXeXNJ8nrTZ7W1YRTysD6w7o7Lp8h1Rwq4f4o8j+QUfY0vje+zJZsxHPmQNt3ykeKh99hgn6dbiz6/EHqmbHjSAu1M4SqEDfqIKThHYenamAZmcQ4XAOo6mdQ6+77VgpV1lC1/k52IiUoJjkGlQrnLCvutiI7KN+sgK8c2+2KCJrPRCvVV+BkStvP5jWWqzytpKQ+nHzhL448OrIA9gdJC39C8eDrXeRnI7t4FqlkXGoMlZAV9UMcrQHRxOMi8tOyTzi9oyBFDTSsQ+LJqer0tz1Ft9gek6vzg50SpVAFQdLERv8R1YqMaeI+xsvTAEW9MRgvRMkzLuzRT2guw3jD+Fjip37xgK5QI0R+CsOAmQPoTZizzcIyAUAuCJFCgq/gD/CibLxoymiVUI1an9Snt3KpNRoWdvWIne33V0pv8PHg9yW2JuLOqQGilLCsK/Q3oQr3upEMXxXGjCL7WUizvRZGFEEV+VplYt7v4tKZMj5pk3vdeM0MJ5XZg7PvUsRzycxcnrT3Knoc3qM+ttqZfTsb8d6U/pQifq7GuahFOHrvroqgJLj+/81Yy1pZFkjfJgY0XklzAnA6CFq/YT0gxbV+TH5IS+WaYOUzK36S8s5qqMwhngv4IzE3ctXXqnhIZXNznyVBqbYa/Gx2bCjcEj2n5RyGYRS/+MculgLBznQ9MkG2D7QP3Mq/nwHXKMIaLS8VK8hG3d0rAjlvnauXShNrJxtkj4qkwnxD4LrEjk1ue+PLcbLqYWwAr8n0s+41jrsIPVyyK9vFaEZrA9KaBxkJyiqx50GZNn7ckx+mkI18gH52iyVPtU8FKMXjugCl6mPLiHYiDQd+G63LudmOKys9itVnBDBNMP2b/I7+bIVRlheGaZWr6MATKl86B3y+0kuOrLRMyWcdibc2iQkZm+8f7dTUwbKbBJsKOFY73/8nlL/JiYpjz6nUIk+IqSjaFyK6XFlCcukWpWHpDJnSvbEzey7qHyCqJ+I+on8iP8GOtempQaS0/4kZGSwlrE6xOj6EwGpfjbDZKHkP/2RAdNYPQq3z7bdVJMJNnFMz6e37mzMpCBihwPdSFTRE3SCY66wvTgJzGCUyhHbSi5i1azzOXd8uHCSpT/6L1o6VU0pjoR8Kaa4wCWHGf7P/WEd+avyp02Jnu1cmpW0+Gq+vK9h0tdb2IClM/eNfdouQP+ep8MJUjraDRICmAJQY5obS71vniOo+reaFy0zRgB', 'thinking': '', 'type': 'thinking'}, {'signature': 'CAQS/REKEQgSGAI4AUIJbmFycmF0aW9uEgwFh3AVJZP3s8y1Hq0aDG76uhHONaScXAGShiIwGgjLXHUx1yZrkLPljjzTOQycpLJiKVwXcxm/EzKJm1s8M5WxuREWEldXcoaN0/msKpkRMT+eLb5ih9rAHULTomUNsUwJeu0jrJe4GLazy7sP5KtjEOEBKQsXUfoQ7fDkZOQUHZi9zzQN4hFNmYJUf3OVqf9TQNdC1+m//HAZPfWmAy6HtDvm7kgBJkCEoiFnm7tn/PX1hXGPRgWc6BTXUMr2WA8aKyqggRUbMH2+JYv5iWZddCc1up1Nbr3sF7WPcot9ZBhyHQXr692zeJk6SJgygIVrASbMly2bBxTWJB623gVgzpCmVSLZFpkKXgPcp8hGyDKOvF6SiQSHqGRxQofr7Vs1aPhfZixROJGD+MlrkggVAnGMyYIDUZpavtCeiU1JmBSz0084bXPtT/tnpsKOnSPYZQo3B0BXKbqA/WsKng8/V3Z8Uuajo6IVZ2hgA1WDduqQOQkExGU6pFuLhebIjuWxSNI/DffvR//LNqCfvdnf0hvoPtDxfx/SawIx4/spq9xbwFzHtLgoHg1F2wFw7clPDlg1PyD/v2yJG3V9T8GL6jGPE6rhfil/u4HNvf7BW0dQzCTTXcqZy4nByy0+p+S7ZxfMl4Kpxa2gOhhSNjfpfuX0QpL4kME8J1nliFN1d8KqEPuDI18BQHUhD/UfY29oA0x510FjM/BsLZ4n5sppkoIi7CFoQq2aAKVNfC1KilhIpRD+1dyDtZQklN8gZpOESHw6ML1TtU0yfAcbmqoxJ6hhjzwH6CMsTQEEy8MbRBM+QKKkIRdRCPHGFW2LkuR50+J0pF/tr6pqVvJeozfiMnrEilB27TZP4nKTJG8rd//vCWen0y7CA/8dBJ9gdp5tx7NVA45k1gdcxER1gYouNX3n1vG3/B1F4+93wXW9Mqo6Us59TQmrb9Nqa7pSQNSrH06w1GxyBAlkFfqAlafk16aMpq416TJYpGy43WCUiCcLWlhiWgz2sgOLAXP9a5weWAKRC8J6jkcUkDeXnW+HFSiPM6+9Bo6kVhLUhN5rc4mHZOZpF7ptPpsQN4XCqLPsqpRH/8o5XpyC6ZneTeF2ExtjhwZVjXDwMbIzVYIbw1y2PPGw8dC7pc4sIVSnI4oQAH0v650ghfhmMevWW4h4Pylbw/DG4fdeT3+8z0f8MOSTtfipx05/QttHlAvODRxa5yDCvOz6n3f1BO0N/4MbGyOM27qW4x8mBVoryUZWfsmhfi0/GfNvB8+ylTGbYNX27OcEF5WZtNqbWEy7V5GbmZUSxspcg7FBZ3j3/3L7muvZXoZi1f5erq4y3lSH+oF7qPFnvieyLOCwubcQy+oyowj4cD0idRecnp+s+52D7+mzF1gpW+f+cJasoxigbxkOloS4yeiKvgM4nWSpGEj12RPx5o80EmziXN9t4zJzW1htA5hXJJeFympMYtyCUCevRWVmrRYktQ7UnCa6tL/AVPi1ni5XCXHfLOuqfK5KLYxjwVtdhhfmCLvpaKVNCEOCf4nNGHqD98wUb0ejWNwDYBnJGkBRw9RVspfwNdoWoVcn2qv8ywGUp7yc/sk2LpI6VziRyvWqbGNG1ZJqVVutRc46+sOE4VSeS2/CTNKNa6NMCf/hxxaUVsYfKdP+S1Lc4ianKp0sUugPpmtoydC0CP2RwjnHLcnzxCnvqxlLQWXMNPTsu2yBcHlWJLtcmdg/McLagy7l5kG6sN7ZxJLD/kAx8E6uqohP+K/jMSrYuQJnhr4tf4biWFY3Jzf/KpAYyVSq+pbfFKXMbL9nHEoZQT15Wyabd6DMAq3QZOthM5pUzdNqUXkcrI1PwjASAg4dVU/2o+98mTXnyhP7VlEkTnddpwyJF+YCCc01pqa6ZX99pnR+YOpIrb5DtPcKq0t5cFCDyQxN1lUuqZcYujvX/GC7+6IpRTz/NkEY/5cntOfZJyd6SkUx/JR5AJIRPCNeib0rcWnnFn0XK8s22x40wLjEtufXDO9+yraf4kHxlBI3SB0meMjepX50CJhAx7NekkRFnwoecLfqIyrjXrgAZsTMPiX94snjdrzisAXtBKanulilSxOfW2HSmfnk0L2GXHJJ3S+vCbgREml9dekfmHhmwEYtv3kfhth7ERIwVT8j69OtIBCmeTIJuA7o73PXc1LJ2whuZK7FZe6NTOs61MP7JVlIc6tbhtOBk3NXU8pBPOCY6Q55d63FtpUU/KCbQ0K2WQ9o5jNGwNNJ6Cw5FDCt+x5YJeP1d2QzDSiq+cSZqwqCZVcNoAftGGzgrRZ4vkpbP0xQZHCtWsUwHVTjrY4NfRDjHdVPRn8J9Xtooc/hIWkqzFaczYTlapu2RCJqe8aEo3jzWZCEJ7AiBzBYraYDgOfeiQyeJUsbbXHdGKiiw22Bdp4M+Yz671MZ0ie9EWRks0Q2DHGUTne9S4tAAJPGbUf4xtCMoJ40Q4QW05QLSZy3wNFAUFYmXE4ECDFB8+IEeNFFF/tgAnfh8GvFh/1xY+LE+ozMminjSRwhq09zQAEkNFyu0IKgvDkyOI+iccF7doWLTUasPxLH1xGPBSKkeWCnSoX/4C2c9du66BbRXamHqae+VCiS6BN953iYKOu8rMkneW+TFjV0CzAf8PNG9LBayUgY5Uw9x0Sap6zI4PpaZKH4d9ktkFU+91yBbN5SRtdd8/EQatJXmBNfw7kRrll+LjwZweWLEb0jT1RpGuDqHLdLTMMgowSczrp2zCsJdQH/seaYrpwkCFR8dRjM7JRUkVa1A0SvdZAX75vuBvvLBzN2c+Rbt5oqy93lqIBIjKb7w2UkRTb4pBWcNKNPUgt3az/PxQxEasaltdmuyvsXYbur3VPNn8JzN/6J5N+QCnz571TccapCmkPvFE+gayXU+UF1VKYyFcUvlev7lsQlmjqzK2QA8X7Q3TzFapqhvJKlt7dzBHraqZBgPgb9N4oKNayi8FZT71+kaYhIhFrESKYRHfHkj5n3kfhgLy+x+jLDbKxMggkgHnJz9RuEHZXBWtkYAQ==', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_verifier', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAMRdS21FeJfzQhM3s', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7396, 'output_tokens': 770, 'output_tokens_details': {'thinking_tokens': 739}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='skeptic', id='lc_run--01a0fe03-4ae9-76c0-89a6-4f0b114e3030-0', tool_calls=[{'name': 'transfer_to_verifier', 'args': {}, 'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7396, 'output_tokens': 770, 'total_tokens': 8166, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 739}}), ToolMessage(content='Successfully transferred to verifier', name='transfer_to_verifier', tool_call_id='toolu_01H6fV8JvRJNvXy47eRnd3jH')], 'active_agent': 'verifier'}, goto='verifier'))Traceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 709, in invoke\n input = step.invoke(input, config)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 447, in invoke\n ret = self.func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 84, in _write\n self.do_write(\n ~~~~~~~~~~~~~^\n config,\n ^^^^^^^\n writes,\n ^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 126, in do_write\n write(_assemble_writes(writes))\n ~~~~~~~~~~~~~~~~^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 181, in _assemble_writes\n if ww := w.mapper(w.value):\n ~~~~~~~~^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/graph/state.py\", line 1762, in _control_branch\n raise ParentCommand(command)\n\n\nlanggraph.errors.ParentCommand: Command(graph='__parent__', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', id='389058d1-083f-4632-bbc5-09bfd3a0ffd2', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9'), AIMessage(content=[{'signature': 'CAQS1QgKEAgSGAI4AUIIdGhpbmtpbmcSDN6TfU6NDM971lEbzxoMvRUA1J9ox3fJQYT9IjArCQQGHBP4pRHPavXzRlbMbaYHC57Yj+Gbxgr0helvd2OIL/o6Rbt4PFA3FaIFUlgq8gc7KQI9FtGnvSAkLBRzIsUp/2d5Rd2zgEsfkFVeXsn8WYYZkinAQVVy6A7B3LaEJW0xb/x88Di67fedwx0Q+FlZtwcmT3Dcaa11JveyHRr3dm+zGSZP6CXeXNJ8nrTZ7W1YRTysD6w7o7Lp8h1Rwq4f4o8j+QUfY0vje+zJZsxHPmQNt3ykeKh99hgn6dbiz6/EHqmbHjSAu1M4SqEDfqIKThHYenamAZmcQ4XAOo6mdQ6+77VgpV1lC1/k52IiUoJjkGlQrnLCvutiI7KN+sgK8c2+2KCJrPRCvVV+BkStvP5jWWqzytpKQ+nHzhL448OrIA9gdJC39C8eDrXeRnI7t4FqlkXGoMlZAV9UMcrQHRxOMi8tOyTzi9oyBFDTSsQ+LJqer0tz1Ft9gek6vzg50SpVAFQdLERv8R1YqMaeI+xsvTAEW9MRgvRMkzLuzRT2guw3jD+Fjip37xgK5QI0R+CsOAmQPoTZizzcIyAUAuCJFCgq/gD/CibLxoymiVUI1an9Snt3KpNRoWdvWIne33V0pv8PHg9yW2JuLOqQGilLCsK/Q3oQr3upEMXxXGjCL7WUizvRZGFEEV+VplYt7v4tKZMj5pk3vdeM0MJ5XZg7PvUsRzycxcnrT3Knoc3qM+ttqZfTsb8d6U/pQifq7GuahFOHrvroqgJLj+/81Yy1pZFkjfJgY0XklzAnA6CFq/YT0gxbV+TH5IS+WaYOUzK36S8s5qqMwhngv4IzE3ctXXqnhIZXNznyVBqbYa/Gx2bCjcEj2n5RyGYRS/+MculgLBznQ9MkG2D7QP3Mq/nwHXKMIaLS8VK8hG3d0rAjlvnauXShNrJxtkj4qkwnxD4LrEjk1ue+PLcbLqYWwAr8n0s+41jrsIPVyyK9vFaEZrA9KaBxkJyiqx50GZNn7ckx+mkI18gH52iyVPtU8FKMXjugCl6mPLiHYiDQd+G63LudmOKys9itVnBDBNMP2b/I7+bIVRlheGaZWr6MATKl86B3y+0kuOrLRMyWcdibc2iQkZm+8f7dTUwbKbBJsKOFY73/8nlL/JiYpjz6nUIk+IqSjaFyK6XFlCcukWpWHpDJnSvbEzey7qHyCqJ+I+on8iP8GOtempQaS0/4kZGSwlrE6xOj6EwGpfjbDZKHkP/2RAdNYPQq3z7bdVJMJNnFMz6e37mzMpCBihwPdSFTRE3SCY66wvTgJzGCUyhHbSi5i1azzOXd8uHCSpT/6L1o6VU0pjoR8Kaa4wCWHGf7P/WEd+avyp02Jnu1cmpW0+Gq+vK9h0tdb2IClM/eNfdouQP+ep8MJUjraDRICmAJQY5obS71vniOo+reaFy0zRgB', 'thinking': '', 'type': 'thinking'}, {'signature': 'CAQS/REKEQgSGAI4AUIJbmFycmF0aW9uEgwFh3AVJZP3s8y1Hq0aDG76uhHONaScXAGShiIwGgjLXHUx1yZrkLPljjzTOQycpLJiKVwXcxm/EzKJm1s8M5WxuREWEldXcoaN0/msKpkRMT+eLb5ih9rAHULTomUNsUwJeu0jrJe4GLazy7sP5KtjEOEBKQsXUfoQ7fDkZOQUHZi9zzQN4hFNmYJUf3OVqf9TQNdC1+m//HAZPfWmAy6HtDvm7kgBJkCEoiFnm7tn/PX1hXGPRgWc6BTXUMr2WA8aKyqggRUbMH2+JYv5iWZddCc1up1Nbr3sF7WPcot9ZBhyHQXr692zeJk6SJgygIVrASbMly2bBxTWJB623gVgzpCmVSLZFpkKXgPcp8hGyDKOvF6SiQSHqGRxQofr7Vs1aPhfZixROJGD+MlrkggVAnGMyYIDUZpavtCeiU1JmBSz0084bXPtT/tnpsKOnSPYZQo3B0BXKbqA/WsKng8/V3Z8Uuajo6IVZ2hgA1WDduqQOQkExGU6pFuLhebIjuWxSNI/DffvR//LNqCfvdnf0hvoPtDxfx/SawIx4/spq9xbwFzHtLgoHg1F2wFw7clPDlg1PyD/v2yJG3V9T8GL6jGPE6rhfil/u4HNvf7BW0dQzCTTXcqZy4nByy0+p+S7ZxfMl4Kpxa2gOhhSNjfpfuX0QpL4kME8J1nliFN1d8KqEPuDI18BQHUhD/UfY29oA0x510FjM/BsLZ4n5sppkoIi7CFoQq2aAKVNfC1KilhIpRD+1dyDtZQklN8gZpOESHw6ML1TtU0yfAcbmqoxJ6hhjzwH6CMsTQEEy8MbRBM+QKKkIRdRCPHGFW2LkuR50+J0pF/tr6pqVvJeozfiMnrEilB27TZP4nKTJG8rd//vCWen0y7CA/8dBJ9gdp5tx7NVA45k1gdcxER1gYouNX3n1vG3/B1F4+93wXW9Mqo6Us59TQmrb9Nqa7pSQNSrH06w1GxyBAlkFfqAlafk16aMpq416TJYpGy43WCUiCcLWlhiWgz2sgOLAXP9a5weWAKRC8J6jkcUkDeXnW+HFSiPM6+9Bo6kVhLUhN5rc4mHZOZpF7ptPpsQN4XCqLPsqpRH/8o5XpyC6ZneTeF2ExtjhwZVjXDwMbIzVYIbw1y2PPGw8dC7pc4sIVSnI4oQAH0v650ghfhmMevWW4h4Pylbw/DG4fdeT3+8z0f8MOSTtfipx05/QttHlAvODRxa5yDCvOz6n3f1BO0N/4MbGyOM27qW4x8mBVoryUZWfsmhfi0/GfNvB8+ylTGbYNX27OcEF5WZtNqbWEy7V5GbmZUSxspcg7FBZ3j3/3L7muvZXoZi1f5erq4y3lSH+oF7qPFnvieyLOCwubcQy+oyowj4cD0idRecnp+s+52D7+mzF1gpW+f+cJasoxigbxkOloS4yeiKvgM4nWSpGEj12RPx5o80EmziXN9t4zJzW1htA5hXJJeFympMYtyCUCevRWVmrRYktQ7UnCa6tL/AVPi1ni5XCXHfLOuqfK5KLYxjwVtdhhfmCLvpaKVNCEOCf4nNGHqD98wUb0ejWNwDYBnJGkBRw9RVspfwNdoWoVcn2qv8ywGUp7yc/sk2LpI6VziRyvWqbGNG1ZJqVVutRc46+sOE4VSeS2/CTNKNa6NMCf/hxxaUVsYfKdP+S1Lc4ianKp0sUugPpmtoydC0CP2RwjnHLcnzxCnvqxlLQWXMNPTsu2yBcHlWJLtcmdg/McLagy7l5kG6sN7ZxJLD/kAx8E6uqohP+K/jMSrYuQJnhr4tf4biWFY3Jzf/KpAYyVSq+pbfFKXMbL9nHEoZQT15Wyabd6DMAq3QZOthM5pUzdNqUXkcrI1PwjASAg4dVU/2o+98mTXnyhP7VlEkTnddpwyJF+YCCc01pqa6ZX99pnR+YOpIrb5DtPcKq0t5cFCDyQxN1lUuqZcYujvX/GC7+6IpRTz/NkEY/5cntOfZJyd6SkUx/JR5AJIRPCNeib0rcWnnFn0XK8s22x40wLjEtufXDO9+yraf4kHxlBI3SB0meMjepX50CJhAx7NekkRFnwoecLfqIyrjXrgAZsTMPiX94snjdrzisAXtBKanulilSxOfW2HSmfnk0L2GXHJJ3S+vCbgREml9dekfmHhmwEYtv3kfhth7ERIwVT8j69OtIBCmeTIJuA7o73PXc1LJ2whuZK7FZe6NTOs61MP7JVlIc6tbhtOBk3NXU8pBPOCY6Q55d63FtpUU/KCbQ0K2WQ9o5jNGwNNJ6Cw5FDCt+x5YJeP1d2QzDSiq+cSZqwqCZVcNoAftGGzgrRZ4vkpbP0xQZHCtWsUwHVTjrY4NfRDjHdVPRn8J9Xtooc/hIWkqzFaczYTlapu2RCJqe8aEo3jzWZCEJ7AiBzBYraYDgOfeiQyeJUsbbXHdGKiiw22Bdp4M+Yz671MZ0ie9EWRks0Q2DHGUTne9S4tAAJPGbUf4xtCMoJ40Q4QW05QLSZy3wNFAUFYmXE4ECDFB8+IEeNFFF/tgAnfh8GvFh/1xY+LE+ozMminjSRwhq09zQAEkNFyu0IKgvDkyOI+iccF7doWLTUasPxLH1xGPBSKkeWCnSoX/4C2c9du66BbRXamHqae+VCiS6BN953iYKOu8rMkneW+TFjV0CzAf8PNG9LBayUgY5Uw9x0Sap6zI4PpaZKH4d9ktkFU+91yBbN5SRtdd8/EQatJXmBNfw7kRrll+LjwZweWLEb0jT1RpGuDqHLdLTMMgowSczrp2zCsJdQH/seaYrpwkCFR8dRjM7JRUkVa1A0SvdZAX75vuBvvLBzN2c+Rbt5oqy93lqIBIjKb7w2UkRTb4pBWcNKNPUgt3az/PxQxEasaltdmuyvsXYbur3VPNn8JzN/6J5N+QCnz571TccapCmkPvFE+gayXU+UF1VKYyFcUvlev7lsQlmjqzK2QA8X7Q3TzFapqhvJKlt7dzBHraqZBgPgb9N4oKNayi8FZT71+kaYhIhFrESKYRHfHkj5n3kfhgLy+x+jLDbKxMggkgHnJz9RuEHZXBWtkYAQ==', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_verifier', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAMRdS21FeJfzQhM3s', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7396, 'output_tokens': 770, 'output_tokens_details': {'thinking_tokens': 739}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='skeptic', id='lc_run--01a0fe03-4ae9-76c0-89a6-4f0b114e3030-0', tool_calls=[{'name': 'transfer_to_verifier', 'args': {}, 'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7396, 'output_tokens': 770, 'total_tokens': 8166, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 739}}), ToolMessage(content='Successfully transferred to verifier', name='transfer_to_verifier', tool_call_id='toolu_01H6fV8JvRJNvXy47eRnd3jH')], 'active_agent': 'verifier'}, goto='verifier')" + } + }, + { + "key": "exception.stacktrace", + "value": { + "stringValue": "Exception: ParentCommand(Command(graph='__parent__', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', id='389058d1-083f-4632-bbc5-09bfd3a0ffd2', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9'), AIMessage(content=[{'signature': 'CAQS1QgKEAgSGAI4AUIIdGhpbmtpbmcSDN6TfU6NDM971lEbzxoMvRUA1J9ox3fJQYT9IjArCQQGHBP4pRHPavXzRlbMbaYHC57Yj+Gbxgr0helvd2OIL/o6Rbt4PFA3FaIFUlgq8gc7KQI9FtGnvSAkLBRzIsUp/2d5Rd2zgEsfkFVeXsn8WYYZkinAQVVy6A7B3LaEJW0xb/x88Di67fedwx0Q+FlZtwcmT3Dcaa11JveyHRr3dm+zGSZP6CXeXNJ8nrTZ7W1YRTysD6w7o7Lp8h1Rwq4f4o8j+QUfY0vje+zJZsxHPmQNt3ykeKh99hgn6dbiz6/EHqmbHjSAu1M4SqEDfqIKThHYenamAZmcQ4XAOo6mdQ6+77VgpV1lC1/k52IiUoJjkGlQrnLCvutiI7KN+sgK8c2+2KCJrPRCvVV+BkStvP5jWWqzytpKQ+nHzhL448OrIA9gdJC39C8eDrXeRnI7t4FqlkXGoMlZAV9UMcrQHRxOMi8tOyTzi9oyBFDTSsQ+LJqer0tz1Ft9gek6vzg50SpVAFQdLERv8R1YqMaeI+xsvTAEW9MRgvRMkzLuzRT2guw3jD+Fjip37xgK5QI0R+CsOAmQPoTZizzcIyAUAuCJFCgq/gD/CibLxoymiVUI1an9Snt3KpNRoWdvWIne33V0pv8PHg9yW2JuLOqQGilLCsK/Q3oQr3upEMXxXGjCL7WUizvRZGFEEV+VplYt7v4tKZMj5pk3vdeM0MJ5XZg7PvUsRzycxcnrT3Knoc3qM+ttqZfTsb8d6U/pQifq7GuahFOHrvroqgJLj+/81Yy1pZFkjfJgY0XklzAnA6CFq/YT0gxbV+TH5IS+WaYOUzK36S8s5qqMwhngv4IzE3ctXXqnhIZXNznyVBqbYa/Gx2bCjcEj2n5RyGYRS/+MculgLBznQ9MkG2D7QP3Mq/nwHXKMIaLS8VK8hG3d0rAjlvnauXShNrJxtkj4qkwnxD4LrEjk1ue+PLcbLqYWwAr8n0s+41jrsIPVyyK9vFaEZrA9KaBxkJyiqx50GZNn7ckx+mkI18gH52iyVPtU8FKMXjugCl6mPLiHYiDQd+G63LudmOKys9itVnBDBNMP2b/I7+bIVRlheGaZWr6MATKl86B3y+0kuOrLRMyWcdibc2iQkZm+8f7dTUwbKbBJsKOFY73/8nlL/JiYpjz6nUIk+IqSjaFyK6XFlCcukWpWHpDJnSvbEzey7qHyCqJ+I+on8iP8GOtempQaS0/4kZGSwlrE6xOj6EwGpfjbDZKHkP/2RAdNYPQq3z7bdVJMJNnFMz6e37mzMpCBihwPdSFTRE3SCY66wvTgJzGCUyhHbSi5i1azzOXd8uHCSpT/6L1o6VU0pjoR8Kaa4wCWHGf7P/WEd+avyp02Jnu1cmpW0+Gq+vK9h0tdb2IClM/eNfdouQP+ep8MJUjraDRICmAJQY5obS71vniOo+reaFy0zRgB', 'thinking': '', 'type': 'thinking'}, {'signature': 'CAQS/REKEQgSGAI4AUIJbmFycmF0aW9uEgwFh3AVJZP3s8y1Hq0aDG76uhHONaScXAGShiIwGgjLXHUx1yZrkLPljjzTOQycpLJiKVwXcxm/EzKJm1s8M5WxuREWEldXcoaN0/msKpkRMT+eLb5ih9rAHULTomUNsUwJeu0jrJe4GLazy7sP5KtjEOEBKQsXUfoQ7fDkZOQUHZi9zzQN4hFNmYJUf3OVqf9TQNdC1+m//HAZPfWmAy6HtDvm7kgBJkCEoiFnm7tn/PX1hXGPRgWc6BTXUMr2WA8aKyqggRUbMH2+JYv5iWZddCc1up1Nbr3sF7WPcot9ZBhyHQXr692zeJk6SJgygIVrASbMly2bBxTWJB623gVgzpCmVSLZFpkKXgPcp8hGyDKOvF6SiQSHqGRxQofr7Vs1aPhfZixROJGD+MlrkggVAnGMyYIDUZpavtCeiU1JmBSz0084bXPtT/tnpsKOnSPYZQo3B0BXKbqA/WsKng8/V3Z8Uuajo6IVZ2hgA1WDduqQOQkExGU6pFuLhebIjuWxSNI/DffvR//LNqCfvdnf0hvoPtDxfx/SawIx4/spq9xbwFzHtLgoHg1F2wFw7clPDlg1PyD/v2yJG3V9T8GL6jGPE6rhfil/u4HNvf7BW0dQzCTTXcqZy4nByy0+p+S7ZxfMl4Kpxa2gOhhSNjfpfuX0QpL4kME8J1nliFN1d8KqEPuDI18BQHUhD/UfY29oA0x510FjM/BsLZ4n5sppkoIi7CFoQq2aAKVNfC1KilhIpRD+1dyDtZQklN8gZpOESHw6ML1TtU0yfAcbmqoxJ6hhjzwH6CMsTQEEy8MbRBM+QKKkIRdRCPHGFW2LkuR50+J0pF/tr6pqVvJeozfiMnrEilB27TZP4nKTJG8rd//vCWen0y7CA/8dBJ9gdp5tx7NVA45k1gdcxER1gYouNX3n1vG3/B1F4+93wXW9Mqo6Us59TQmrb9Nqa7pSQNSrH06w1GxyBAlkFfqAlafk16aMpq416TJYpGy43WCUiCcLWlhiWgz2sgOLAXP9a5weWAKRC8J6jkcUkDeXnW+HFSiPM6+9Bo6kVhLUhN5rc4mHZOZpF7ptPpsQN4XCqLPsqpRH/8o5XpyC6ZneTeF2ExtjhwZVjXDwMbIzVYIbw1y2PPGw8dC7pc4sIVSnI4oQAH0v650ghfhmMevWW4h4Pylbw/DG4fdeT3+8z0f8MOSTtfipx05/QttHlAvODRxa5yDCvOz6n3f1BO0N/4MbGyOM27qW4x8mBVoryUZWfsmhfi0/GfNvB8+ylTGbYNX27OcEF5WZtNqbWEy7V5GbmZUSxspcg7FBZ3j3/3L7muvZXoZi1f5erq4y3lSH+oF7qPFnvieyLOCwubcQy+oyowj4cD0idRecnp+s+52D7+mzF1gpW+f+cJasoxigbxkOloS4yeiKvgM4nWSpGEj12RPx5o80EmziXN9t4zJzW1htA5hXJJeFympMYtyCUCevRWVmrRYktQ7UnCa6tL/AVPi1ni5XCXHfLOuqfK5KLYxjwVtdhhfmCLvpaKVNCEOCf4nNGHqD98wUb0ejWNwDYBnJGkBRw9RVspfwNdoWoVcn2qv8ywGUp7yc/sk2LpI6VziRyvWqbGNG1ZJqVVutRc46+sOE4VSeS2/CTNKNa6NMCf/hxxaUVsYfKdP+S1Lc4ianKp0sUugPpmtoydC0CP2RwjnHLcnzxCnvqxlLQWXMNPTsu2yBcHlWJLtcmdg/McLagy7l5kG6sN7ZxJLD/kAx8E6uqohP+K/jMSrYuQJnhr4tf4biWFY3Jzf/KpAYyVSq+pbfFKXMbL9nHEoZQT15Wyabd6DMAq3QZOthM5pUzdNqUXkcrI1PwjASAg4dVU/2o+98mTXnyhP7VlEkTnddpwyJF+YCCc01pqa6ZX99pnR+YOpIrb5DtPcKq0t5cFCDyQxN1lUuqZcYujvX/GC7+6IpRTz/NkEY/5cntOfZJyd6SkUx/JR5AJIRPCNeib0rcWnnFn0XK8s22x40wLjEtufXDO9+yraf4kHxlBI3SB0meMjepX50CJhAx7NekkRFnwoecLfqIyrjXrgAZsTMPiX94snjdrzisAXtBKanulilSxOfW2HSmfnk0L2GXHJJ3S+vCbgREml9dekfmHhmwEYtv3kfhth7ERIwVT8j69OtIBCmeTIJuA7o73PXc1LJ2whuZK7FZe6NTOs61MP7JVlIc6tbhtOBk3NXU8pBPOCY6Q55d63FtpUU/KCbQ0K2WQ9o5jNGwNNJ6Cw5FDCt+x5YJeP1d2QzDSiq+cSZqwqCZVcNoAftGGzgrRZ4vkpbP0xQZHCtWsUwHVTjrY4NfRDjHdVPRn8J9Xtooc/hIWkqzFaczYTlapu2RCJqe8aEo3jzWZCEJ7AiBzBYraYDgOfeiQyeJUsbbXHdGKiiw22Bdp4M+Yz671MZ0ie9EWRks0Q2DHGUTne9S4tAAJPGbUf4xtCMoJ40Q4QW05QLSZy3wNFAUFYmXE4ECDFB8+IEeNFFF/tgAnfh8GvFh/1xY+LE+ozMminjSRwhq09zQAEkNFyu0IKgvDkyOI+iccF7doWLTUasPxLH1xGPBSKkeWCnSoX/4C2c9du66BbRXamHqae+VCiS6BN953iYKOu8rMkneW+TFjV0CzAf8PNG9LBayUgY5Uw9x0Sap6zI4PpaZKH4d9ktkFU+91yBbN5SRtdd8/EQatJXmBNfw7kRrll+LjwZweWLEb0jT1RpGuDqHLdLTMMgowSczrp2zCsJdQH/seaYrpwkCFR8dRjM7JRUkVa1A0SvdZAX75vuBvvLBzN2c+Rbt5oqy93lqIBIjKb7w2UkRTb4pBWcNKNPUgt3az/PxQxEasaltdmuyvsXYbur3VPNn8JzN/6J5N+QCnz571TccapCmkPvFE+gayXU+UF1VKYyFcUvlev7lsQlmjqzK2QA8X7Q3TzFapqhvJKlt7dzBHraqZBgPgb9N4oKNayi8FZT71+kaYhIhFrESKYRHfHkj5n3kfhgLy+x+jLDbKxMggkgHnJz9RuEHZXBWtkYAQ==', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_verifier', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAMRdS21FeJfzQhM3s', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7396, 'output_tokens': 770, 'output_tokens_details': {'thinking_tokens': 739}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='skeptic', id='lc_run--01a0fe03-4ae9-76c0-89a6-4f0b114e3030-0', tool_calls=[{'name': 'transfer_to_verifier', 'args': {}, 'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7396, 'output_tokens': 770, 'total_tokens': 8166, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 739}}), ToolMessage(content='Successfully transferred to verifier', name='transfer_to_verifier', tool_call_id='toolu_01H6fV8JvRJNvXy47eRnd3jH')], 'active_agent': 'verifier'}, goto='verifier'))Traceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 709, in invoke\n input = step.invoke(input, config)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 447, in invoke\n ret = self.func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 84, in _write\n self.do_write(\n ~~~~~~~~~~~~~^\n config,\n ^^^^^^^\n writes,\n ^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 126, in do_write\n write(_assemble_writes(writes))\n ~~~~~~~~~~~~~~~~^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 181, in _assemble_writes\n if ww := w.mapper(w.value):\n ~~~~~~~~^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/graph/state.py\", line 1762, in _control_branch\n raise ParentCommand(command)\n\n\nlanggraph.errors.ParentCommand: Command(graph='__parent__', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', id='389058d1-083f-4632-bbc5-09bfd3a0ffd2', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9'), AIMessage(content=[{'signature': 'CAQS1QgKEAgSGAI4AUIIdGhpbmtpbmcSDN6TfU6NDM971lEbzxoMvRUA1J9ox3fJQYT9IjArCQQGHBP4pRHPavXzRlbMbaYHC57Yj+Gbxgr0helvd2OIL/o6Rbt4PFA3FaIFUlgq8gc7KQI9FtGnvSAkLBRzIsUp/2d5Rd2zgEsfkFVeXsn8WYYZkinAQVVy6A7B3LaEJW0xb/x88Di67fedwx0Q+FlZtwcmT3Dcaa11JveyHRr3dm+zGSZP6CXeXNJ8nrTZ7W1YRTysD6w7o7Lp8h1Rwq4f4o8j+QUfY0vje+zJZsxHPmQNt3ykeKh99hgn6dbiz6/EHqmbHjSAu1M4SqEDfqIKThHYenamAZmcQ4XAOo6mdQ6+77VgpV1lC1/k52IiUoJjkGlQrnLCvutiI7KN+sgK8c2+2KCJrPRCvVV+BkStvP5jWWqzytpKQ+nHzhL448OrIA9gdJC39C8eDrXeRnI7t4FqlkXGoMlZAV9UMcrQHRxOMi8tOyTzi9oyBFDTSsQ+LJqer0tz1Ft9gek6vzg50SpVAFQdLERv8R1YqMaeI+xsvTAEW9MRgvRMkzLuzRT2guw3jD+Fjip37xgK5QI0R+CsOAmQPoTZizzcIyAUAuCJFCgq/gD/CibLxoymiVUI1an9Snt3KpNRoWdvWIne33V0pv8PHg9yW2JuLOqQGilLCsK/Q3oQr3upEMXxXGjCL7WUizvRZGFEEV+VplYt7v4tKZMj5pk3vdeM0MJ5XZg7PvUsRzycxcnrT3Knoc3qM+ttqZfTsb8d6U/pQifq7GuahFOHrvroqgJLj+/81Yy1pZFkjfJgY0XklzAnA6CFq/YT0gxbV+TH5IS+WaYOUzK36S8s5qqMwhngv4IzE3ctXXqnhIZXNznyVBqbYa/Gx2bCjcEj2n5RyGYRS/+MculgLBznQ9MkG2D7QP3Mq/nwHXKMIaLS8VK8hG3d0rAjlvnauXShNrJxtkj4qkwnxD4LrEjk1ue+PLcbLqYWwAr8n0s+41jrsIPVyyK9vFaEZrA9KaBxkJyiqx50GZNn7ckx+mkI18gH52iyVPtU8FKMXjugCl6mPLiHYiDQd+G63LudmOKys9itVnBDBNMP2b/I7+bIVRlheGaZWr6MATKl86B3y+0kuOrLRMyWcdibc2iQkZm+8f7dTUwbKbBJsKOFY73/8nlL/JiYpjz6nUIk+IqSjaFyK6XFlCcukWpWHpDJnSvbEzey7qHyCqJ+I+on8iP8GOtempQaS0/4kZGSwlrE6xOj6EwGpfjbDZKHkP/2RAdNYPQq3z7bdVJMJNnFMz6e37mzMpCBihwPdSFTRE3SCY66wvTgJzGCUyhHbSi5i1azzOXd8uHCSpT/6L1o6VU0pjoR8Kaa4wCWHGf7P/WEd+avyp02Jnu1cmpW0+Gq+vK9h0tdb2IClM/eNfdouQP+ep8MJUjraDRICmAJQY5obS71vniOo+reaFy0zRgB', 'thinking': '', 'type': 'thinking'}, {'signature': 'CAQS/REKEQgSGAI4AUIJbmFycmF0aW9uEgwFh3AVJZP3s8y1Hq0aDG76uhHONaScXAGShiIwGgjLXHUx1yZrkLPljjzTOQycpLJiKVwXcxm/EzKJm1s8M5WxuREWEldXcoaN0/msKpkRMT+eLb5ih9rAHULTomUNsUwJeu0jrJe4GLazy7sP5KtjEOEBKQsXUfoQ7fDkZOQUHZi9zzQN4hFNmYJUf3OVqf9TQNdC1+m//HAZPfWmAy6HtDvm7kgBJkCEoiFnm7tn/PX1hXGPRgWc6BTXUMr2WA8aKyqggRUbMH2+JYv5iWZddCc1up1Nbr3sF7WPcot9ZBhyHQXr692zeJk6SJgygIVrASbMly2bBxTWJB623gVgzpCmVSLZFpkKXgPcp8hGyDKOvF6SiQSHqGRxQofr7Vs1aPhfZixROJGD+MlrkggVAnGMyYIDUZpavtCeiU1JmBSz0084bXPtT/tnpsKOnSPYZQo3B0BXKbqA/WsKng8/V3Z8Uuajo6IVZ2hgA1WDduqQOQkExGU6pFuLhebIjuWxSNI/DffvR//LNqCfvdnf0hvoPtDxfx/SawIx4/spq9xbwFzHtLgoHg1F2wFw7clPDlg1PyD/v2yJG3V9T8GL6jGPE6rhfil/u4HNvf7BW0dQzCTTXcqZy4nByy0+p+S7ZxfMl4Kpxa2gOhhSNjfpfuX0QpL4kME8J1nliFN1d8KqEPuDI18BQHUhD/UfY29oA0x510FjM/BsLZ4n5sppkoIi7CFoQq2aAKVNfC1KilhIpRD+1dyDtZQklN8gZpOESHw6ML1TtU0yfAcbmqoxJ6hhjzwH6CMsTQEEy8MbRBM+QKKkIRdRCPHGFW2LkuR50+J0pF/tr6pqVvJeozfiMnrEilB27TZP4nKTJG8rd//vCWen0y7CA/8dBJ9gdp5tx7NVA45k1gdcxER1gYouNX3n1vG3/B1F4+93wXW9Mqo6Us59TQmrb9Nqa7pSQNSrH06w1GxyBAlkFfqAlafk16aMpq416TJYpGy43WCUiCcLWlhiWgz2sgOLAXP9a5weWAKRC8J6jkcUkDeXnW+HFSiPM6+9Bo6kVhLUhN5rc4mHZOZpF7ptPpsQN4XCqLPsqpRH/8o5XpyC6ZneTeF2ExtjhwZVjXDwMbIzVYIbw1y2PPGw8dC7pc4sIVSnI4oQAH0v650ghfhmMevWW4h4Pylbw/DG4fdeT3+8z0f8MOSTtfipx05/QttHlAvODRxa5yDCvOz6n3f1BO0N/4MbGyOM27qW4x8mBVoryUZWfsmhfi0/GfNvB8+ylTGbYNX27OcEF5WZtNqbWEy7V5GbmZUSxspcg7FBZ3j3/3L7muvZXoZi1f5erq4y3lSH+oF7qPFnvieyLOCwubcQy+oyowj4cD0idRecnp+s+52D7+mzF1gpW+f+cJasoxigbxkOloS4yeiKvgM4nWSpGEj12RPx5o80EmziXN9t4zJzW1htA5hXJJeFympMYtyCUCevRWVmrRYktQ7UnCa6tL/AVPi1ni5XCXHfLOuqfK5KLYxjwVtdhhfmCLvpaKVNCEOCf4nNGHqD98wUb0ejWNwDYBnJGkBRw9RVspfwNdoWoVcn2qv8ywGUp7yc/sk2LpI6VziRyvWqbGNG1ZJqVVutRc46+sOE4VSeS2/CTNKNa6NMCf/hxxaUVsYfKdP+S1Lc4ianKp0sUugPpmtoydC0CP2RwjnHLcnzxCnvqxlLQWXMNPTsu2yBcHlWJLtcmdg/McLagy7l5kG6sN7ZxJLD/kAx8E6uqohP+K/jMSrYuQJnhr4tf4biWFY3Jzf/KpAYyVSq+pbfFKXMbL9nHEoZQT15Wyabd6DMAq3QZOthM5pUzdNqUXkcrI1PwjASAg4dVU/2o+98mTXnyhP7VlEkTnddpwyJF+YCCc01pqa6ZX99pnR+YOpIrb5DtPcKq0t5cFCDyQxN1lUuqZcYujvX/GC7+6IpRTz/NkEY/5cntOfZJyd6SkUx/JR5AJIRPCNeib0rcWnnFn0XK8s22x40wLjEtufXDO9+yraf4kHxlBI3SB0meMjepX50CJhAx7NekkRFnwoecLfqIyrjXrgAZsTMPiX94snjdrzisAXtBKanulilSxOfW2HSmfnk0L2GXHJJ3S+vCbgREml9dekfmHhmwEYtv3kfhth7ERIwVT8j69OtIBCmeTIJuA7o73PXc1LJ2whuZK7FZe6NTOs61MP7JVlIc6tbhtOBk3NXU8pBPOCY6Q55d63FtpUU/KCbQ0K2WQ9o5jNGwNNJ6Cw5FDCt+x5YJeP1d2QzDSiq+cSZqwqCZVcNoAftGGzgrRZ4vkpbP0xQZHCtWsUwHVTjrY4NfRDjHdVPRn8J9Xtooc/hIWkqzFaczYTlapu2RCJqe8aEo3jzWZCEJ7AiBzBYraYDgOfeiQyeJUsbbXHdGKiiw22Bdp4M+Yz671MZ0ie9EWRks0Q2DHGUTne9S4tAAJPGbUf4xtCMoJ40Q4QW05QLSZy3wNFAUFYmXE4ECDFB8+IEeNFFF/tgAnfh8GvFh/1xY+LE+ozMminjSRwhq09zQAEkNFyu0IKgvDkyOI+iccF7doWLTUasPxLH1xGPBSKkeWCnSoX/4C2c9du66BbRXamHqae+VCiS6BN953iYKOu8rMkneW+TFjV0CzAf8PNG9LBayUgY5Uw9x0Sap6zI4PpaZKH4d9ktkFU+91yBbN5SRtdd8/EQatJXmBNfw7kRrll+LjwZweWLEb0jT1RpGuDqHLdLTMMgowSczrp2zCsJdQH/seaYrpwkCFR8dRjM7JRUkVa1A0SvdZAX75vuBvvLBzN2c+Rbt5oqy93lqIBIjKb7w2UkRTb4pBWcNKNPUgt3az/PxQxEasaltdmuyvsXYbur3VPNn8JzN/6J5N+QCnz571TccapCmkPvFE+gayXU+UF1VKYyFcUvlev7lsQlmjqzK2QA8X7Q3TzFapqhvJKlt7dzBHraqZBgPgb9N4oKNayi8FZT71+kaYhIhFrESKYRHfHkj5n3kfhgLy+x+jLDbKxMggkgHnJz9RuEHZXBWtkYAQ==', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_verifier', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAMRdS21FeJfzQhM3s', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7396, 'output_tokens': 770, 'output_tokens_details': {'thinking_tokens': 739}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='skeptic', id='lc_run--01a0fe03-4ae9-76c0-89a6-4f0b114e3030-0', tool_calls=[{'name': 'transfer_to_verifier', 'args': {}, 'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7396, 'output_tokens': 770, 'total_tokens': 8166, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 739}}), ToolMessage(content='Successfully transferred to verifier', name='transfer_to_verifier', tool_call_id='toolu_01H6fV8JvRJNvXy47eRnd3jH')], 'active_agent': 'verifier'}, goto='verifier')\n" + } + }, + { + "key": "exception.escaped", + "value": { + "stringValue": "False" + } + } + ] + } + ], + "status": { + "code": 2 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "3ed306e7faadc279", + "parentSpanId": "3049adb48476557c", + "name": "FilesystemMiddleware.wrap_tool_call", + "kind": 1, + "startTimeUnixNano": "1790968030836527104", + "endTimeUnixNano": "1790968030838146048", + "attributes": [ + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "FilesystemMiddleware.wrap_tool_call" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "2" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "tools" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"__pregel_push\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_push\",0,false]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "skeptic:3dfc4328-0d19-e1ea-bcda-3d3d36d7cda8|tools:e52223df-38bd-4e1b-4faf-e72de38488d4" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "skeptic" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "skeptic:3dfc4328-0d19-e1ea-bcda-3d3d36d7cda8" + } + }, + { + "key": "langsmith.metadata.ls_method", + "value": { + "stringValue": "traceable" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "e30=" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJvdXRwdXQiOnsiZ3JhcGgiOiJfX3BhcmVudF9fIiwidXBkYXRlIjp7Im1lc3NhZ2VzIjpbeyJjb250ZW50IjoiRm9yIGEgc3ludGhldGljIHRyYWNpbmcgZml4dHVyZSwgZXhwbGFpbiBQeXRob24gdHVwbGUgdmVyc3VzIGxpc3QgaW1tdXRhYmlsaXR5IGluIGF0IG1vc3QgMTIwIHdvcmRzLiBVc2Ugb25seSBvZmZpY2lhbCBQeXRob24gZG9jdW1lbnRhdGlvbiBpZiB5b3Ugc2VhcmNoLiBIYXZlIHRoZSBza2VwdGljIGFuZCB2ZXJpZmllciBjaGVjayBpdCwgdGhlbiBoYXZlIHRoZSBlZGl0b3Igd3JpdGUgL2Fuc3dlci5tZC4gS2VlcCB0aGUgcnVuIHNob3J0IGFuZCBkbyBub3QgYXNrIGZvbGxvdy11cCBxdWVzdGlvbnMuIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJodW1hbiIsImlkIjoiNDYyMDQzMDUtZjhlZS00Mzg0LTljZTgtZmNkODBiZmNkZjc1In0seyJjb250ZW50IjpbeyJpZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7InF1ZXJ5IjoiZG9jcy5weXRob24ub3JnIHR1cGxlcyBpbW11dGFibGUgc2VxdWVuY2VzIGxpc3RzIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIHR1dG9yaWFsIn0sIm5hbWUiOiJ3ZWJfc2VhcmNoIiwidHlwZSI6InRvb2xfdXNlIiwidG9vbHNldF9uYW1lIjpudWxsfV0sImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2ZkdzlhVzlicmVuUHFneWJmTndQIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjI4NDUsIm91dHB1dF90b2tlbnMiOjczLCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjowfSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCIsIm1vZGVsX3Byb3ZpZGVyIjoiYW50aHJvcGljIn0sInR5cGUiOiJhaSIsIm5hbWUiOiJyZXNlYXJjaGVyIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTIxYTMtNzAwMC1hMzgxLTI4NGE2YTBjMDNmNy0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6IndlYl9zZWFyY2giLCJhcmdzIjp7InF1ZXJ5IjoiZG9jcy5weXRob24ub3JnIHR1cGxlcyBpbW11dGFibGUgc2VxdWVuY2VzIGxpc3RzIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIHR1dG9yaWFsIn0sImlkIjoidG9vbHVfMDFCMkxmSEdEallTRkpobWlYTmo0Q3VRIiwidHlwZSI6InRvb2xfY2FsbCJ9XSwiaW52YWxpZF90b29sX2NhbGxzIjpbXSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjoyODQ1LCJvdXRwdXRfdG9rZW5zIjo3MywidG90YWxfdG9rZW5zIjoyOTE4LCJpbnB1dF90b2tlbl9kZXRhaWxzIjp7ImNhY2hlX3JlYWQiOjAsImNhY2hlX2NyZWF0aW9uIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VuX2RldGFpbHMiOnsicmVhc29uaW5nIjowfX19LHsiY29udGVudCI6Ilt7XCJ0aXRsZVwiOiBcIjUuIERhdGEgU3RydWN0dXJlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiWW91IG1pZ2h0IGhhdmUgbm90aWNlZCB0aGF0IG1ldGhvZHMgbGlrZWBpbnNlcnRgLGByZW1vdmVgIG9yYHNvcnRgIHRoYXQgb25seSBtb2RpZnkgdGhlIGxpc3QgaGF2ZSBubyByZXR1cm4gdmFsdWUgcHJpbnRlZCBcXHUyMDEzIHRoZXkgcmV0dXJuIHRoZSBkZWZhdWx0YE5vbmVgLiBbMV0gVGhpcyBpcyBhIGRlc2lnbiBwcmluY2lwbGUgZm9yIGFsbCBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyBpbiBQeXRob24uXFxuLi4uXFxuIyMgNS4zLiBUdXBsZXMgYW5kIFNlcXVlbmNlc1xcdTAwYjZcXG5cXG5XZSBzYXcgdGhhdCBsaXN0cyBhbmQgc3RyaW5ncyBoYXZlIG1hbnkgY29tbW9uIHByb3BlcnRpZXMsIHN1Y2ggYXMgaW5kZXhpbmcgYW5kIHNsaWNpbmcgb3BlcmF0aW9ucy4gVGhleSBhcmUgdHdvIGV4YW1wbGVzIG9mIHNlcXVlbmNlIGRhdGEgdHlwZXMgKHNlZSBTZXF1ZW5jZSBUeXBlcyBcXHUyMDE0IGxpc3QsIHR1cGxlLCByYW5nZSkuIFNpbmNlIFB5dGhvbiBpcyBhbiBldm9sdmluZyBsYW5ndWFnZSwgb3RoZXIgc2VxdWVuY2UgZGF0YSB0eXBlcyBtYXkgYmUgYWRkZWQuIFRoZXJlIGlzIGFsc28gYW5vdGhlciBzdGFuZGFyZCBzZXF1ZW5jZSBkYXRhIHR5cGU6IHRoZSB0dXBsZS5cXG5cXG5BIHR1cGxlIGNvbnNpc3RzIG9mIGEgbnVtYmVyIG9mIHZhbHVlcyBzZXBhcmF0ZWQgYnkgY29tbWFzLCBmb3IgaW5zdGFuY2U6XFxuLi4uXFxuPj4+ICMgVHVwbGVzIGFyZSBpbW11dGFibGU6XFxuPj4+IHRbMF0gPSA4ODg4OFxcblRyYWNlYmFjayAobW9zdCByZWNlbnQgY2FsbCBsYXN0KTpcXG4gIEZpbGUgXFxcIjxzdGRpbj5cXFwiLCBsaW5lIDEsIGluIDxtb2R1bGU+XFxuVHlwZUVycm9yOiAndHVwbGUnIG9iamVjdCBkb2VzIG5vdCBzdXBwb3J0IGl0ZW0gYXNzaWdubWVudFxcbj4+PiAjIGJ1dCB0aGV5IGNhbiBjb250YWluIG11dGFibGUgb2JqZWN0czpcXG4+Pj4gdiA9IChbMSwgMiwgM10sIFszLCAyLCAxXSlcXG4+Pj4gdlxcbihbMSwgMiwgM10sIFszLCAyLCAxXSlcXG4uLi5cXG5BcyB5b3Ugc2VlLCBvbiBvdXRwdXQgdHVwbGVzIGFyZSBhbHdheXMgZW5jbG9zZWQgaW4gcGFyZW50aGVzZXMsIHNvIHRoYXQgbmVzdGVkIHR1cGxlcyBhcmUgaW50ZXJwcmV0ZWQgY29ycmVjdGx5OyB0aGV5IG1heSBiZSBpbnB1dCB3aXRoIG9yIHdpdGhvdXQgc3Vycm91bmRpbmcgcGFyZW50aGVzZXMsIGFsdGhvdWdoIG9mdGVuIHBhcmVudGhlc2VzIGFyZSBuZWNlc3NhcnkgYW55d2F5IChpZiB0aGUgdHVwbGUgaXMgcGFydCBvZiBhIGxhcmdlciBleHByZXNzaW9uKS4gSXQgaXMgbm90IHBvc3NpYmxlIHRvIGFzc2lnbiB0byB0aGUgaW5kaXZpZHVhbCBpdGVtcyBvZiBhIHR1cGxlLCBob3dldmVyIGl0IGlzIHBvc3NpYmxlIHRvIGNyZWF0ZSB0dXBsZXMgd2hpY2ggY29udGFpbiBtdXRhYmxlIG9iamVjdHMsIHN1Y2ggYXMgbGlzdHMuXFxuXFxuVGhvdWdoIHR1cGxlcyBtYXkgc2VlbSBzaW1pbGFyIHRvIGxpc3RzLCB0aGV5IGFyZSBvZnRlbiB1c2VkIGluIGRpZmZlcmVudCBzaXR1YXRpb25zIGFuZCBmb3IgZGlmZmVyZW50IHB1cnBvc2VzLiBUdXBsZXMgYXJlIGltbXV0YWJsZSwgYW5kIHVzdWFsbHkgY29udGFpbiBhIGhldGVyb2dlbmVvdXMgc2VxdWVuY2Ugb2YgZWxlbWVudHMgdGhhdCBhcmUgYWNjZXNzZWQgdmlhIHVucGFja2luZyAoc2VlIGxhdGVyIGluIHRoaXMgc2VjdGlvbikgb3IgaW5kZXhpbmcgKG9yIGV2ZW4gYnkgYXR0cmlidXRlIGluIHRoZSBjYXNlIG9mIG5hbWVkdHVwbGVzKS4gTGlzdHMgYXJlIG11dGFibGUsIGFuZCB0aGVpciBlbGVtZW50cyBhcmUgdXN1YWxseSBob21vZ2VuZW91cyBhbmQgYXJlIGFjY2Vzc2VkIGJ5IGl0ZXJhdGluZyBvdmVyIHRoZSBsaXN0Llxcbi4uLlxcbkFub3RoZXIgdXNlZnVsIGRhdGEgdHlwZSBidWlsdCBpbnRvIFB5dGhvbiBpcyB0aGUgZGljdGlvbmFyeSAoc2VlIE1hcHBpbmcgVHlwZXMgXFx1MjAxNCBkaWN0KS4gRGljdGlvbmFyaWVzIGFyZSBzb21ldGltZXMgZm91bmQgaW4gb3RoZXIgbGFuZ3VhZ2VzIGFzIFxcdTIwMWNhc3NvY2lhdGl2ZSBtZW1vcmllc1xcdTIwMWQgb3IgXFx1MjAxY2Fzc29jaWF0aXZlIGFycmF5c1xcdTIwMWQuIFVubGlrZSBzZXF1ZW5jZXMsIHdoaWNoIGFyZSBpbmRleGVkIGJ5IGEgcmFuZ2Ugb2YgbnVtYmVycywgZGljdGlvbmFyaWVzIGFyZSBpbmRleGVkIGJ5IGtleXMsIHdoaWNoIGNhbiBiZSBhbnkgaW1tdXRhYmxlIHR5cGU7IHN0cmluZ3MgYW5kIG51bWJlcnMgY2FuIGFsd2F5cyBiZSBrZXlzLiBUdXBsZXMgY2FuIGJlIHVzZWQgYXMga2V5cyBpZiB0aGV5IGNvbnRhaW4gb25seSBzdHJpbmdzLCBudW1iZXJzLCBvciB0dXBsZXM7IGlmIGEgdHVwbGUgY29udGFpbnMgYW55IG11dGFibGUgb2JqZWN0IGVpdGhlciBkaXJlY3RseSBvciBpbmRpcmVjdGx5LCBpdCBjYW5ub3QgYmUgdXNlZCBhcyBhIGtleS4gWW91IGNhblxcdTIwMTl0IHVzZSBsaXN0cyBhcyBrZXlzLCBzaW5jZSBsaXN0cyBjYW4gYmUgbW9kaWZpZWQgaW4gcGxhY2UgdXNpbmcgaW5kZXggYXNzaWdubWVudHMsIHNsaWNlIGFzc2lnbm1lbnRzLCBvciBtZXRob2RzIGxpa2UgYXBwZW5kKCkgYW5kIGV4dGVuZCgpLlwiXX0sIHtcInRpdGxlXCI6IFwiMy4gRGF0YSBtb2RlbCBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvcmVmZXJlbmNlL2RhdGFtb2RlbC5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJUaGUgdmFsdWUgb2Ygc29tZSBvYmplY3RzIGNhbiBjaGFuZ2UuIE9iamVjdHMgd2hvc2UgdmFsdWUgY2FuIGNoYW5nZSBhcmUgc2FpZCB0byBiZSBtdXRhYmxlOyBvYmplY3RzIHdob3NlIHZhbHVlIGlzIHVuY2hhbmdlYWJsZSBvbmNlIHRoZXkgYXJlIGNyZWF0ZWQgYXJlIGNhbGxlZCBpbW11dGFibGUuIChUaGUgdmFsdWUgb2YgYW4gaW1tdXRhYmxlIGNvbnRhaW5lciBvYmplY3QgdGhhdCBjb250YWlucyBhIHJlZmVyZW5jZSB0byBhIG11dGFibGUgb2JqZWN0IGNhbiBjaGFuZ2Ugd2hlbiB0aGUgbGF0dGVyXFx1MjAxOXMgdmFsdWUgaXMgY2hhbmdlZDsgaG93ZXZlciB0aGUgY29udGFpbmVyIGlzIHN0aWxsIGNvbnNpZGVyZWQgaW1tdXRhYmxlLCBiZWNhdXNlIHRoZSBjb2xsZWN0aW9uIG9mIG9iamVjdHMgaXQgY29udGFpbnMgY2Fubm90IGJlIGNoYW5nZWQuIFNvLCBpbW11dGFiaWxpdHkgaXMgbm90IHN0cmljdGx5IHRoZSBzYW1lIGFzIGhhdmluZyBhbiB1bmNoYW5nZWFibGUgdmFsdWUsIGl0IGlzIG1vcmUgc3VidGxlLikgQW4gb2JqZWN0XFx1MjAxOXMgbXV0YWJpbGl0eSBpcyBkZXRlcm1pbmVkIGJ5IGl0cyB0eXBlOyBmb3IgaW5zdGFuY2UsIG51bWJlcnMsIHN0cmluZ3MgYW5kIHR1cGxlcyBhcmUgaW1tdXRhYmxlLCB3aGlsZSBkaWN0aW9uYXJpZXMgYW5kIGxpc3RzIGFyZSBtdXRhYmxlLlxcbi4uLlxcblNvbWUgb2JqZWN0cyBjb250YWluIHJlZmVyZW5jZXMgdG8gb3RoZXIgb2JqZWN0czsgdGhlc2UgYXJlIGNhbGxlZCBjb250YWluZXJzLiBFeGFtcGxlcyBvZiBjb250YWluZXJzIGFyZSB0dXBsZXMsIGxpc3RzIGFuZCBkaWN0aW9uYXJpZXMuIFRoZSByZWZlcmVuY2VzIGFyZSBwYXJ0IG9mIGEgY29udGFpbmVyXFx1MjAxOXMgdmFsdWUuIEluIG1vc3QgY2FzZXMsIHdoZW4gd2UgdGFsayBhYm91dCB0aGUgdmFsdWUgb2YgYSBjb250YWluZXIsIHdlIGltcGx5IHRoZSB2YWx1ZXMsIG5vdCB0aGUgaWRlbnRpdGllcyBvZiB0aGUgY29udGFpbmVkIG9iamVjdHM7IGhvd2V2ZXIsIHdoZW4gd2UgdGFsayBhYm91dCB0aGUgbXV0YWJpbGl0eSBvZiBhIGNvbnRhaW5lciwgb25seSB0aGUgaWRlbnRpdGllcyBvZiB0aGUgaW1tZWRpYXRlbHkgY29udGFpbmVkIG9iamVjdHMgYXJlIGltcGxpZWQuIFNvLCBpZiBhbiBpbW11dGFibGUgY29udGFpbmVyIChsaWtlIGEgdHVwbGUpIGNvbnRhaW5zIGEgcmVmZXJlbmNlIHRvIGEgbXV0YWJsZSBvYmplY3QsIGl0cyB2YWx1ZSBjaGFuZ2VzIGlmIHRoYXQgbXV0YWJsZSBvYmplY3QgaXMgY2hhbmdlZC5cXG4uLi5cXG4jIyMgMy4yLjUuIFNlcXVlbmNlc1xcdTAwYjZcXG4uLi5cXG4jIyMjIDMuMi41LjEuIEltbXV0YWJsZSBzZXF1ZW5jZXNcXHUwMGI2XFxuXFxuQW4gb2JqZWN0IG9mIGFuIGltbXV0YWJsZSBzZXF1ZW5jZSB0eXBlIGNhbm5vdCBjaGFuZ2Ugb25jZSBpdCBpcyBjcmVhdGVkLiAoSWYgdGhlIG9iamVjdCBjb250YWlucyByZWZlcmVuY2VzIHRvIG90aGVyIG9iamVjdHMsIHRoZXNlIG90aGVyIG9iamVjdHMgbWF5IGJlIG11dGFibGUgYW5kIG1heSBiZSBjaGFuZ2VkOyBob3dldmVyLCB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGRpcmVjdGx5IHJlZmVyZW5jZWQgYnkgYW4gaW1tdXRhYmxlIG9iamVjdCBjYW5ub3QgY2hhbmdlLilcXG5cXG5UaGUgZm9sbG93aW5nIHR5cGVzIGFyZSBpbW11dGFibGUgc2VxdWVuY2VzOlxcbi4uLlxcblR1cGxlc1xcbjogVGhlIGl0ZW1zIG9mIGEgYHR1cGxlYCBhcmUgYXJiaXRyYXJ5IFB5dGhvbiBvYmplY3RzLiBUdXBsZXMgb2YgdHdvIG9yIG1vcmUgaXRlbXMgYXJlIGZvcm1lZCBieSBjb21tYS1zZXBhcmF0ZWQgbGlzdHMgb2YgZXhwcmVzc2lvbnMuIEEgdHVwbGUgb2Ygb25lIGl0ZW0gKGEgXFx1MjAxOHNpbmdsZXRvblxcdTIwMTkpIGNhbiBiZSBmb3JtZWQgYnkgYWZmaXhpbmcgYSBjb21tYSB0byBhbiBleHByZXNzaW9uIChhbiBleHByZXNzaW9uIGJ5IGl0c2VsZiBkb2VzIG5vdCBjcmVhdGUgYSB0dXBsZSwgc2luY2UgcGFyZW50aGVzZXMgbXVzdCBiZSB1c2FibGUgZm9yIGdyb3VwaW5nIG9mIGV4cHJlc3Npb25zKS4gQW4gZW1wdHkgdHVwbGUgY2FuIGJlIGZvcm1lZCBieSBhbiBlbXB0eSBwYWlyIG9mIHBhcmVudGhlc2VzLlxcbi4uLlxcbiMjIyMgMy4yLjUuMi4gTXV0YWJsZSBzZXF1ZW5jZXNcXHUwMGI2XFxuXFxuTXV0YWJsZSBzZXF1ZW5jZXMgY2FuIGJlIGNoYW5nZWQgYWZ0ZXIgdGhleSBhcmUgY3JlYXRlZC4gVGhlIHN1YnNjcmlwdGlvbiBhbmQgc2xpY2luZyBub3RhdGlvbnMgY2FuIGJlIHVzZWQgYXMgdGhlIHRhcmdldCBvZiBhc3NpZ25tZW50IGFuZCBgZGVsYCAoZGVsZXRlKSBzdGF0ZW1lbnRzLlxcbi4uLlxcblRoZXJlIGFyZSBjdXJyZW50bHkgdHdvIGludHJpbnNpYyBtdXRhYmxlIHNlcXVlbmNlIHR5cGVzOlxcblxcbkxpc3RzXFxuOiBUaGUgaXRlbXMgb2YgYSBsaXN0IGFyZSBhcmJpdHJhcnkgUHl0aG9uIG9iamVjdHMuIExpc3RzIGFyZSBmb3JtZWQgYnkgcGxhY2luZyBhIGNvbW1hLXNlcGFyYXRlZCBsaXN0IG9mIGV4cHJlc3Npb25zIGluIHNxdWFyZSBicmFja2V0cy4gKE5vdGUgdGhhdCB0aGVyZSBhcmUgbm8gc3BlY2lhbCBjYXNlcyBuZWVkZWQgdG8gZm9ybSBsaXN0cyBvZiBsZW5ndGggMCBvciAxLilcIl19LCB7XCJ0aXRsZVwiOiBcIkJ1aWx0LWluIFR5cGVzIFxcdTIwMTQgUHl0aG9uIDMuMTQuNyBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9idWlsdGlucy9zdGR0eXBlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJjb2xsZWN0aW9uIGNsYXNzZXMgYXJlIG11dGFibGUuIFRoZSBtZXRob2RzIHRoYXQgYWRkLCBzdWJ0cmFjdCwgb3IgcmVhcnJhbmdlIHRoZWlyIC4uLiBpbiBwbGFjZSwgYW5kIGRvblxcdTIwMTl0IHJldHVybiBhIC4uLiAsIG5ldmVyIHJldHVybiB0aGUgY29sbGVjdGlvbiBpbnN0YW5jZSBpdHNlbGYgYnV0IGBOb25lYC5cXG4uLi5cXG4jIyBTZXF1ZW5jZSBUeXBlcyBcXHUyMDE0IGBsaXN0YCwgYHR1cGxlYCwgYHJhbmdlYFxcdTAwYjZcXG5cXG5UaGVyZSBhcmUgdGhyZWUgYmFzaWMgc2VxdWVuY2UgdHlwZXM6IGxpc3RzLCB0dXBsZXMsIGFuZCByYW5nZSBvYmplY3RzLiBBZGRpdGlvbmFsIHNlcXVlbmNlIHR5cGVzIHRhaWxvcmVkIGZvciBwcm9jZXNzaW5nIG9mIGJpbmFyeSBkYXRhIGFuZCB0ZXh0IHN0cmluZ3MgYXJlIGRlc2NyaWJlZCBpbiBkZWRpY2F0ZWQgc2VjdGlvbnMuXFxuLi4uXFxuVGhlIG9wZXJhdGlvbnMgaW4gdGhlIGZvbGxvd2luZyB0YWJsZSAuLi4gbXV0YWJsZSBhbmQgaW1tdXRhYmxlLiBUaGUgYGNvbGxlY3Rpb25zLiAuLi4gaXMgcHJvdmlkZWQgdG8gbWFrZSAuLi4gZWFzaWVyIHRvIGNvcnJlY3RseSBpbXBsZW1lbnQgdGhlc2Ugb3BlcmF0aW9ucyBvbiBjdXN0b20gc2VxdWVuY2UgdHlwZXNcXG4uLi5cXG4jIyMgSW1tdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbm11dGFibGUgc2VxdWVuY2UgdHlwZXMgaXNcXG4uLi5cXG5zdXBwb3J0IGFsbG93cyBpbW11dGFibGUgc2VxdWVuY2VzLCAuLi4gLCB0byBiZSB1c2VkIGFzIGBkaWN0YCBrZXlzIGFuZCBzdG9yZWQgaW4gLi4uIGVuc2V0YCBpbnN0YW5jZXNcXG4uLi5cXG4jIyMgTXV0YWJsZSBTZXF1ZW5jZSBUeXBlc1xcdTAwYjZcXG4uLi5cXG4jIyMgTGlzdHNcXHUwMGI2XFxuXFxuTGlzdHMgYXJlIG11dGFibGUgc2VxdWVuY2VzLCB0eXBpY2FsbHkgdXNlZCB0byBzdG9yZSBjb2xsZWN0aW9ucyBvZiBob21vZ2VuZW91cyBpdGVtcyAod2hlcmUgdGhlIHByZWNpc2UgZGVncmVlIG9mIHNpbWlsYXJpdHkgd2lsbCB2YXJ5IGJ5IGFwcGxpY2F0aW9uKS5cXG4uLi5cXG4jIyMgVHVwbGVzXFx1MDBiNlxcblxcblR1cGxlcyBhcmUgaW1tdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaGV0ZXJvZ2VuZW91cyBkYXRhIChzdWNoIGFzIHRoZSAyLXR1cGxlcyBwcm9kdWNlZCBieSB0aGUgYGVudW1lcmF0ZSgpYCBidWlsdC1pbikuIFR1cGxlcyBhcmUgYWxzbyB1c2VkIGZvciBjYXNlcyB3aGVyZSBhbiBpbW11dGFibGUgc2VxdWVuY2Ugb2YgaG9tb2dlbmVvdXMgZGF0YSBpcyBuZWVkZWQgKHN1Y2ggYXMgYWxsb3dpbmcgc3RvcmFnZSBpbiBhIGBzZXRgIG9yIGBkaWN0YCBpbnN0YW5jZSkuXFxuLi4uXFxudHVwbGUoaXRlcmFibGU9XFxuLi4uXFxuVGhlIGNvbnN0cnVjdG9yIGJ1aWxkcyBhIHR1cGxlIHdob3NlIGl0ZW1zIGFyZSB0aGUgc2FtZSBhbmQgaW4gdGhlIHNhbWUgb3JkZXIgYXMgaXRlcmFibGVcXHUyMDE5cyBpdGVtcy4gaXRlcmFibGUgbWF5IGJlIGVpdGhlciBhIHNlcXVlbmNlLCBhIGNvbnRhaW5lciB0aGF0IHN1cHBvcnRzIGl0ZXJhdGlvbiwgb3IgYW4gaXRlcmF0b3Igb2JqZWN0LiBJZiBpdGVyYWJsZSBpcyBhbHJlYWR5IGEgdHVwbGUsIGl0IGlzIHJldHVybmVkIHVuY2hhbmdlZC4gRm9yIGV4YW1wbGUsIGB0dXBsZSgnYWJjJylgIHJldHVybnMgYCgnYScsICdiJywgJ2MnKWAgYW5kIGB0dXBsZSggWzEsIDIsIDNdIClgIHJldHVybnMgYCgxLCAyLCAzKWAuIElmIG5vIGFyZ3VtZW50IGlzIGdpdmVuLCB0aGUgY29uc3RydWN0b3IgY3JlYXRlcyBhIG5ldyBlbXB0eSB0dXBsZSwgYCgpYC5cXG4uLi5cXG5UdXBsZXMgaW1wbGVtZW50IGFsbCBvZiB0aGUgY29tbW9uIHNlcXVlbmNlIG9wZXJhdGlvbnMuXFxuLi4uXFxuRm9yIGhldGVyb2dlbmVvdXMgY29sbGVjdGlvbnMgb2YgZGF0YSB3aGVyZSBhY2Nlc3MgYnkgbmFtZSBpcyBjbGVhcmVyIHRoYW4gYWNjZXNzIGJ5IGluZGV4LCBgY29sbGVjdGlvbnMubmFtZWR0dXBsZSgpYCBtYXkgYmUgYSBtb3JlIGFwcHJvcHJpYXRlIGNob2ljZSB0aGFuIGEgc2ltcGxlIHR1cGxlIG9iamVjdC5cIl19LCB7XCJ0aXRsZVwiOiBcIkJ1aWx0LWluIFR5cGVzIFxcdTIwMTQgUHl0aG9uIDMuMTQuNSBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9saWJyYXJ5L3N0ZHR5cGVzLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcIlNvbWUgY29sbGVjdGlvbiBjbGFzc2VzIGFyZSBtdXRhYmxlLiBUaGUgbWV0aG9kcyB0aGF0IGFkZCwgc3VidHJhY3QsIG9yIHJlYXJyYW5nZSB0aGVpciAuLi4gaW4gcGxhY2UsIGFuZCBkb25cXHUyMDE5dCByZXR1cm4gYSAuLi4gLCBuZXZlciByZXR1cm4gdGhlIGNvbGxlY3Rpb24gaW5zdGFuY2UgaXRzZWxmIGJ1dCBgTm9uZWAuXFxuLi4uXFxuIyMgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBgbGlzdGAsIGB0dXBsZWAsIGByYW5nZWBcXHUwMGI2XFxuXFxuVGhlcmUgYXJlIHRocmVlIGJhc2ljIHNlcXVlbmNlIHR5cGVzOiBsaXN0cywgdHVwbGVzLCBhbmQgLi4uIG9iamVjdHMuIEFkZGl0aW9uYWwgc2VxdWVuY2UgdHlwZXMgdGFpbG9yZWQgZm9yIHByb2Nlc3Npbmcgb2YgYmluYXJ5IGRhdGEgYW5kIHRleHQgc3RyaW5ncyBhcmUgZGVzY3JpYmVkIGluIGRlZGljYXRlZCBzZWN0aW9ucy5cXG4uLi5cXG5UaGUgb3BlcmF0aW9ucyBpbiB0aGUgZm9sbG93aW5nIHRhYmxlIC4uLiBpbW11dGFibGUuIFRoZSBgIC4uLiBpcyBwcm92aWRlZCB0byBtYWtlIC4uLiBlYXNpZXIgdG8gY29ycmVjdGx5IGltcGxlbWVudCB0aGVzZSBvcGVyYXRpb25zIG9uIGN1c3RvbSBzZXF1ZW5jZSB0eXBlc1xcbi4uLlxcbiMjIyBJbW11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxub3BlcmF0aW9uIHRoYXQgaW1tdXRhYmxlIHNlcXVlbmNlIC4uLiBieSBtdXRhYmxlIHNlcXVlbmNlIHR5cGVzIGlzIC4uLiBgaGFzaCgpYFxcbi4uLlxcblRoaXMgc3VwcG9ydCBhbGxvd3MgaW1tdXRhYmxlIHNlcXVlbmNlcywgc3VjaCBhcyBgdHVwbGVgIGluc3RhbmNlcywgdG8gYmUgdXNlZCBhcyBgZGljdGAga2V5cyBhbmQgc3RvcmVkIGluIGBzZXRgIGFuZCBgZnJvemVuc2V0YCBpbnN0YW5jZXMuXFxuLi4uXFxuIyMjIE11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxuIyMjIExpc3RzXFx1MDBiNlxcblxcbkxpc3RzIGFyZSBtdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaG9tb2dlbmVvdXMgaXRlbXMgKHdoZXJlIHRoZSBwcmVjaXNlIGRlZ3JlZSBvZiBzaW1pbGFyaXR5IHdpbGwgdmFyeSBieSBhcHBsaWNhdGlvbikuXFxuLi4uXFxuIyMjIFR1cGxlc1xcdTAwYjZcXG5cXG5UdXBsZXMgYXJlIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhldGVyb2dlbmVvdXMgZGF0YSAoc3VjaCBhcyB0aGUgMi10dXBsZXMgcHJvZHVjZWQgYnkgdGhlIGBlbnVtZXJhdGUoKWAgYnVpbHQtaW4pLiBUdXBsZXMgYXJlIGFsc28gdXNlZCBmb3IgY2FzZXMgd2hlcmUgYW4gaW1tdXRhYmxlIHNlcXVlbmNlIG9mIGhvbW9nZW5lb3VzIGRhdGEgaXMgbmVlZGVkIChzdWNoIGFzIGFsbG93aW5nIHN0b3JhZ2UgaW4gYSBgc2V0YCBvciBgZGljdGAgaW5zdGFuY2UpLlxcbi4uLlxcblRoZSBjb25zdHJ1Y3RvciBidWlsZHMgYSB0dXBsZSB3aG9zZSBpdGVtcyBhcmUgdGhlIHNhbWUgYW5kIGluIHRoZSBzYW1lIG9yZGVyIGFzIGl0ZXJhYmxlXFx1MjAxOXMgaXRlbXMuIGl0ZXJhYmxlIG1heSBiZSBlaXRoZXIgYSBzZXF1ZW5jZSwgYSBjb250YWluZXIgdGhhdCBzdXBwb3J0cyBpdGVyYXRpb24sIG9yIGFuIGl0ZXJhdG9yIG9iamVjdC4gSWYgaXRlcmFibGUgaXMgYWxyZWFkeSBhIHR1cGxlLCBpdCBpcyByZXR1cm5lZCB1bmNoYW5nZWQuIEZvciBleGFtcGxlLCBgdHVwbGUoJ2FiYycpYCByZXR1cm5zIGAoJ2EnLCAnYicsICdjJylgIGFuZCBgdHVwbGUoIFsxLCAyLCAzXSApYCByZXR1cm5zIGAoMSwgMiwgMylgLiBJZiBubyBhcmd1bWVudCBpcyBnaXZlbiwgdGhlIGNvbnN0cnVjdG9yIGNyZWF0ZXMgYSBuZXcgZW1wdHkgdHVwbGUsIGAoKWAuXFxuLi4uXFxuVHVwbGVzIGltcGxlbWVudCBhbGwgb2YgdGhlIGNvbW1vbiBzZXF1ZW5jZSBvcGVyYXRpb25zLlxcbi4uLlxcbkZvciBoZXRlcm9nZW5lb3VzIGNvbGxlY3Rpb25zIG9mIGRhdGEgd2hlcmUgYWNjZXNzIGJ5IG5hbWUgaXMgY2xlYXJlciB0aGFuIGFjY2VzcyBieSBpbmRleCwgYGNvbGxlY3Rpb25zLm5hbWVkdHVwbGUoKWAgbWF5IGJlIGEgbW9yZSBhcHByb3ByaWF0ZSBjaG9pY2UgdGhhbiBhIHNpbXBsZSB0dXBsZSBvYmplY3QuXCJdfSwge1widGl0bGVcIjogXCI1LiBEYXRhIFN0cnVjdHVyZXMgXFx1MjAxNCBQeXRob24gMy4xMC4yMCBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy4xMC90dXRvcmlhbC9kYXRhc3RydWN0dXJlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJZb3UgbWlnaHQgaGF2ZSBub3RpY2VkIHRoYXQgbWV0aG9kcyBsaWtlIGBpbnNlcnRgLCBgcmVtb3ZlYCBvciBgc29ydGAgdGhhdCBvbmx5IG1vZGlmeSB0aGUgbGlzdCBoYXZlIG5vIHJldHVybiB2YWx1ZSBwcmludGVkIFxcdTIwMTMgdGhleSByZXR1cm4gdGhlIGRlZmF1bHQgYE5vbmVgLiAxIFRoaXMgaXMgYSBkZXNpZ24gcHJpbmNpcGxlIGZvciBhbGwgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgaW4gUHl0aG9uLlxcbi4uLlxcbiMjIDUuMy4gVHVwbGVzIGFuZCBTZXF1ZW5jZXNcXHUwMGI2XFxuXFxuV2Ugc2F3IHRoYXQgbGlzdHMgYW5kIHN0cmluZ3MgaGF2ZSBtYW55IGNvbW1vbiBwcm9wZXJ0aWVzLCBzdWNoIGFzIGluZGV4aW5nIGFuZCBzbGljaW5nIG9wZXJhdGlvbnMuIFRoZXkgYXJlIHR3byBleGFtcGxlcyBvZiBzZXF1ZW5jZSBkYXRhIHR5cGVzIChzZWUgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBsaXN0LCB0dXBsZSwgcmFuZ2UpLiBTaW5jZSBQeXRob24gaXMgYW4gZXZvbHZpbmcgbGFuZ3VhZ2UsIG90aGVyIHNlcXVlbmNlIGRhdGEgdHlwZXMgbWF5IGJlIGFkZGVkLiBUaGVyZSBpcyBhbHNvIGFub3RoZXIgc3RhbmRhcmQgc2VxdWVuY2UgZGF0YSB0eXBlOiB0aGUgdHVwbGUuXFxuXFxuQSB0dXBsZSBjb25zaXN0cyBvZiBhIG51bWJlciBvZiB2YWx1ZXMgc2VwYXJhdGVkIGJ5IGNvbW1hcywgZm9yIGluc3RhbmNlOlxcbi4uLlxcbj4+PiAjIFR1cGxlcyBhcmUgaW1tdXRhYmxlOlxcbi4uLiB0WzBdID0gODg4ODhcXG5UcmFjZWJhY2sgKG1vc3QgcmVjZW50IGNhbGwgbGFzdCk6XFxuICBGaWxlIFxcXCI8c3RkaW4+XFxcIiwgbGluZSAxLCBpbiA8bW9kdWxlPlxcblR5cGVFcnJvcjogJ3R1cGxlJyBvYmplY3QgZG9lcyBub3Qgc3VwcG9ydCBpdGVtIGFzc2lnbm1lbnRcXG4+Pj4gIyBidXQgdGhleSBjYW4gY29udGFpbiBtdXRhYmxlIG9iamVjdHM6XFxuLi4uIHYgPSAoWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuPj4+IHZcXG4oWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuLi4uXFxuQXMgeW91IHNlZSwgb24gb3V0cHV0IHR1cGxlcyBhcmUgYWx3YXlzIGVuY2xvc2VkIGluIHBhcmVudGhlc2VzLCBzbyB0aGF0IG5lc3RlZCB0dXBsZXMgYXJlIGludGVycHJldGVkIGNvcnJlY3RseTsgdGhleSBtYXkgYmUgaW5wdXQgd2l0aCBvciB3aXRob3V0IHN1cnJvdW5kaW5nIHBhcmVudGhlc2VzLCBhbHRob3VnaCBvZnRlbiBwYXJlbnRoZXNlcyBhcmUgbmVjZXNzYXJ5IGFueXdheSAoaWYgdGhlIHR1cGxlIGlzIHBhcnQgb2YgYSBsYXJnZXIgZXhwcmVzc2lvbikuIEl0IGlzIG5vdCBwb3NzaWJsZSB0byBhc3NpZ24gdG8gdGhlIGluZGl2aWR1YWwgaXRlbXMgb2YgYSB0dXBsZSwgaG93ZXZlciBpdCBpcyBwb3NzaWJsZSB0byBjcmVhdGUgdHVwbGVzIHdoaWNoIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzLCBzdWNoIGFzIGxpc3RzLlxcblxcblRob3VnaCB0dXBsZXMgbWF5IHNlZW0gc2ltaWxhciB0byBsaXN0cywgdGhleSBhcmUgb2Z0ZW4gdXNlZCBpbiBkaWZmZXJlbnQgc2l0dWF0aW9ucyBhbmQgZm9yIGRpZmZlcmVudCBwdXJwb3Nlcy4gVHVwbGVzIGFyZSBpbW11dGFibGUsIGFuZCB1c3VhbGx5IGNvbnRhaW4gYSBoZXRlcm9nZW5lb3VzIHNlcXVlbmNlIG9mIGVsZW1lbnRzIHRoYXQgYXJlIGFjY2Vzc2VkIHZpYSB1bnBhY2tpbmcgKHNlZSBsYXRlciBpbiB0aGlzIHNlY3Rpb24pIG9yIGluZGV4aW5nIChvciBldmVuIGJ5IGF0dHJpYnV0ZSBpbiB0aGUgY2FzZSBvZiBgbmFtZWR0dXBsZXNgKS4gTGlzdHMgYXJlIG11dGFibGUsIGFuZCB0aGVpciBlbGVtZW50cyBhcmUgdXN1YWxseSBob21vZ2VuZW91cyBhbmQgYXJlIGFjY2Vzc2VkIGJ5IGl0ZXJhdGluZyBvdmVyIHRoZSBsaXN0Llxcbi4uLlxcbiMjIDUuNC5cXG4uLi5cXG4uIFNldCBvYmplY3RzXFxuLi4uXFxuIyMgNS41LiBEaWN0aW9uYXJpZXNcXHUwMGI2XFxuXFxuQW5vdGhlciB1c2VmdWwgZGF0YSB0eXBlIGJ1aWx0IGludG8gUHl0aG9uIGlzIHRoZSBkaWN0aW9uYXJ5IChzZWUgTWFwcGluZyBUeXBlcyBcXHUyMDE0IGRpY3QpLiBEaWN0aW9uYXJpZXMgYXJlIHNvbWV0aW1lcyBmb3VuZCBpbiBvdGhlciBsYW5ndWFnZXMgYXMgXFx1MjAxY2Fzc29jaWF0aXZlIG1lbW9yaWVzXFx1MjAxZCBvciBcXHUyMDFjYXNzb2NpYXRpdmUgYXJyYXlzXFx1MjAxZC4gVW5saWtlIHNlcXVlbmNlcywgd2hpY2ggYXJlIGluZGV4ZWQgYnkgYSByYW5nZSBvZiBudW1iZXJzLCBkaWN0aW9uYXJpZXMgYXJlIGluZGV4ZWQgYnkga2V5cywgd2hpY2ggY2FuIGJlIGFueSBpbW11dGFibGUgdHlwZTsgc3RyaW5ncyBhbmQgbnVtYmVycyBjYW4gYWx3YXlzIGJlIGtleXMuIFR1cGxlcyBjYW4gYmUgdXNlZCBhcyBrZXlzIGlmIHRoZXkgY29udGFpbiBvbmx5IHN0cmluZ3MsIG51bWJlcnMsIG9yIHR1cGxlczsgaWYgYSB0dXBsZSBjb250YWlucyBhbnkgbXV0YWJsZSBvYmplY3QgZWl0aGVyIGRpcmVjdGx5IG9yIGluZGlyZWN0bHksIGl0IGNhbm5vdCBiZSB1c2VkIGFzIGEga2V5LiBZb3UgY2FuXFx1MjAxOXQgdXNlIGxpc3RzIGFzIGtleXMsIHNpbmNlIGxpc3RzIGNhbiBiZSBtb2RpZmllZCBpbiBwbGFjZSB1c2luZyBpbmRleCBhc3NpZ25tZW50cywgc2xpY2UgYXNzaWdubWVudHMsIG9yIG1ldGhvZHMgbGlrZSBgYXBwZW5kKClgIGFuZCBgZXh0ZW5kKClgLlwiXX1dIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJ0b29sIiwibmFtZSI6IndlYl9zZWFyY2giLCJpZCI6IjZlNTEyNWFjLTMxNjQtNGE3Ni05YzUyLWFjMzQ2MGNlMzZlNCIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsInN0YXR1cyI6InN1Y2Nlc3MifSx7ImNvbnRlbnQiOlt7ImlkIjoidG9vbHVfMDFYamozYVJkV3loQkpmUG1USkJ4UWE5IiwiY2FsbGVyIjp7InR5cGUiOiJkaXJlY3QifSwiaW5wdXQiOnt9LCJuYW1lIjoidHJhbnNmZXJfdG9fc2tlcHRpYyIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHdBNkY0Y0NONXloRDRDcnM5TSIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo3Mzc4LCJvdXRwdXRfdG9rZW5zIjozMywib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6MH0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoicmVzZWFyY2hlciIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy0zY2Q4LTc5ZjEtODhhMS1jZTZjZWUyOTRlNzktMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ0cmFuc2Zlcl90b19za2VwdGljIiwiYXJncyI6e30sImlkIjoidG9vbHVfMDFYamozYVJkV3loQkpmUG1USkJ4UWE5IiwidHlwZSI6InRvb2xfY2FsbCJ9XSwiaW52YWxpZF90b29sX2NhbGxzIjpbXSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjo3Mzc4LCJvdXRwdXRfdG9rZW5zIjozMywidG90YWxfdG9rZW5zIjo3NDExLCJpbnB1dF90b2tlbl9kZXRhaWxzIjp7ImNhY2hlX3JlYWQiOjAsImNhY2hlX2NyZWF0aW9uIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VuX2RldGFpbHMiOnsicmVhc29uaW5nIjowfX19LHsiY29udGVudCI6IlN1Y2Nlc3NmdWxseSB0cmFuc2ZlcnJlZCB0byBza2VwdGljIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJ0b29sIiwibmFtZSI6InRyYW5zZmVyX3RvX3NrZXB0aWMiLCJpZCI6IjM4OTA1OGQxLTA4M2YtNDYzMi1iYmM1LTA5YmZkM2EwZmZkMiIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsInN0YXR1cyI6InN1Y2Nlc3MifSx7ImNvbnRlbnQiOlt7InNpZ25hdHVyZSI6IkNBUVMxUWdLRUFnU0dBSTRBVUlJZEdocGJtdHBibWNTRE42VGZVNk5ETTk3MWxFYnp4b012UlVBMUo5b3gzZkpRWVQ5SWpBckNRUUdIQlA0cFJIUGF2WHpSbGJNYmFZSEM1N1lqK0dieGdyMGhlbHZkMk9JTC9vNlJidDRQRkEzRmFJRlVsZ3E4Z2M3S1FJOUZ0R252U0FrTEJSeklzVXAvMmQ1UmQyemdFc2ZrRlZlWHNuOFdZWVpraW5BUVZWeTZBN0IzTGFFSlcweGIveDg4RGk2N2ZlZHd4MFErRmxadHdjbVQzRGNhYTExSnZleUhScjNkbSt6R1NaUDZDWGVYTko4bnJUWjdXMVlSVHlzRDZ3N283THA4aDFSd3E0ZjRvOGorUVVmWTB2amUrekpac3hIUG1RTnQzeWtlS2g5OWhnbjZkYml6Ni9FSHFtYkhqU0F1MU00U3FFRGZxSUtUaEhZZW5hbUFabWNRNFhBT282bWRRNis3N1ZncFYxbEMxL2s1MklpVW9KamtHbFFybkxDdnV0aUk3S04rc2dLOGMyKzJLQ0pyUFJDdlZWK0JrU3R2UDVqV1dxenl0cEtRK25IemhMNDQ4T3JJQTlnZEpDMzlDOGVEclhlUm5JN3Q0RnFsa1hHb01sWkFWOVVNY3JRSFJ4T01pOHRPeVR6aTlveUJGRFRTc1ErTEpxZXIwdHoxRnQ5Z2VrNnZ6ZzUwU3BWQUZRZExFUnY4UjFZcU1hZUkreHN2VEFFVzlNUmd2Uk1rekx1elJUMmd1dzNqRCtGamlwMzd4Z0s1UUkwUitDc09BbVFQb1RaaXp6Y0l5QVVBdUNKRkNncS9nRC9DaWJMeG95bWlWVUkxYW45U250M0twTlJvV2R2V0luZTMzVjBwdjhQSGc5eVcySnVMT3FRR2lsTENzSy9RM29RcjN1cEVNWHhYR2pDTDdXVWl6dlJaR0ZFRVYrVnBsWXQ3djR0S1pNajVwazN2ZGVNME1KNVhaZzdQdlVzUnp5Y3hjbnJUM0tub2MzcU0rdHRxWmZUc2I4ZDZVL3BRaWZxN0d1YWhGT0hydnJvcWdKTGorLzgxWXkxcFpGa2pmSmdZMFhrbHpBbkE2Q0ZxL1lUMGd4YlYrVEg1SVMrV2FZT1V6SzM2UzhzNXFxTXdobmd2NEl6RTNjdFhYcW5oSVpYTnpueVZCcWJZYS9HeDJiQ2pjRWoybjVSeUdZUlMvK01jdWxnTEJ6blE5TWtHMkQ3UVAzTXEvbndIWEtNSWFMUzhWSzhoRzNkMHJBamx2bmF1WFNoTnJKeHRrajRxa3dueEQ0THJFamsxdWUrUExjYkxxWVd3QXI4bjBzKzQxanJzSVBWeXlLOXZGYUVackE5S2FCeGtKeWlxeDUwR1pObjdja3grbWtJMThnSDUyaXlWUHRVOEZLTVhqdWdDbDZtUExpSFlpRFFkK0c2M0x1ZG1PS3lzOWl0Vm5CREJOTVAyYi9JNytiSVZSbGhlR2FaV3I2TUFUS2w4NkIzeSswa3VPckxSTXlXY2RpYmMyaVFrWm0rOGY3ZFRVd2JLYkJKc0tPRlk3My84bmxML0ppWXBqejZuVUlrK0lxU2phRnlLNlhGbENjdWtXcFdIcERKblN2YkV6ZXk3cUh5Q3FKK0krb244aVA4R090ZW1wUWFTMC80a1pHU3dsckU2eE9qNkV3R3BmamJEWktIa1AvMlJBZE5ZUFFxM3o3YmRWSk1KTm5GTXo2ZTM3bXpNcENCaWh3UGRTRlRSRTNTQ1k2Nnd2VGdKekdDVXloSGJTaTVpMWF6ek9YZDh1SENTcFQvNkwxbzZWVTBwam9SOEthYTR3Q1dIR2Y3UC9XRWQrYXZ5cDAySm51MWNtcFcwK0dxK3ZLOWgwdGRiMklDbE0vZU5mZG91UVArZXA4TUpVanJhRFJJQ21BSlFZNW9iUzcxdm5pT28rcmVhRnkwelJnQiIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJzaWduYXR1cmUiOiJDQVFTL1JFS0VRZ1NHQUk0QVVJSmJtRnljbUYwYVc5dUVnd0ZoM0FWSlpQM3M4eTFIcTBhREc3NnVoSE9OYVNjWEFHU2hpSXdHZ2pMWEhVeDF5WnJrTFBsamp6VE9ReWNwTEppS1Z3WGN4bS9FektKbTFzOE01V3h1UkVXRWxkWGNvYU4wL21zS3BrUk1UK2VMYjVpaDlyQUhVTFRvbVVOc1V3SmV1MGpySmU0R0xhenk3c1A1S3RqRU9FQktRc1hVZm9RN2ZEa1pPUVVIWmk5enpRTjRoRk5tWUpVZjNPVnFmOVRRTmRDMSttLy9IQVpQZldtQXk2SHREdm03a2dCSmtDRW9pRm5tN3RuL1BYMWhYR1BSZ1djNkJUWFVNcjJXQThhS3lxZ2dSVWJNSDIrSll2NWlXWmRkQ2MxdXAxTmJyM3NGN1dQY290OVpCaHlIUVhyNjkyemVKazZTSmd5Z0lWckFTYk1seTJiQnhUV0pCNjIzZ1ZnenBDbVZTTFpGcGtLWGdQY3A4aEd5REtPdkY2U2lRU0hxR1J4UW9mcjdWczFhUGhmWml4Uk9KR0QrTWxya2dnVkFuR015WUlEVVpwYXZ0Q2VpVTFKbUJTejAwODRiWFB0VC90bnBzS09uU1BZWlFvM0IwQlhLYnFBL1dzS25nOC9WM1o4VXVham82SVZaMmhnQTFXRGR1cVFPUWtFeEdVNnBGdUxoZWJJanVXeFNOSS9EZmZ2Ui8vTE5xQ2Z2ZG5mMGh2b1B0RHhmeC9TYXdJeDQvc3BxOXhid0Z6SHRMZ29IZzFGMndGdzdjbFBEbGcxUHlEL3YyeUpHM1Y5VDhHTDZqR1BFNnJoZmlsL3U0SE52ZjdCVzBkUXpDVFRYY3FaeTRuQnl5MCtwK1M3WnhmTWw0S3B4YTJnT2hoU05qZnBmdVgwUXBMNGtNRThKMW5saUZOMWQ4S3FFUHVESTE4QlFIVWhEL1VmWTI5b0EweDUxMEZqTS9Cc0xaNG41c3Bwa29JaTdDRm9RcTJhQUtWTmZDMUtpbGhJcFJEKzFkeUR0WlFrbE44Z1pwT0VTSHc2TUwxVHRVMHlmQWNibXFveEo2aGhqendINkNNc1RRRUV5OE1iUkJNK1FLS2tJUmRSQ1BIR0ZXMkxrdVI1MCtKMHBGL3RyNnBxVnZKZW96ZmlNbnJFaWxCMjdUWlA0bktUSkc4cmQvL3ZDV2VuMHk3Q0EvOGRCSjlnZHA1dHg3TlZBNDVrMWdkY3hFUjFnWW91TlgzbjF2RzMvQjFGNCs5M3dYVzlNcW82VXM1OVRRbXJiOU5xYTdwU1FOU3JIMDZ3MUd4eUJBbGtGZnFBbGFmazE2YU1wcTQxNlRKWXBHeTQzV0NVaUNjTFdsaGlXZ3oyc2dPTEFYUDlhNXdlV0FLUkM4SjZqa2NVa0RlWG5XK0hGU2lQTTYrOUJvNmtWaExVaE41cmM0bUhaT1pwRjdwdFBwc1FONFhDcUxQc3FwUkgvOG81WHB5QzZabmVUZUYyRXh0amh3WlZqWER3TWJJelZZSWJ3MXkyUFBHdzhkQzdwYzRzSVZTbkk0b1FBSDB2NjUwZ2hmaG1NZXZXVzRoNFB5bGJ3L0RHNGZkZVQzKzh6MGY4TU9TVHRmaXB4MDUvUXR0SGxBdk9EUnhhNXlEQ3ZPejZuM2YxQk8wTi80TWJHeU9NMjdxVzR4OG1CVm9yeVVaV2ZzbWhmaTAvR2ZOdkI4K3lsVEdiWU5YMjdPY0VGNVdadE5xYldFeTdWNUdibVpVU3hzcGNnN0ZCWjNqMy8zTDdtdXZaWG9aaTFmNWVycTR5M2xTSCtvRjdxUEZudmlleUxPQ3d1YmNReStveW93ajRjRDBpZFJlY25wK3MrNTJENyttekYxZ3BXK2YrY0phc294aWdieGtPbG9TNHllaUt2Z000bldTcEdFajEyUlB4NW84MEVtemlYTjl0NHpKelcxaHRBNWhYSkplRnltcE1ZdHlDVUNldlJXVm1yUllrdFE3VW5DYTZ0TC9BVlBpMW5pNVhDWEhmTE91cWZLNUtMWXhqd1Z0ZGhoZm1DTHZwYUtWTkNFT0NmNG5OR0hxRDk4d1ViMGVqV053RFlCbkpHa0JSdzlSVnNwZndOZG9Xb1ZjbjJxdjh5d0dVcDd5Yy9zazJMcEk2VnppUnl2V3FiR05HMVpKcVZWdXRSYzQ2K3NPRTRWU2VTMi9DVE5LTmE2Tk1DZi9oeHhhVVZzWWZLZFArUzFMYzRpYW5LcDBzVXVnUHBtdG95ZEMwQ1AyUndqbkhMY256eENudnF4bExRV1hNTlBUc3UyeUJjSGxXSkx0Y21kZy9NY0xhZ3k3bDVrRzZzTjdaeEpMRC9rQXg4RTZ1cW9oUCtLL2pNU3JZdVFKbmhyNHRmNGJpV0ZZM0p6Zi9LcEFZeVZTcStwYmZGS1hNYkw5bkhFb1pRVDE1V3lhYmQ2RE1BcTNRWk90aE01cFV6ZE5xVVhrY3JJMVB3akFTQWc0ZFZVLzJvKzk4bVRYbnloUDdWbEVrVG5kZHB3eUpGK1lDQ2MwMXBxYTZaWDk5cG5SK1lPcElyYjVEdFBjS3EwdDVjRkNEeVF4TjFsVXVxWmNZdWp2WC9HQzcrNklwUlR6L05rRVkvNWNudE9mWkp5ZDZTa1V4L0pSNUFKSVJQQ05laWIwcmNXbm5GbjBYSzhzMjJ4NDB3TGpFdHVmWERPOSt5cmFmNGtIeGxCSTNTQjBtZU1qZXBYNTBDSmhBeDdOZWtrUkZud29lY0xmcUl5cmpYcmdBWnNUTVBpWDk0c25qZHJ6aXNBWHRCS2FudWxpbFN4T2ZXMkhTbWZuazBMMkdYSEpKM1MrdkNiZ1JFbWw5ZGVrZm1IaG13RVl0djNrZmh0aDdFUkl3VlQ4ajY5T3RJQkNtZVRJSnVBN283M1BYYzFMSjJ3aHVaSzdGWmU2TlRPczYxTVA3SlZsSWM2dGJodE9CazNOWFU4cEJQT0NZNlE1NWQ2M0Z0cFVVL0tDYlEwSzJXUTlvNWpOR3dOTko2Q3c1RkRDdCt4NVlKZVAxZDJRekRTaXErY1NacXdxQ1pWY05vQWZ0R0d6Z3JSWjR2a3BiUDB4UVpIQ3RXc1V3SFZUanJZNE5mUkRqSGRWUFJuOEo5WHRvb2MvaElXa3F6RmFjellUbGFwdTJSQ0pxZThhRW8zanpXWkNFSjdBaUJ6QllyYVlEZ09mZWlReWVKVXNiYlhIZEdLaWl3MjJCZHA0TStZejY3MU1aMGllOUVXUmtzMFEyREhHVVRuZTlTNHRBQUpQR2JVZjR4dENNb0o0MFE0UVcwNVFMU1p5M3dORkFVRlltWEU0RUNERkI4K0lFZU5GRkYvdGdBbmZoOEd2RmgvMXhZK0xFK296TW1pbmpTUndocTA5elFBRWtORnl1MElLZ3ZEa3lPSStpY2NGN2RvV0xUVWFzUHhMSDF4R1BCU0trZVdDblNvWC80QzJjOWR1NjZCYlJYYW1IcWFlK1ZDaVM2Qk45NTNpWUtPdThyTWtuZVcrVEZqVjBDekFmOFBORzlMQmF5VWdZNVV3OXgwU2FwNnpJNFBwYVpLSDRkOWt0a0ZVKzkxeUJiTjVTUnRkZDgvRVFhdEpYbUJOZnc3a1JybGwrTGp3WndlV0xFYjBqVDFScEd1RHFITGRMVE1NZ293U2N6cnAyekNzSmRRSC9zZWFZcnB3a0NGUjhkUmpNN0pSVWtWYTFBMFN2ZFpBWDc1dnVCdnZMQnpOMmMrUmJ0NW9xeTkzbHFJQklqS2I3dzJVa1JUYjRwQldjTktOUFVndDNhei9QeFF4RWFzYWx0ZG11eXZzWFlidXIzVlBObjhKek4vNko1TitRQ256NTcxVGNjYXBDbWtQdkZFK2dheVhVK1VGMVZLWXlGY1V2bGV2N2xzUWxtanF6SzJRQThYN1EzVHpGYXBxaHZKS2x0N2R6QkhyYXFaQmdQZ2I5TjRvS05heWk4RlpUNzEra2FZaEloRnJFU0tZUkhmSGtqNW4za2ZoZ0x5K3grakxEYkt4TWdna2dIbkp6OVJ1RUhaWEJXdGtZQVE9PSIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJpZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3ZlcmlmaWVyIiwidHlwZSI6InRvb2xfdXNlIiwidG9vbHNldF9uYW1lIjpudWxsfV0sImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2Zkd0FNUmRTMjFGZUpmelFoTTNzIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjczOTYsIm91dHB1dF90b2tlbnMiOjc3MCwib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6NzM5fSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCIsIm1vZGVsX3Byb3ZpZGVyIjoiYW50aHJvcGljIn0sInR5cGUiOiJhaSIsIm5hbWUiOiJza2VwdGljIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTRhZTktNzZjMC04OWE2LTRmMGIxMTRlMzAzMC0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6InRyYW5zZmVyX3RvX3ZlcmlmaWVyIiwiYXJncyI6e30sImlkIjoidG9vbHVfMDFINmZWOEp2UkpOdlh5NDdlUm5kM2pIIiwidHlwZSI6InRvb2xfY2FsbCJ9XSwiaW52YWxpZF90b29sX2NhbGxzIjpbXSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjo3Mzk2LCJvdXRwdXRfdG9rZW5zIjo3NzAsInRvdGFsX3Rva2VucyI6ODE2NiwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6NzM5fX19LHsiY29udGVudCI6IlN1Y2Nlc3NmdWxseSB0cmFuc2ZlcnJlZCB0byB2ZXJpZmllciIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsInN0YXR1cyI6InN1Y2Nlc3MifV0sImFjdGl2ZV9hZ2VudCI6InZlcmlmaWVyIn0sInJlc3VtZSI6bnVsbCwiZ290byI6InZlcmlmaWVyIn19" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "343bcf62767e113c", + "parentSpanId": "3ed306e7faadc279", + "name": "transfer_to_verifier", + "kind": 1, + "startTimeUnixNano": "1790968030837124096", + "endTimeUnixNano": "1790968030837764096", + "attributes": [ + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "tool" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "transfer_to_verifier" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "execute_tool" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "gen_ai.tool.name", + "value": { + "stringValue": "transfer_to_verifier" + } + }, + { + "key": "gen_ai.tool.call.id", + "value": { + "stringValue": "toolu_01H6fV8JvRJNvXy47eRnd3jH" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "2" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "tools" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"__pregel_push\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_push\",0,false]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "skeptic:3dfc4328-0d19-e1ea-bcda-3d3d36d7cda8|tools:e52223df-38bd-4e1b-4faf-e72de38488d4" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "skeptic" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "skeptic:3dfc4328-0d19-e1ea-bcda-3d3d36d7cda8" + } + }, + { + "key": "langsmith.metadata.__handoff_destination", + "value": { + "stringValue": "verifier" + } + }, + { + "key": "langsmith.metadata.ls_method", + "value": { + "stringValue": "traceable" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "seq:step:1" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "e30=" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJvdXRwdXQiOnsiZ3JhcGgiOiJfX3BhcmVudF9fIiwidXBkYXRlIjp7Im1lc3NhZ2VzIjpbeyJjb250ZW50IjoiRm9yIGEgc3ludGhldGljIHRyYWNpbmcgZml4dHVyZSwgZXhwbGFpbiBQeXRob24gdHVwbGUgdmVyc3VzIGxpc3QgaW1tdXRhYmlsaXR5IGluIGF0IG1vc3QgMTIwIHdvcmRzLiBVc2Ugb25seSBvZmZpY2lhbCBQeXRob24gZG9jdW1lbnRhdGlvbiBpZiB5b3Ugc2VhcmNoLiBIYXZlIHRoZSBza2VwdGljIGFuZCB2ZXJpZmllciBjaGVjayBpdCwgdGhlbiBoYXZlIHRoZSBlZGl0b3Igd3JpdGUgL2Fuc3dlci5tZC4gS2VlcCB0aGUgcnVuIHNob3J0IGFuZCBkbyBub3QgYXNrIGZvbGxvdy11cCBxdWVzdGlvbnMuIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJodW1hbiIsImlkIjoiNDYyMDQzMDUtZjhlZS00Mzg0LTljZTgtZmNkODBiZmNkZjc1In0seyJjb250ZW50IjpbeyJpZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7InF1ZXJ5IjoiZG9jcy5weXRob24ub3JnIHR1cGxlcyBpbW11dGFibGUgc2VxdWVuY2VzIGxpc3RzIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIHR1dG9yaWFsIn0sIm5hbWUiOiJ3ZWJfc2VhcmNoIiwidHlwZSI6InRvb2xfdXNlIiwidG9vbHNldF9uYW1lIjpudWxsfV0sImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2ZkdzlhVzlicmVuUHFneWJmTndQIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjI4NDUsIm91dHB1dF90b2tlbnMiOjczLCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjowfSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCIsIm1vZGVsX3Byb3ZpZGVyIjoiYW50aHJvcGljIn0sInR5cGUiOiJhaSIsIm5hbWUiOiJyZXNlYXJjaGVyIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTIxYTMtNzAwMC1hMzgxLTI4NGE2YTBjMDNmNy0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6IndlYl9zZWFyY2giLCJhcmdzIjp7InF1ZXJ5IjoiZG9jcy5weXRob24ub3JnIHR1cGxlcyBpbW11dGFibGUgc2VxdWVuY2VzIGxpc3RzIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIHR1dG9yaWFsIn0sImlkIjoidG9vbHVfMDFCMkxmSEdEallTRkpobWlYTmo0Q3VRIiwidHlwZSI6InRvb2xfY2FsbCJ9XSwiaW52YWxpZF90b29sX2NhbGxzIjpbXSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjoyODQ1LCJvdXRwdXRfdG9rZW5zIjo3MywidG90YWxfdG9rZW5zIjoyOTE4LCJpbnB1dF90b2tlbl9kZXRhaWxzIjp7ImNhY2hlX3JlYWQiOjAsImNhY2hlX2NyZWF0aW9uIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VuX2RldGFpbHMiOnsicmVhc29uaW5nIjowfX19LHsiY29udGVudCI6Ilt7XCJ0aXRsZVwiOiBcIjUuIERhdGEgU3RydWN0dXJlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiWW91IG1pZ2h0IGhhdmUgbm90aWNlZCB0aGF0IG1ldGhvZHMgbGlrZWBpbnNlcnRgLGByZW1vdmVgIG9yYHNvcnRgIHRoYXQgb25seSBtb2RpZnkgdGhlIGxpc3QgaGF2ZSBubyByZXR1cm4gdmFsdWUgcHJpbnRlZCBcXHUyMDEzIHRoZXkgcmV0dXJuIHRoZSBkZWZhdWx0YE5vbmVgLiBbMV0gVGhpcyBpcyBhIGRlc2lnbiBwcmluY2lwbGUgZm9yIGFsbCBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyBpbiBQeXRob24uXFxuLi4uXFxuIyMgNS4zLiBUdXBsZXMgYW5kIFNlcXVlbmNlc1xcdTAwYjZcXG5cXG5XZSBzYXcgdGhhdCBsaXN0cyBhbmQgc3RyaW5ncyBoYXZlIG1hbnkgY29tbW9uIHByb3BlcnRpZXMsIHN1Y2ggYXMgaW5kZXhpbmcgYW5kIHNsaWNpbmcgb3BlcmF0aW9ucy4gVGhleSBhcmUgdHdvIGV4YW1wbGVzIG9mIHNlcXVlbmNlIGRhdGEgdHlwZXMgKHNlZSBTZXF1ZW5jZSBUeXBlcyBcXHUyMDE0IGxpc3QsIHR1cGxlLCByYW5nZSkuIFNpbmNlIFB5dGhvbiBpcyBhbiBldm9sdmluZyBsYW5ndWFnZSwgb3RoZXIgc2VxdWVuY2UgZGF0YSB0eXBlcyBtYXkgYmUgYWRkZWQuIFRoZXJlIGlzIGFsc28gYW5vdGhlciBzdGFuZGFyZCBzZXF1ZW5jZSBkYXRhIHR5cGU6IHRoZSB0dXBsZS5cXG5cXG5BIHR1cGxlIGNvbnNpc3RzIG9mIGEgbnVtYmVyIG9mIHZhbHVlcyBzZXBhcmF0ZWQgYnkgY29tbWFzLCBmb3IgaW5zdGFuY2U6XFxuLi4uXFxuPj4+ICMgVHVwbGVzIGFyZSBpbW11dGFibGU6XFxuPj4+IHRbMF0gPSA4ODg4OFxcblRyYWNlYmFjayAobW9zdCByZWNlbnQgY2FsbCBsYXN0KTpcXG4gIEZpbGUgXFxcIjxzdGRpbj5cXFwiLCBsaW5lIDEsIGluIDxtb2R1bGU+XFxuVHlwZUVycm9yOiAndHVwbGUnIG9iamVjdCBkb2VzIG5vdCBzdXBwb3J0IGl0ZW0gYXNzaWdubWVudFxcbj4+PiAjIGJ1dCB0aGV5IGNhbiBjb250YWluIG11dGFibGUgb2JqZWN0czpcXG4+Pj4gdiA9IChbMSwgMiwgM10sIFszLCAyLCAxXSlcXG4+Pj4gdlxcbihbMSwgMiwgM10sIFszLCAyLCAxXSlcXG4uLi5cXG5BcyB5b3Ugc2VlLCBvbiBvdXRwdXQgdHVwbGVzIGFyZSBhbHdheXMgZW5jbG9zZWQgaW4gcGFyZW50aGVzZXMsIHNvIHRoYXQgbmVzdGVkIHR1cGxlcyBhcmUgaW50ZXJwcmV0ZWQgY29ycmVjdGx5OyB0aGV5IG1heSBiZSBpbnB1dCB3aXRoIG9yIHdpdGhvdXQgc3Vycm91bmRpbmcgcGFyZW50aGVzZXMsIGFsdGhvdWdoIG9mdGVuIHBhcmVudGhlc2VzIGFyZSBuZWNlc3NhcnkgYW55d2F5IChpZiB0aGUgdHVwbGUgaXMgcGFydCBvZiBhIGxhcmdlciBleHByZXNzaW9uKS4gSXQgaXMgbm90IHBvc3NpYmxlIHRvIGFzc2lnbiB0byB0aGUgaW5kaXZpZHVhbCBpdGVtcyBvZiBhIHR1cGxlLCBob3dldmVyIGl0IGlzIHBvc3NpYmxlIHRvIGNyZWF0ZSB0dXBsZXMgd2hpY2ggY29udGFpbiBtdXRhYmxlIG9iamVjdHMsIHN1Y2ggYXMgbGlzdHMuXFxuXFxuVGhvdWdoIHR1cGxlcyBtYXkgc2VlbSBzaW1pbGFyIHRvIGxpc3RzLCB0aGV5IGFyZSBvZnRlbiB1c2VkIGluIGRpZmZlcmVudCBzaXR1YXRpb25zIGFuZCBmb3IgZGlmZmVyZW50IHB1cnBvc2VzLiBUdXBsZXMgYXJlIGltbXV0YWJsZSwgYW5kIHVzdWFsbHkgY29udGFpbiBhIGhldGVyb2dlbmVvdXMgc2VxdWVuY2Ugb2YgZWxlbWVudHMgdGhhdCBhcmUgYWNjZXNzZWQgdmlhIHVucGFja2luZyAoc2VlIGxhdGVyIGluIHRoaXMgc2VjdGlvbikgb3IgaW5kZXhpbmcgKG9yIGV2ZW4gYnkgYXR0cmlidXRlIGluIHRoZSBjYXNlIG9mIG5hbWVkdHVwbGVzKS4gTGlzdHMgYXJlIG11dGFibGUsIGFuZCB0aGVpciBlbGVtZW50cyBhcmUgdXN1YWxseSBob21vZ2VuZW91cyBhbmQgYXJlIGFjY2Vzc2VkIGJ5IGl0ZXJhdGluZyBvdmVyIHRoZSBsaXN0Llxcbi4uLlxcbkFub3RoZXIgdXNlZnVsIGRhdGEgdHlwZSBidWlsdCBpbnRvIFB5dGhvbiBpcyB0aGUgZGljdGlvbmFyeSAoc2VlIE1hcHBpbmcgVHlwZXMgXFx1MjAxNCBkaWN0KS4gRGljdGlvbmFyaWVzIGFyZSBzb21ldGltZXMgZm91bmQgaW4gb3RoZXIgbGFuZ3VhZ2VzIGFzIFxcdTIwMWNhc3NvY2lhdGl2ZSBtZW1vcmllc1xcdTIwMWQgb3IgXFx1MjAxY2Fzc29jaWF0aXZlIGFycmF5c1xcdTIwMWQuIFVubGlrZSBzZXF1ZW5jZXMsIHdoaWNoIGFyZSBpbmRleGVkIGJ5IGEgcmFuZ2Ugb2YgbnVtYmVycywgZGljdGlvbmFyaWVzIGFyZSBpbmRleGVkIGJ5IGtleXMsIHdoaWNoIGNhbiBiZSBhbnkgaW1tdXRhYmxlIHR5cGU7IHN0cmluZ3MgYW5kIG51bWJlcnMgY2FuIGFsd2F5cyBiZSBrZXlzLiBUdXBsZXMgY2FuIGJlIHVzZWQgYXMga2V5cyBpZiB0aGV5IGNvbnRhaW4gb25seSBzdHJpbmdzLCBudW1iZXJzLCBvciB0dXBsZXM7IGlmIGEgdHVwbGUgY29udGFpbnMgYW55IG11dGFibGUgb2JqZWN0IGVpdGhlciBkaXJlY3RseSBvciBpbmRpcmVjdGx5LCBpdCBjYW5ub3QgYmUgdXNlZCBhcyBhIGtleS4gWW91IGNhblxcdTIwMTl0IHVzZSBsaXN0cyBhcyBrZXlzLCBzaW5jZSBsaXN0cyBjYW4gYmUgbW9kaWZpZWQgaW4gcGxhY2UgdXNpbmcgaW5kZXggYXNzaWdubWVudHMsIHNsaWNlIGFzc2lnbm1lbnRzLCBvciBtZXRob2RzIGxpa2UgYXBwZW5kKCkgYW5kIGV4dGVuZCgpLlwiXX0sIHtcInRpdGxlXCI6IFwiMy4gRGF0YSBtb2RlbCBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvcmVmZXJlbmNlL2RhdGFtb2RlbC5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJUaGUgdmFsdWUgb2Ygc29tZSBvYmplY3RzIGNhbiBjaGFuZ2UuIE9iamVjdHMgd2hvc2UgdmFsdWUgY2FuIGNoYW5nZSBhcmUgc2FpZCB0byBiZSBtdXRhYmxlOyBvYmplY3RzIHdob3NlIHZhbHVlIGlzIHVuY2hhbmdlYWJsZSBvbmNlIHRoZXkgYXJlIGNyZWF0ZWQgYXJlIGNhbGxlZCBpbW11dGFibGUuIChUaGUgdmFsdWUgb2YgYW4gaW1tdXRhYmxlIGNvbnRhaW5lciBvYmplY3QgdGhhdCBjb250YWlucyBhIHJlZmVyZW5jZSB0byBhIG11dGFibGUgb2JqZWN0IGNhbiBjaGFuZ2Ugd2hlbiB0aGUgbGF0dGVyXFx1MjAxOXMgdmFsdWUgaXMgY2hhbmdlZDsgaG93ZXZlciB0aGUgY29udGFpbmVyIGlzIHN0aWxsIGNvbnNpZGVyZWQgaW1tdXRhYmxlLCBiZWNhdXNlIHRoZSBjb2xsZWN0aW9uIG9mIG9iamVjdHMgaXQgY29udGFpbnMgY2Fubm90IGJlIGNoYW5nZWQuIFNvLCBpbW11dGFiaWxpdHkgaXMgbm90IHN0cmljdGx5IHRoZSBzYW1lIGFzIGhhdmluZyBhbiB1bmNoYW5nZWFibGUgdmFsdWUsIGl0IGlzIG1vcmUgc3VidGxlLikgQW4gb2JqZWN0XFx1MjAxOXMgbXV0YWJpbGl0eSBpcyBkZXRlcm1pbmVkIGJ5IGl0cyB0eXBlOyBmb3IgaW5zdGFuY2UsIG51bWJlcnMsIHN0cmluZ3MgYW5kIHR1cGxlcyBhcmUgaW1tdXRhYmxlLCB3aGlsZSBkaWN0aW9uYXJpZXMgYW5kIGxpc3RzIGFyZSBtdXRhYmxlLlxcbi4uLlxcblNvbWUgb2JqZWN0cyBjb250YWluIHJlZmVyZW5jZXMgdG8gb3RoZXIgb2JqZWN0czsgdGhlc2UgYXJlIGNhbGxlZCBjb250YWluZXJzLiBFeGFtcGxlcyBvZiBjb250YWluZXJzIGFyZSB0dXBsZXMsIGxpc3RzIGFuZCBkaWN0aW9uYXJpZXMuIFRoZSByZWZlcmVuY2VzIGFyZSBwYXJ0IG9mIGEgY29udGFpbmVyXFx1MjAxOXMgdmFsdWUuIEluIG1vc3QgY2FzZXMsIHdoZW4gd2UgdGFsayBhYm91dCB0aGUgdmFsdWUgb2YgYSBjb250YWluZXIsIHdlIGltcGx5IHRoZSB2YWx1ZXMsIG5vdCB0aGUgaWRlbnRpdGllcyBvZiB0aGUgY29udGFpbmVkIG9iamVjdHM7IGhvd2V2ZXIsIHdoZW4gd2UgdGFsayBhYm91dCB0aGUgbXV0YWJpbGl0eSBvZiBhIGNvbnRhaW5lciwgb25seSB0aGUgaWRlbnRpdGllcyBvZiB0aGUgaW1tZWRpYXRlbHkgY29udGFpbmVkIG9iamVjdHMgYXJlIGltcGxpZWQuIFNvLCBpZiBhbiBpbW11dGFibGUgY29udGFpbmVyIChsaWtlIGEgdHVwbGUpIGNvbnRhaW5zIGEgcmVmZXJlbmNlIHRvIGEgbXV0YWJsZSBvYmplY3QsIGl0cyB2YWx1ZSBjaGFuZ2VzIGlmIHRoYXQgbXV0YWJsZSBvYmplY3QgaXMgY2hhbmdlZC5cXG4uLi5cXG4jIyMgMy4yLjUuIFNlcXVlbmNlc1xcdTAwYjZcXG4uLi5cXG4jIyMjIDMuMi41LjEuIEltbXV0YWJsZSBzZXF1ZW5jZXNcXHUwMGI2XFxuXFxuQW4gb2JqZWN0IG9mIGFuIGltbXV0YWJsZSBzZXF1ZW5jZSB0eXBlIGNhbm5vdCBjaGFuZ2Ugb25jZSBpdCBpcyBjcmVhdGVkLiAoSWYgdGhlIG9iamVjdCBjb250YWlucyByZWZlcmVuY2VzIHRvIG90aGVyIG9iamVjdHMsIHRoZXNlIG90aGVyIG9iamVjdHMgbWF5IGJlIG11dGFibGUgYW5kIG1heSBiZSBjaGFuZ2VkOyBob3dldmVyLCB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGRpcmVjdGx5IHJlZmVyZW5jZWQgYnkgYW4gaW1tdXRhYmxlIG9iamVjdCBjYW5ub3QgY2hhbmdlLilcXG5cXG5UaGUgZm9sbG93aW5nIHR5cGVzIGFyZSBpbW11dGFibGUgc2VxdWVuY2VzOlxcbi4uLlxcblR1cGxlc1xcbjogVGhlIGl0ZW1zIG9mIGEgYHR1cGxlYCBhcmUgYXJiaXRyYXJ5IFB5dGhvbiBvYmplY3RzLiBUdXBsZXMgb2YgdHdvIG9yIG1vcmUgaXRlbXMgYXJlIGZvcm1lZCBieSBjb21tYS1zZXBhcmF0ZWQgbGlzdHMgb2YgZXhwcmVzc2lvbnMuIEEgdHVwbGUgb2Ygb25lIGl0ZW0gKGEgXFx1MjAxOHNpbmdsZXRvblxcdTIwMTkpIGNhbiBiZSBmb3JtZWQgYnkgYWZmaXhpbmcgYSBjb21tYSB0byBhbiBleHByZXNzaW9uIChhbiBleHByZXNzaW9uIGJ5IGl0c2VsZiBkb2VzIG5vdCBjcmVhdGUgYSB0dXBsZSwgc2luY2UgcGFyZW50aGVzZXMgbXVzdCBiZSB1c2FibGUgZm9yIGdyb3VwaW5nIG9mIGV4cHJlc3Npb25zKS4gQW4gZW1wdHkgdHVwbGUgY2FuIGJlIGZvcm1lZCBieSBhbiBlbXB0eSBwYWlyIG9mIHBhcmVudGhlc2VzLlxcbi4uLlxcbiMjIyMgMy4yLjUuMi4gTXV0YWJsZSBzZXF1ZW5jZXNcXHUwMGI2XFxuXFxuTXV0YWJsZSBzZXF1ZW5jZXMgY2FuIGJlIGNoYW5nZWQgYWZ0ZXIgdGhleSBhcmUgY3JlYXRlZC4gVGhlIHN1YnNjcmlwdGlvbiBhbmQgc2xpY2luZyBub3RhdGlvbnMgY2FuIGJlIHVzZWQgYXMgdGhlIHRhcmdldCBvZiBhc3NpZ25tZW50IGFuZCBgZGVsYCAoZGVsZXRlKSBzdGF0ZW1lbnRzLlxcbi4uLlxcblRoZXJlIGFyZSBjdXJyZW50bHkgdHdvIGludHJpbnNpYyBtdXRhYmxlIHNlcXVlbmNlIHR5cGVzOlxcblxcbkxpc3RzXFxuOiBUaGUgaXRlbXMgb2YgYSBsaXN0IGFyZSBhcmJpdHJhcnkgUHl0aG9uIG9iamVjdHMuIExpc3RzIGFyZSBmb3JtZWQgYnkgcGxhY2luZyBhIGNvbW1hLXNlcGFyYXRlZCBsaXN0IG9mIGV4cHJlc3Npb25zIGluIHNxdWFyZSBicmFja2V0cy4gKE5vdGUgdGhhdCB0aGVyZSBhcmUgbm8gc3BlY2lhbCBjYXNlcyBuZWVkZWQgdG8gZm9ybSBsaXN0cyBvZiBsZW5ndGggMCBvciAxLilcIl19LCB7XCJ0aXRsZVwiOiBcIkJ1aWx0LWluIFR5cGVzIFxcdTIwMTQgUHl0aG9uIDMuMTQuNyBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9idWlsdGlucy9zdGR0eXBlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJjb2xsZWN0aW9uIGNsYXNzZXMgYXJlIG11dGFibGUuIFRoZSBtZXRob2RzIHRoYXQgYWRkLCBzdWJ0cmFjdCwgb3IgcmVhcnJhbmdlIHRoZWlyIC4uLiBpbiBwbGFjZSwgYW5kIGRvblxcdTIwMTl0IHJldHVybiBhIC4uLiAsIG5ldmVyIHJldHVybiB0aGUgY29sbGVjdGlvbiBpbnN0YW5jZSBpdHNlbGYgYnV0IGBOb25lYC5cXG4uLi5cXG4jIyBTZXF1ZW5jZSBUeXBlcyBcXHUyMDE0IGBsaXN0YCwgYHR1cGxlYCwgYHJhbmdlYFxcdTAwYjZcXG5cXG5UaGVyZSBhcmUgdGhyZWUgYmFzaWMgc2VxdWVuY2UgdHlwZXM6IGxpc3RzLCB0dXBsZXMsIGFuZCByYW5nZSBvYmplY3RzLiBBZGRpdGlvbmFsIHNlcXVlbmNlIHR5cGVzIHRhaWxvcmVkIGZvciBwcm9jZXNzaW5nIG9mIGJpbmFyeSBkYXRhIGFuZCB0ZXh0IHN0cmluZ3MgYXJlIGRlc2NyaWJlZCBpbiBkZWRpY2F0ZWQgc2VjdGlvbnMuXFxuLi4uXFxuVGhlIG9wZXJhdGlvbnMgaW4gdGhlIGZvbGxvd2luZyB0YWJsZSAuLi4gbXV0YWJsZSBhbmQgaW1tdXRhYmxlLiBUaGUgYGNvbGxlY3Rpb25zLiAuLi4gaXMgcHJvdmlkZWQgdG8gbWFrZSAuLi4gZWFzaWVyIHRvIGNvcnJlY3RseSBpbXBsZW1lbnQgdGhlc2Ugb3BlcmF0aW9ucyBvbiBjdXN0b20gc2VxdWVuY2UgdHlwZXNcXG4uLi5cXG4jIyMgSW1tdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbm11dGFibGUgc2VxdWVuY2UgdHlwZXMgaXNcXG4uLi5cXG5zdXBwb3J0IGFsbG93cyBpbW11dGFibGUgc2VxdWVuY2VzLCAuLi4gLCB0byBiZSB1c2VkIGFzIGBkaWN0YCBrZXlzIGFuZCBzdG9yZWQgaW4gLi4uIGVuc2V0YCBpbnN0YW5jZXNcXG4uLi5cXG4jIyMgTXV0YWJsZSBTZXF1ZW5jZSBUeXBlc1xcdTAwYjZcXG4uLi5cXG4jIyMgTGlzdHNcXHUwMGI2XFxuXFxuTGlzdHMgYXJlIG11dGFibGUgc2VxdWVuY2VzLCB0eXBpY2FsbHkgdXNlZCB0byBzdG9yZSBjb2xsZWN0aW9ucyBvZiBob21vZ2VuZW91cyBpdGVtcyAod2hlcmUgdGhlIHByZWNpc2UgZGVncmVlIG9mIHNpbWlsYXJpdHkgd2lsbCB2YXJ5IGJ5IGFwcGxpY2F0aW9uKS5cXG4uLi5cXG4jIyMgVHVwbGVzXFx1MDBiNlxcblxcblR1cGxlcyBhcmUgaW1tdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaGV0ZXJvZ2VuZW91cyBkYXRhIChzdWNoIGFzIHRoZSAyLXR1cGxlcyBwcm9kdWNlZCBieSB0aGUgYGVudW1lcmF0ZSgpYCBidWlsdC1pbikuIFR1cGxlcyBhcmUgYWxzbyB1c2VkIGZvciBjYXNlcyB3aGVyZSBhbiBpbW11dGFibGUgc2VxdWVuY2Ugb2YgaG9tb2dlbmVvdXMgZGF0YSBpcyBuZWVkZWQgKHN1Y2ggYXMgYWxsb3dpbmcgc3RvcmFnZSBpbiBhIGBzZXRgIG9yIGBkaWN0YCBpbnN0YW5jZSkuXFxuLi4uXFxudHVwbGUoaXRlcmFibGU9XFxuLi4uXFxuVGhlIGNvbnN0cnVjdG9yIGJ1aWxkcyBhIHR1cGxlIHdob3NlIGl0ZW1zIGFyZSB0aGUgc2FtZSBhbmQgaW4gdGhlIHNhbWUgb3JkZXIgYXMgaXRlcmFibGVcXHUyMDE5cyBpdGVtcy4gaXRlcmFibGUgbWF5IGJlIGVpdGhlciBhIHNlcXVlbmNlLCBhIGNvbnRhaW5lciB0aGF0IHN1cHBvcnRzIGl0ZXJhdGlvbiwgb3IgYW4gaXRlcmF0b3Igb2JqZWN0LiBJZiBpdGVyYWJsZSBpcyBhbHJlYWR5IGEgdHVwbGUsIGl0IGlzIHJldHVybmVkIHVuY2hhbmdlZC4gRm9yIGV4YW1wbGUsIGB0dXBsZSgnYWJjJylgIHJldHVybnMgYCgnYScsICdiJywgJ2MnKWAgYW5kIGB0dXBsZSggWzEsIDIsIDNdIClgIHJldHVybnMgYCgxLCAyLCAzKWAuIElmIG5vIGFyZ3VtZW50IGlzIGdpdmVuLCB0aGUgY29uc3RydWN0b3IgY3JlYXRlcyBhIG5ldyBlbXB0eSB0dXBsZSwgYCgpYC5cXG4uLi5cXG5UdXBsZXMgaW1wbGVtZW50IGFsbCBvZiB0aGUgY29tbW9uIHNlcXVlbmNlIG9wZXJhdGlvbnMuXFxuLi4uXFxuRm9yIGhldGVyb2dlbmVvdXMgY29sbGVjdGlvbnMgb2YgZGF0YSB3aGVyZSBhY2Nlc3MgYnkgbmFtZSBpcyBjbGVhcmVyIHRoYW4gYWNjZXNzIGJ5IGluZGV4LCBgY29sbGVjdGlvbnMubmFtZWR0dXBsZSgpYCBtYXkgYmUgYSBtb3JlIGFwcHJvcHJpYXRlIGNob2ljZSB0aGFuIGEgc2ltcGxlIHR1cGxlIG9iamVjdC5cIl19LCB7XCJ0aXRsZVwiOiBcIkJ1aWx0LWluIFR5cGVzIFxcdTIwMTQgUHl0aG9uIDMuMTQuNSBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9saWJyYXJ5L3N0ZHR5cGVzLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcIlNvbWUgY29sbGVjdGlvbiBjbGFzc2VzIGFyZSBtdXRhYmxlLiBUaGUgbWV0aG9kcyB0aGF0IGFkZCwgc3VidHJhY3QsIG9yIHJlYXJyYW5nZSB0aGVpciAuLi4gaW4gcGxhY2UsIGFuZCBkb25cXHUyMDE5dCByZXR1cm4gYSAuLi4gLCBuZXZlciByZXR1cm4gdGhlIGNvbGxlY3Rpb24gaW5zdGFuY2UgaXRzZWxmIGJ1dCBgTm9uZWAuXFxuLi4uXFxuIyMgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBgbGlzdGAsIGB0dXBsZWAsIGByYW5nZWBcXHUwMGI2XFxuXFxuVGhlcmUgYXJlIHRocmVlIGJhc2ljIHNlcXVlbmNlIHR5cGVzOiBsaXN0cywgdHVwbGVzLCBhbmQgLi4uIG9iamVjdHMuIEFkZGl0aW9uYWwgc2VxdWVuY2UgdHlwZXMgdGFpbG9yZWQgZm9yIHByb2Nlc3Npbmcgb2YgYmluYXJ5IGRhdGEgYW5kIHRleHQgc3RyaW5ncyBhcmUgZGVzY3JpYmVkIGluIGRlZGljYXRlZCBzZWN0aW9ucy5cXG4uLi5cXG5UaGUgb3BlcmF0aW9ucyBpbiB0aGUgZm9sbG93aW5nIHRhYmxlIC4uLiBpbW11dGFibGUuIFRoZSBgIC4uLiBpcyBwcm92aWRlZCB0byBtYWtlIC4uLiBlYXNpZXIgdG8gY29ycmVjdGx5IGltcGxlbWVudCB0aGVzZSBvcGVyYXRpb25zIG9uIGN1c3RvbSBzZXF1ZW5jZSB0eXBlc1xcbi4uLlxcbiMjIyBJbW11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxub3BlcmF0aW9uIHRoYXQgaW1tdXRhYmxlIHNlcXVlbmNlIC4uLiBieSBtdXRhYmxlIHNlcXVlbmNlIHR5cGVzIGlzIC4uLiBgaGFzaCgpYFxcbi4uLlxcblRoaXMgc3VwcG9ydCBhbGxvd3MgaW1tdXRhYmxlIHNlcXVlbmNlcywgc3VjaCBhcyBgdHVwbGVgIGluc3RhbmNlcywgdG8gYmUgdXNlZCBhcyBgZGljdGAga2V5cyBhbmQgc3RvcmVkIGluIGBzZXRgIGFuZCBgZnJvemVuc2V0YCBpbnN0YW5jZXMuXFxuLi4uXFxuIyMjIE11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxuIyMjIExpc3RzXFx1MDBiNlxcblxcbkxpc3RzIGFyZSBtdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaG9tb2dlbmVvdXMgaXRlbXMgKHdoZXJlIHRoZSBwcmVjaXNlIGRlZ3JlZSBvZiBzaW1pbGFyaXR5IHdpbGwgdmFyeSBieSBhcHBsaWNhdGlvbikuXFxuLi4uXFxuIyMjIFR1cGxlc1xcdTAwYjZcXG5cXG5UdXBsZXMgYXJlIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhldGVyb2dlbmVvdXMgZGF0YSAoc3VjaCBhcyB0aGUgMi10dXBsZXMgcHJvZHVjZWQgYnkgdGhlIGBlbnVtZXJhdGUoKWAgYnVpbHQtaW4pLiBUdXBsZXMgYXJlIGFsc28gdXNlZCBmb3IgY2FzZXMgd2hlcmUgYW4gaW1tdXRhYmxlIHNlcXVlbmNlIG9mIGhvbW9nZW5lb3VzIGRhdGEgaXMgbmVlZGVkIChzdWNoIGFzIGFsbG93aW5nIHN0b3JhZ2UgaW4gYSBgc2V0YCBvciBgZGljdGAgaW5zdGFuY2UpLlxcbi4uLlxcblRoZSBjb25zdHJ1Y3RvciBidWlsZHMgYSB0dXBsZSB3aG9zZSBpdGVtcyBhcmUgdGhlIHNhbWUgYW5kIGluIHRoZSBzYW1lIG9yZGVyIGFzIGl0ZXJhYmxlXFx1MjAxOXMgaXRlbXMuIGl0ZXJhYmxlIG1heSBiZSBlaXRoZXIgYSBzZXF1ZW5jZSwgYSBjb250YWluZXIgdGhhdCBzdXBwb3J0cyBpdGVyYXRpb24sIG9yIGFuIGl0ZXJhdG9yIG9iamVjdC4gSWYgaXRlcmFibGUgaXMgYWxyZWFkeSBhIHR1cGxlLCBpdCBpcyByZXR1cm5lZCB1bmNoYW5nZWQuIEZvciBleGFtcGxlLCBgdHVwbGUoJ2FiYycpYCByZXR1cm5zIGAoJ2EnLCAnYicsICdjJylgIGFuZCBgdHVwbGUoIFsxLCAyLCAzXSApYCByZXR1cm5zIGAoMSwgMiwgMylgLiBJZiBubyBhcmd1bWVudCBpcyBnaXZlbiwgdGhlIGNvbnN0cnVjdG9yIGNyZWF0ZXMgYSBuZXcgZW1wdHkgdHVwbGUsIGAoKWAuXFxuLi4uXFxuVHVwbGVzIGltcGxlbWVudCBhbGwgb2YgdGhlIGNvbW1vbiBzZXF1ZW5jZSBvcGVyYXRpb25zLlxcbi4uLlxcbkZvciBoZXRlcm9nZW5lb3VzIGNvbGxlY3Rpb25zIG9mIGRhdGEgd2hlcmUgYWNjZXNzIGJ5IG5hbWUgaXMgY2xlYXJlciB0aGFuIGFjY2VzcyBieSBpbmRleCwgYGNvbGxlY3Rpb25zLm5hbWVkdHVwbGUoKWAgbWF5IGJlIGEgbW9yZSBhcHByb3ByaWF0ZSBjaG9pY2UgdGhhbiBhIHNpbXBsZSB0dXBsZSBvYmplY3QuXCJdfSwge1widGl0bGVcIjogXCI1LiBEYXRhIFN0cnVjdHVyZXMgXFx1MjAxNCBQeXRob24gMy4xMC4yMCBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy4xMC90dXRvcmlhbC9kYXRhc3RydWN0dXJlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJZb3UgbWlnaHQgaGF2ZSBub3RpY2VkIHRoYXQgbWV0aG9kcyBsaWtlIGBpbnNlcnRgLCBgcmVtb3ZlYCBvciBgc29ydGAgdGhhdCBvbmx5IG1vZGlmeSB0aGUgbGlzdCBoYXZlIG5vIHJldHVybiB2YWx1ZSBwcmludGVkIFxcdTIwMTMgdGhleSByZXR1cm4gdGhlIGRlZmF1bHQgYE5vbmVgLiAxIFRoaXMgaXMgYSBkZXNpZ24gcHJpbmNpcGxlIGZvciBhbGwgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgaW4gUHl0aG9uLlxcbi4uLlxcbiMjIDUuMy4gVHVwbGVzIGFuZCBTZXF1ZW5jZXNcXHUwMGI2XFxuXFxuV2Ugc2F3IHRoYXQgbGlzdHMgYW5kIHN0cmluZ3MgaGF2ZSBtYW55IGNvbW1vbiBwcm9wZXJ0aWVzLCBzdWNoIGFzIGluZGV4aW5nIGFuZCBzbGljaW5nIG9wZXJhdGlvbnMuIFRoZXkgYXJlIHR3byBleGFtcGxlcyBvZiBzZXF1ZW5jZSBkYXRhIHR5cGVzIChzZWUgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBsaXN0LCB0dXBsZSwgcmFuZ2UpLiBTaW5jZSBQeXRob24gaXMgYW4gZXZvbHZpbmcgbGFuZ3VhZ2UsIG90aGVyIHNlcXVlbmNlIGRhdGEgdHlwZXMgbWF5IGJlIGFkZGVkLiBUaGVyZSBpcyBhbHNvIGFub3RoZXIgc3RhbmRhcmQgc2VxdWVuY2UgZGF0YSB0eXBlOiB0aGUgdHVwbGUuXFxuXFxuQSB0dXBsZSBjb25zaXN0cyBvZiBhIG51bWJlciBvZiB2YWx1ZXMgc2VwYXJhdGVkIGJ5IGNvbW1hcywgZm9yIGluc3RhbmNlOlxcbi4uLlxcbj4+PiAjIFR1cGxlcyBhcmUgaW1tdXRhYmxlOlxcbi4uLiB0WzBdID0gODg4ODhcXG5UcmFjZWJhY2sgKG1vc3QgcmVjZW50IGNhbGwgbGFzdCk6XFxuICBGaWxlIFxcXCI8c3RkaW4+XFxcIiwgbGluZSAxLCBpbiA8bW9kdWxlPlxcblR5cGVFcnJvcjogJ3R1cGxlJyBvYmplY3QgZG9lcyBub3Qgc3VwcG9ydCBpdGVtIGFzc2lnbm1lbnRcXG4+Pj4gIyBidXQgdGhleSBjYW4gY29udGFpbiBtdXRhYmxlIG9iamVjdHM6XFxuLi4uIHYgPSAoWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuPj4+IHZcXG4oWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuLi4uXFxuQXMgeW91IHNlZSwgb24gb3V0cHV0IHR1cGxlcyBhcmUgYWx3YXlzIGVuY2xvc2VkIGluIHBhcmVudGhlc2VzLCBzbyB0aGF0IG5lc3RlZCB0dXBsZXMgYXJlIGludGVycHJldGVkIGNvcnJlY3RseTsgdGhleSBtYXkgYmUgaW5wdXQgd2l0aCBvciB3aXRob3V0IHN1cnJvdW5kaW5nIHBhcmVudGhlc2VzLCBhbHRob3VnaCBvZnRlbiBwYXJlbnRoZXNlcyBhcmUgbmVjZXNzYXJ5IGFueXdheSAoaWYgdGhlIHR1cGxlIGlzIHBhcnQgb2YgYSBsYXJnZXIgZXhwcmVzc2lvbikuIEl0IGlzIG5vdCBwb3NzaWJsZSB0byBhc3NpZ24gdG8gdGhlIGluZGl2aWR1YWwgaXRlbXMgb2YgYSB0dXBsZSwgaG93ZXZlciBpdCBpcyBwb3NzaWJsZSB0byBjcmVhdGUgdHVwbGVzIHdoaWNoIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzLCBzdWNoIGFzIGxpc3RzLlxcblxcblRob3VnaCB0dXBsZXMgbWF5IHNlZW0gc2ltaWxhciB0byBsaXN0cywgdGhleSBhcmUgb2Z0ZW4gdXNlZCBpbiBkaWZmZXJlbnQgc2l0dWF0aW9ucyBhbmQgZm9yIGRpZmZlcmVudCBwdXJwb3Nlcy4gVHVwbGVzIGFyZSBpbW11dGFibGUsIGFuZCB1c3VhbGx5IGNvbnRhaW4gYSBoZXRlcm9nZW5lb3VzIHNlcXVlbmNlIG9mIGVsZW1lbnRzIHRoYXQgYXJlIGFjY2Vzc2VkIHZpYSB1bnBhY2tpbmcgKHNlZSBsYXRlciBpbiB0aGlzIHNlY3Rpb24pIG9yIGluZGV4aW5nIChvciBldmVuIGJ5IGF0dHJpYnV0ZSBpbiB0aGUgY2FzZSBvZiBgbmFtZWR0dXBsZXNgKS4gTGlzdHMgYXJlIG11dGFibGUsIGFuZCB0aGVpciBlbGVtZW50cyBhcmUgdXN1YWxseSBob21vZ2VuZW91cyBhbmQgYXJlIGFjY2Vzc2VkIGJ5IGl0ZXJhdGluZyBvdmVyIHRoZSBsaXN0Llxcbi4uLlxcbiMjIDUuNC5cXG4uLi5cXG4uIFNldCBvYmplY3RzXFxuLi4uXFxuIyMgNS41LiBEaWN0aW9uYXJpZXNcXHUwMGI2XFxuXFxuQW5vdGhlciB1c2VmdWwgZGF0YSB0eXBlIGJ1aWx0IGludG8gUHl0aG9uIGlzIHRoZSBkaWN0aW9uYXJ5IChzZWUgTWFwcGluZyBUeXBlcyBcXHUyMDE0IGRpY3QpLiBEaWN0aW9uYXJpZXMgYXJlIHNvbWV0aW1lcyBmb3VuZCBpbiBvdGhlciBsYW5ndWFnZXMgYXMgXFx1MjAxY2Fzc29jaWF0aXZlIG1lbW9yaWVzXFx1MjAxZCBvciBcXHUyMDFjYXNzb2NpYXRpdmUgYXJyYXlzXFx1MjAxZC4gVW5saWtlIHNlcXVlbmNlcywgd2hpY2ggYXJlIGluZGV4ZWQgYnkgYSByYW5nZSBvZiBudW1iZXJzLCBkaWN0aW9uYXJpZXMgYXJlIGluZGV4ZWQgYnkga2V5cywgd2hpY2ggY2FuIGJlIGFueSBpbW11dGFibGUgdHlwZTsgc3RyaW5ncyBhbmQgbnVtYmVycyBjYW4gYWx3YXlzIGJlIGtleXMuIFR1cGxlcyBjYW4gYmUgdXNlZCBhcyBrZXlzIGlmIHRoZXkgY29udGFpbiBvbmx5IHN0cmluZ3MsIG51bWJlcnMsIG9yIHR1cGxlczsgaWYgYSB0dXBsZSBjb250YWlucyBhbnkgbXV0YWJsZSBvYmplY3QgZWl0aGVyIGRpcmVjdGx5IG9yIGluZGlyZWN0bHksIGl0IGNhbm5vdCBiZSB1c2VkIGFzIGEga2V5LiBZb3UgY2FuXFx1MjAxOXQgdXNlIGxpc3RzIGFzIGtleXMsIHNpbmNlIGxpc3RzIGNhbiBiZSBtb2RpZmllZCBpbiBwbGFjZSB1c2luZyBpbmRleCBhc3NpZ25tZW50cywgc2xpY2UgYXNzaWdubWVudHMsIG9yIG1ldGhvZHMgbGlrZSBgYXBwZW5kKClgIGFuZCBgZXh0ZW5kKClgLlwiXX1dIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJ0b29sIiwibmFtZSI6IndlYl9zZWFyY2giLCJpZCI6IjZlNTEyNWFjLTMxNjQtNGE3Ni05YzUyLWFjMzQ2MGNlMzZlNCIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsInN0YXR1cyI6InN1Y2Nlc3MifSx7ImNvbnRlbnQiOlt7ImlkIjoidG9vbHVfMDFYamozYVJkV3loQkpmUG1USkJ4UWE5IiwiY2FsbGVyIjp7InR5cGUiOiJkaXJlY3QifSwiaW5wdXQiOnt9LCJuYW1lIjoidHJhbnNmZXJfdG9fc2tlcHRpYyIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHdBNkY0Y0NONXloRDRDcnM5TSIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo3Mzc4LCJvdXRwdXRfdG9rZW5zIjozMywib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6MH0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoicmVzZWFyY2hlciIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy0zY2Q4LTc5ZjEtODhhMS1jZTZjZWUyOTRlNzktMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ0cmFuc2Zlcl90b19za2VwdGljIiwiYXJncyI6e30sImlkIjoidG9vbHVfMDFYamozYVJkV3loQkpmUG1USkJ4UWE5IiwidHlwZSI6InRvb2xfY2FsbCJ9XSwiaW52YWxpZF90b29sX2NhbGxzIjpbXSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjo3Mzc4LCJvdXRwdXRfdG9rZW5zIjozMywidG90YWxfdG9rZW5zIjo3NDExLCJpbnB1dF90b2tlbl9kZXRhaWxzIjp7ImNhY2hlX3JlYWQiOjAsImNhY2hlX2NyZWF0aW9uIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VuX2RldGFpbHMiOnsicmVhc29uaW5nIjowfX19LHsiY29udGVudCI6IlN1Y2Nlc3NmdWxseSB0cmFuc2ZlcnJlZCB0byBza2VwdGljIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJ0b29sIiwibmFtZSI6InRyYW5zZmVyX3RvX3NrZXB0aWMiLCJpZCI6IjM4OTA1OGQxLTA4M2YtNDYzMi1iYmM1LTA5YmZkM2EwZmZkMiIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsInN0YXR1cyI6InN1Y2Nlc3MifSx7ImNvbnRlbnQiOlt7InNpZ25hdHVyZSI6IkNBUVMxUWdLRUFnU0dBSTRBVUlJZEdocGJtdHBibWNTRE42VGZVNk5ETTk3MWxFYnp4b012UlVBMUo5b3gzZkpRWVQ5SWpBckNRUUdIQlA0cFJIUGF2WHpSbGJNYmFZSEM1N1lqK0dieGdyMGhlbHZkMk9JTC9vNlJidDRQRkEzRmFJRlVsZ3E4Z2M3S1FJOUZ0R252U0FrTEJSeklzVXAvMmQ1UmQyemdFc2ZrRlZlWHNuOFdZWVpraW5BUVZWeTZBN0IzTGFFSlcweGIveDg4RGk2N2ZlZHd4MFErRmxadHdjbVQzRGNhYTExSnZleUhScjNkbSt6R1NaUDZDWGVYTko4bnJUWjdXMVlSVHlzRDZ3N283THA4aDFSd3E0ZjRvOGorUVVmWTB2amUrekpac3hIUG1RTnQzeWtlS2g5OWhnbjZkYml6Ni9FSHFtYkhqU0F1MU00U3FFRGZxSUtUaEhZZW5hbUFabWNRNFhBT282bWRRNis3N1ZncFYxbEMxL2s1MklpVW9KamtHbFFybkxDdnV0aUk3S04rc2dLOGMyKzJLQ0pyUFJDdlZWK0JrU3R2UDVqV1dxenl0cEtRK25IemhMNDQ4T3JJQTlnZEpDMzlDOGVEclhlUm5JN3Q0RnFsa1hHb01sWkFWOVVNY3JRSFJ4T01pOHRPeVR6aTlveUJGRFRTc1ErTEpxZXIwdHoxRnQ5Z2VrNnZ6ZzUwU3BWQUZRZExFUnY4UjFZcU1hZUkreHN2VEFFVzlNUmd2Uk1rekx1elJUMmd1dzNqRCtGamlwMzd4Z0s1UUkwUitDc09BbVFQb1RaaXp6Y0l5QVVBdUNKRkNncS9nRC9DaWJMeG95bWlWVUkxYW45U250M0twTlJvV2R2V0luZTMzVjBwdjhQSGc5eVcySnVMT3FRR2lsTENzSy9RM29RcjN1cEVNWHhYR2pDTDdXVWl6dlJaR0ZFRVYrVnBsWXQ3djR0S1pNajVwazN2ZGVNME1KNVhaZzdQdlVzUnp5Y3hjbnJUM0tub2MzcU0rdHRxWmZUc2I4ZDZVL3BRaWZxN0d1YWhGT0hydnJvcWdKTGorLzgxWXkxcFpGa2pmSmdZMFhrbHpBbkE2Q0ZxL1lUMGd4YlYrVEg1SVMrV2FZT1V6SzM2UzhzNXFxTXdobmd2NEl6RTNjdFhYcW5oSVpYTnpueVZCcWJZYS9HeDJiQ2pjRWoybjVSeUdZUlMvK01jdWxnTEJ6blE5TWtHMkQ3UVAzTXEvbndIWEtNSWFMUzhWSzhoRzNkMHJBamx2bmF1WFNoTnJKeHRrajRxa3dueEQ0THJFamsxdWUrUExjYkxxWVd3QXI4bjBzKzQxanJzSVBWeXlLOXZGYUVackE5S2FCeGtKeWlxeDUwR1pObjdja3grbWtJMThnSDUyaXlWUHRVOEZLTVhqdWdDbDZtUExpSFlpRFFkK0c2M0x1ZG1PS3lzOWl0Vm5CREJOTVAyYi9JNytiSVZSbGhlR2FaV3I2TUFUS2w4NkIzeSswa3VPckxSTXlXY2RpYmMyaVFrWm0rOGY3ZFRVd2JLYkJKc0tPRlk3My84bmxML0ppWXBqejZuVUlrK0lxU2phRnlLNlhGbENjdWtXcFdIcERKblN2YkV6ZXk3cUh5Q3FKK0krb244aVA4R090ZW1wUWFTMC80a1pHU3dsckU2eE9qNkV3R3BmamJEWktIa1AvMlJBZE5ZUFFxM3o3YmRWSk1KTm5GTXo2ZTM3bXpNcENCaWh3UGRTRlRSRTNTQ1k2Nnd2VGdKekdDVXloSGJTaTVpMWF6ek9YZDh1SENTcFQvNkwxbzZWVTBwam9SOEthYTR3Q1dIR2Y3UC9XRWQrYXZ5cDAySm51MWNtcFcwK0dxK3ZLOWgwdGRiMklDbE0vZU5mZG91UVArZXA4TUpVanJhRFJJQ21BSlFZNW9iUzcxdm5pT28rcmVhRnkwelJnQiIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJzaWduYXR1cmUiOiJDQVFTL1JFS0VRZ1NHQUk0QVVJSmJtRnljbUYwYVc5dUVnd0ZoM0FWSlpQM3M4eTFIcTBhREc3NnVoSE9OYVNjWEFHU2hpSXdHZ2pMWEhVeDF5WnJrTFBsamp6VE9ReWNwTEppS1Z3WGN4bS9FektKbTFzOE01V3h1UkVXRWxkWGNvYU4wL21zS3BrUk1UK2VMYjVpaDlyQUhVTFRvbVVOc1V3SmV1MGpySmU0R0xhenk3c1A1S3RqRU9FQktRc1hVZm9RN2ZEa1pPUVVIWmk5enpRTjRoRk5tWUpVZjNPVnFmOVRRTmRDMSttLy9IQVpQZldtQXk2SHREdm03a2dCSmtDRW9pRm5tN3RuL1BYMWhYR1BSZ1djNkJUWFVNcjJXQThhS3lxZ2dSVWJNSDIrSll2NWlXWmRkQ2MxdXAxTmJyM3NGN1dQY290OVpCaHlIUVhyNjkyemVKazZTSmd5Z0lWckFTYk1seTJiQnhUV0pCNjIzZ1ZnenBDbVZTTFpGcGtLWGdQY3A4aEd5REtPdkY2U2lRU0hxR1J4UW9mcjdWczFhUGhmWml4Uk9KR0QrTWxya2dnVkFuR015WUlEVVpwYXZ0Q2VpVTFKbUJTejAwODRiWFB0VC90bnBzS09uU1BZWlFvM0IwQlhLYnFBL1dzS25nOC9WM1o4VXVham82SVZaMmhnQTFXRGR1cVFPUWtFeEdVNnBGdUxoZWJJanVXeFNOSS9EZmZ2Ui8vTE5xQ2Z2ZG5mMGh2b1B0RHhmeC9TYXdJeDQvc3BxOXhid0Z6SHRMZ29IZzFGMndGdzdjbFBEbGcxUHlEL3YyeUpHM1Y5VDhHTDZqR1BFNnJoZmlsL3U0SE52ZjdCVzBkUXpDVFRYY3FaeTRuQnl5MCtwK1M3WnhmTWw0S3B4YTJnT2hoU05qZnBmdVgwUXBMNGtNRThKMW5saUZOMWQ4S3FFUHVESTE4QlFIVWhEL1VmWTI5b0EweDUxMEZqTS9Cc0xaNG41c3Bwa29JaTdDRm9RcTJhQUtWTmZDMUtpbGhJcFJEKzFkeUR0WlFrbE44Z1pwT0VTSHc2TUwxVHRVMHlmQWNibXFveEo2aGhqendINkNNc1RRRUV5OE1iUkJNK1FLS2tJUmRSQ1BIR0ZXMkxrdVI1MCtKMHBGL3RyNnBxVnZKZW96ZmlNbnJFaWxCMjdUWlA0bktUSkc4cmQvL3ZDV2VuMHk3Q0EvOGRCSjlnZHA1dHg3TlZBNDVrMWdkY3hFUjFnWW91TlgzbjF2RzMvQjFGNCs5M3dYVzlNcW82VXM1OVRRbXJiOU5xYTdwU1FOU3JIMDZ3MUd4eUJBbGtGZnFBbGFmazE2YU1wcTQxNlRKWXBHeTQzV0NVaUNjTFdsaGlXZ3oyc2dPTEFYUDlhNXdlV0FLUkM4SjZqa2NVa0RlWG5XK0hGU2lQTTYrOUJvNmtWaExVaE41cmM0bUhaT1pwRjdwdFBwc1FONFhDcUxQc3FwUkgvOG81WHB5QzZabmVUZUYyRXh0amh3WlZqWER3TWJJelZZSWJ3MXkyUFBHdzhkQzdwYzRzSVZTbkk0b1FBSDB2NjUwZ2hmaG1NZXZXVzRoNFB5bGJ3L0RHNGZkZVQzKzh6MGY4TU9TVHRmaXB4MDUvUXR0SGxBdk9EUnhhNXlEQ3ZPejZuM2YxQk8wTi80TWJHeU9NMjdxVzR4OG1CVm9yeVVaV2ZzbWhmaTAvR2ZOdkI4K3lsVEdiWU5YMjdPY0VGNVdadE5xYldFeTdWNUdibVpVU3hzcGNnN0ZCWjNqMy8zTDdtdXZaWG9aaTFmNWVycTR5M2xTSCtvRjdxUEZudmlleUxPQ3d1YmNReStveW93ajRjRDBpZFJlY25wK3MrNTJENyttekYxZ3BXK2YrY0phc294aWdieGtPbG9TNHllaUt2Z000bldTcEdFajEyUlB4NW84MEVtemlYTjl0NHpKelcxaHRBNWhYSkplRnltcE1ZdHlDVUNldlJXVm1yUllrdFE3VW5DYTZ0TC9BVlBpMW5pNVhDWEhmTE91cWZLNUtMWXhqd1Z0ZGhoZm1DTHZwYUtWTkNFT0NmNG5OR0hxRDk4d1ViMGVqV053RFlCbkpHa0JSdzlSVnNwZndOZG9Xb1ZjbjJxdjh5d0dVcDd5Yy9zazJMcEk2VnppUnl2V3FiR05HMVpKcVZWdXRSYzQ2K3NPRTRWU2VTMi9DVE5LTmE2Tk1DZi9oeHhhVVZzWWZLZFArUzFMYzRpYW5LcDBzVXVnUHBtdG95ZEMwQ1AyUndqbkhMY256eENudnF4bExRV1hNTlBUc3UyeUJjSGxXSkx0Y21kZy9NY0xhZ3k3bDVrRzZzTjdaeEpMRC9rQXg4RTZ1cW9oUCtLL2pNU3JZdVFKbmhyNHRmNGJpV0ZZM0p6Zi9LcEFZeVZTcStwYmZGS1hNYkw5bkhFb1pRVDE1V3lhYmQ2RE1BcTNRWk90aE01cFV6ZE5xVVhrY3JJMVB3akFTQWc0ZFZVLzJvKzk4bVRYbnloUDdWbEVrVG5kZHB3eUpGK1lDQ2MwMXBxYTZaWDk5cG5SK1lPcElyYjVEdFBjS3EwdDVjRkNEeVF4TjFsVXVxWmNZdWp2WC9HQzcrNklwUlR6L05rRVkvNWNudE9mWkp5ZDZTa1V4L0pSNUFKSVJQQ05laWIwcmNXbm5GbjBYSzhzMjJ4NDB3TGpFdHVmWERPOSt5cmFmNGtIeGxCSTNTQjBtZU1qZXBYNTBDSmhBeDdOZWtrUkZud29lY0xmcUl5cmpYcmdBWnNUTVBpWDk0c25qZHJ6aXNBWHRCS2FudWxpbFN4T2ZXMkhTbWZuazBMMkdYSEpKM1MrdkNiZ1JFbWw5ZGVrZm1IaG13RVl0djNrZmh0aDdFUkl3VlQ4ajY5T3RJQkNtZVRJSnVBN283M1BYYzFMSjJ3aHVaSzdGWmU2TlRPczYxTVA3SlZsSWM2dGJodE9CazNOWFU4cEJQT0NZNlE1NWQ2M0Z0cFVVL0tDYlEwSzJXUTlvNWpOR3dOTko2Q3c1RkRDdCt4NVlKZVAxZDJRekRTaXErY1NacXdxQ1pWY05vQWZ0R0d6Z3JSWjR2a3BiUDB4UVpIQ3RXc1V3SFZUanJZNE5mUkRqSGRWUFJuOEo5WHRvb2MvaElXa3F6RmFjellUbGFwdTJSQ0pxZThhRW8zanpXWkNFSjdBaUJ6QllyYVlEZ09mZWlReWVKVXNiYlhIZEdLaWl3MjJCZHA0TStZejY3MU1aMGllOUVXUmtzMFEyREhHVVRuZTlTNHRBQUpQR2JVZjR4dENNb0o0MFE0UVcwNVFMU1p5M3dORkFVRlltWEU0RUNERkI4K0lFZU5GRkYvdGdBbmZoOEd2RmgvMXhZK0xFK296TW1pbmpTUndocTA5elFBRWtORnl1MElLZ3ZEa3lPSStpY2NGN2RvV0xUVWFzUHhMSDF4R1BCU0trZVdDblNvWC80QzJjOWR1NjZCYlJYYW1IcWFlK1ZDaVM2Qk45NTNpWUtPdThyTWtuZVcrVEZqVjBDekFmOFBORzlMQmF5VWdZNVV3OXgwU2FwNnpJNFBwYVpLSDRkOWt0a0ZVKzkxeUJiTjVTUnRkZDgvRVFhdEpYbUJOZnc3a1JybGwrTGp3WndlV0xFYjBqVDFScEd1RHFITGRMVE1NZ293U2N6cnAyekNzSmRRSC9zZWFZcnB3a0NGUjhkUmpNN0pSVWtWYTFBMFN2ZFpBWDc1dnVCdnZMQnpOMmMrUmJ0NW9xeTkzbHFJQklqS2I3dzJVa1JUYjRwQldjTktOUFVndDNhei9QeFF4RWFzYWx0ZG11eXZzWFlidXIzVlBObjhKek4vNko1TitRQ256NTcxVGNjYXBDbWtQdkZFK2dheVhVK1VGMVZLWXlGY1V2bGV2N2xzUWxtanF6SzJRQThYN1EzVHpGYXBxaHZKS2x0N2R6QkhyYXFaQmdQZ2I5TjRvS05heWk4RlpUNzEra2FZaEloRnJFU0tZUkhmSGtqNW4za2ZoZ0x5K3grakxEYkt4TWdna2dIbkp6OVJ1RUhaWEJXdGtZQVE9PSIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJpZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3ZlcmlmaWVyIiwidHlwZSI6InRvb2xfdXNlIiwidG9vbHNldF9uYW1lIjpudWxsfV0sImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2Zkd0FNUmRTMjFGZUpmelFoTTNzIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjczOTYsIm91dHB1dF90b2tlbnMiOjc3MCwib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6NzM5fSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCIsIm1vZGVsX3Byb3ZpZGVyIjoiYW50aHJvcGljIn0sInR5cGUiOiJhaSIsIm5hbWUiOiJza2VwdGljIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTRhZTktNzZjMC04OWE2LTRmMGIxMTRlMzAzMC0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6InRyYW5zZmVyX3RvX3ZlcmlmaWVyIiwiYXJncyI6e30sImlkIjoidG9vbHVfMDFINmZWOEp2UkpOdlh5NDdlUm5kM2pIIiwidHlwZSI6InRvb2xfY2FsbCJ9XSwiaW52YWxpZF90b29sX2NhbGxzIjpbXSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjo3Mzk2LCJvdXRwdXRfdG9rZW5zIjo3NzAsInRvdGFsX3Rva2VucyI6ODE2NiwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6NzM5fX19LHsiY29udGVudCI6IlN1Y2Nlc3NmdWxseSB0cmFuc2ZlcnJlZCB0byB2ZXJpZmllciIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsInN0YXR1cyI6InN1Y2Nlc3MifV0sImFjdGl2ZV9hZ2VudCI6InZlcmlmaWVyIn0sInJlc3VtZSI6bnVsbCwiZ290byI6InZlcmlmaWVyIn19" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "709919ff60557ac4", + "parentSpanId": "419c86d999598365", + "name": "model", + "kind": 1, + "startTimeUnixNano": "1790968023780810240", + "endTimeUnixNano": "1790968030834845952", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "1" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "skeptic:3dfc4328-0d19-e1ea-bcda-3d3d36d7cda8|model:41713ca4-d95b-3dfd-185e-53f884946d93" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "skeptic" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "skeptic:3dfc4328-0d19-e1ea-bcda-3d3d36d7cda8" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "graph:step:1" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W3siY29udGVudCI6IkZvciBhIHN5bnRoZXRpYyB0cmFjaW5nIGZpeHR1cmUsIGV4cGxhaW4gUHl0aG9uIHR1cGxlIHZlcnN1cyBsaXN0IGltbXV0YWJpbGl0eSBpbiBhdCBtb3N0IDEyMCB3b3Jkcy4gVXNlIG9ubHkgb2ZmaWNpYWwgUHl0aG9uIGRvY3VtZW50YXRpb24gaWYgeW91IHNlYXJjaC4gSGF2ZSB0aGUgc2tlcHRpYyBhbmQgdmVyaWZpZXIgY2hlY2sgaXQsIHRoZW4gaGF2ZSB0aGUgZWRpdG9yIHdyaXRlIC9hbnN3ZXIubWQuIEtlZXAgdGhlIHJ1biBzaG9ydCBhbmQgZG8gbm90IGFzayBmb2xsb3ctdXAgcXVlc3Rpb25zLiIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoiaHVtYW4iLCJpZCI6IjQ2MjA0MzA1LWY4ZWUtNDM4NC05Y2U4LWZjZDgwYmZjZGY3NSJ9LHsiY29udGVudCI6W3siaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9LCJuYW1lIjoid2ViX3NlYXJjaCIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHc5YVc5YnJlblBxZ3liZk53UCIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjoyODQ1LCJvdXRwdXRfdG9rZW5zIjo3Mywib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6MH0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoicmVzZWFyY2hlciIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy0yMWEzLTcwMDAtYTM4MS0yODRhNmEwYzAzZjctMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ3ZWJfc2VhcmNoIiwiYXJncyI6eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9LCJpZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6Mjg0NSwib3V0cHV0X3Rva2VucyI6NzMsInRvdGFsX3Rva2VucyI6MjkxOCwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fSx7ImNvbnRlbnQiOiJbe1widGl0bGVcIjogXCI1LiBEYXRhIFN0cnVjdHVyZXMgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3R1dG9yaWFsL2RhdGFzdHJ1Y3R1cmVzLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcIllvdSBtaWdodCBoYXZlIG5vdGljZWQgdGhhdCBtZXRob2RzIGxpa2VgaW5zZXJ0YCxgcmVtb3ZlYCBvcmBzb3J0YCB0aGF0IG9ubHkgbW9kaWZ5IHRoZSBsaXN0IGhhdmUgbm8gcmV0dXJuIHZhbHVlIHByaW50ZWQgXFx1MjAxMyB0aGV5IHJldHVybiB0aGUgZGVmYXVsdGBOb25lYC4gWzFdIFRoaXMgaXMgYSBkZXNpZ24gcHJpbmNpcGxlIGZvciBhbGwgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgaW4gUHl0aG9uLlxcbi4uLlxcbiMjIDUuMy4gVHVwbGVzIGFuZCBTZXF1ZW5jZXNcXHUwMGI2XFxuXFxuV2Ugc2F3IHRoYXQgbGlzdHMgYW5kIHN0cmluZ3MgaGF2ZSBtYW55IGNvbW1vbiBwcm9wZXJ0aWVzLCBzdWNoIGFzIGluZGV4aW5nIGFuZCBzbGljaW5nIG9wZXJhdGlvbnMuIFRoZXkgYXJlIHR3byBleGFtcGxlcyBvZiBzZXF1ZW5jZSBkYXRhIHR5cGVzIChzZWUgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBsaXN0LCB0dXBsZSwgcmFuZ2UpLiBTaW5jZSBQeXRob24gaXMgYW4gZXZvbHZpbmcgbGFuZ3VhZ2UsIG90aGVyIHNlcXVlbmNlIGRhdGEgdHlwZXMgbWF5IGJlIGFkZGVkLiBUaGVyZSBpcyBhbHNvIGFub3RoZXIgc3RhbmRhcmQgc2VxdWVuY2UgZGF0YSB0eXBlOiB0aGUgdHVwbGUuXFxuXFxuQSB0dXBsZSBjb25zaXN0cyBvZiBhIG51bWJlciBvZiB2YWx1ZXMgc2VwYXJhdGVkIGJ5IGNvbW1hcywgZm9yIGluc3RhbmNlOlxcbi4uLlxcbj4+PiAjIFR1cGxlcyBhcmUgaW1tdXRhYmxlOlxcbj4+PiB0WzBdID0gODg4ODhcXG5UcmFjZWJhY2sgKG1vc3QgcmVjZW50IGNhbGwgbGFzdCk6XFxuICBGaWxlIFxcXCI8c3RkaW4+XFxcIiwgbGluZSAxLCBpbiA8bW9kdWxlPlxcblR5cGVFcnJvcjogJ3R1cGxlJyBvYmplY3QgZG9lcyBub3Qgc3VwcG9ydCBpdGVtIGFzc2lnbm1lbnRcXG4+Pj4gIyBidXQgdGhleSBjYW4gY29udGFpbiBtdXRhYmxlIG9iamVjdHM6XFxuPj4+IHYgPSAoWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuPj4+IHZcXG4oWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuLi4uXFxuQXMgeW91IHNlZSwgb24gb3V0cHV0IHR1cGxlcyBhcmUgYWx3YXlzIGVuY2xvc2VkIGluIHBhcmVudGhlc2VzLCBzbyB0aGF0IG5lc3RlZCB0dXBsZXMgYXJlIGludGVycHJldGVkIGNvcnJlY3RseTsgdGhleSBtYXkgYmUgaW5wdXQgd2l0aCBvciB3aXRob3V0IHN1cnJvdW5kaW5nIHBhcmVudGhlc2VzLCBhbHRob3VnaCBvZnRlbiBwYXJlbnRoZXNlcyBhcmUgbmVjZXNzYXJ5IGFueXdheSAoaWYgdGhlIHR1cGxlIGlzIHBhcnQgb2YgYSBsYXJnZXIgZXhwcmVzc2lvbikuIEl0IGlzIG5vdCBwb3NzaWJsZSB0byBhc3NpZ24gdG8gdGhlIGluZGl2aWR1YWwgaXRlbXMgb2YgYSB0dXBsZSwgaG93ZXZlciBpdCBpcyBwb3NzaWJsZSB0byBjcmVhdGUgdHVwbGVzIHdoaWNoIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzLCBzdWNoIGFzIGxpc3RzLlxcblxcblRob3VnaCB0dXBsZXMgbWF5IHNlZW0gc2ltaWxhciB0byBsaXN0cywgdGhleSBhcmUgb2Z0ZW4gdXNlZCBpbiBkaWZmZXJlbnQgc2l0dWF0aW9ucyBhbmQgZm9yIGRpZmZlcmVudCBwdXJwb3Nlcy4gVHVwbGVzIGFyZSBpbW11dGFibGUsIGFuZCB1c3VhbGx5IGNvbnRhaW4gYSBoZXRlcm9nZW5lb3VzIHNlcXVlbmNlIG9mIGVsZW1lbnRzIHRoYXQgYXJlIGFjY2Vzc2VkIHZpYSB1bnBhY2tpbmcgKHNlZSBsYXRlciBpbiB0aGlzIHNlY3Rpb24pIG9yIGluZGV4aW5nIChvciBldmVuIGJ5IGF0dHJpYnV0ZSBpbiB0aGUgY2FzZSBvZiBuYW1lZHR1cGxlcykuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG5Bbm90aGVyIHVzZWZ1bCBkYXRhIHR5cGUgYnVpbHQgaW50byBQeXRob24gaXMgdGhlIGRpY3Rpb25hcnkgKHNlZSBNYXBwaW5nIFR5cGVzIFxcdTIwMTQgZGljdCkuIERpY3Rpb25hcmllcyBhcmUgc29tZXRpbWVzIGZvdW5kIGluIG90aGVyIGxhbmd1YWdlcyBhcyBcXHUyMDFjYXNzb2NpYXRpdmUgbWVtb3JpZXNcXHUyMDFkIG9yIFxcdTIwMWNhc3NvY2lhdGl2ZSBhcnJheXNcXHUyMDFkLiBVbmxpa2Ugc2VxdWVuY2VzLCB3aGljaCBhcmUgaW5kZXhlZCBieSBhIHJhbmdlIG9mIG51bWJlcnMsIGRpY3Rpb25hcmllcyBhcmUgaW5kZXhlZCBieSBrZXlzLCB3aGljaCBjYW4gYmUgYW55IGltbXV0YWJsZSB0eXBlOyBzdHJpbmdzIGFuZCBudW1iZXJzIGNhbiBhbHdheXMgYmUga2V5cy4gVHVwbGVzIGNhbiBiZSB1c2VkIGFzIGtleXMgaWYgdGhleSBjb250YWluIG9ubHkgc3RyaW5ncywgbnVtYmVycywgb3IgdHVwbGVzOyBpZiBhIHR1cGxlIGNvbnRhaW5zIGFueSBtdXRhYmxlIG9iamVjdCBlaXRoZXIgZGlyZWN0bHkgb3IgaW5kaXJlY3RseSwgaXQgY2Fubm90IGJlIHVzZWQgYXMgYSBrZXkuIFlvdSBjYW5cXHUyMDE5dCB1c2UgbGlzdHMgYXMga2V5cywgc2luY2UgbGlzdHMgY2FuIGJlIG1vZGlmaWVkIGluIHBsYWNlIHVzaW5nIGluZGV4IGFzc2lnbm1lbnRzLCBzbGljZSBhc3NpZ25tZW50cywgb3IgbWV0aG9kcyBsaWtlIGFwcGVuZCgpIGFuZCBleHRlbmQoKS5cIl19LCB7XCJ0aXRsZVwiOiBcIjMuIERhdGEgbW9kZWwgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3JlZmVyZW5jZS9kYXRhbW9kZWwuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiVGhlIHZhbHVlIG9mIHNvbWUgb2JqZWN0cyBjYW4gY2hhbmdlLiBPYmplY3RzIHdob3NlIHZhbHVlIGNhbiBjaGFuZ2UgYXJlIHNhaWQgdG8gYmUgbXV0YWJsZTsgb2JqZWN0cyB3aG9zZSB2YWx1ZSBpcyB1bmNoYW5nZWFibGUgb25jZSB0aGV5IGFyZSBjcmVhdGVkIGFyZSBjYWxsZWQgaW1tdXRhYmxlLiAoVGhlIHZhbHVlIG9mIGFuIGltbXV0YWJsZSBjb250YWluZXIgb2JqZWN0IHRoYXQgY29udGFpbnMgYSByZWZlcmVuY2UgdG8gYSBtdXRhYmxlIG9iamVjdCBjYW4gY2hhbmdlIHdoZW4gdGhlIGxhdHRlclxcdTIwMTlzIHZhbHVlIGlzIGNoYW5nZWQ7IGhvd2V2ZXIgdGhlIGNvbnRhaW5lciBpcyBzdGlsbCBjb25zaWRlcmVkIGltbXV0YWJsZSwgYmVjYXVzZSB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGl0IGNvbnRhaW5zIGNhbm5vdCBiZSBjaGFuZ2VkLiBTbywgaW1tdXRhYmlsaXR5IGlzIG5vdCBzdHJpY3RseSB0aGUgc2FtZSBhcyBoYXZpbmcgYW4gdW5jaGFuZ2VhYmxlIHZhbHVlLCBpdCBpcyBtb3JlIHN1YnRsZS4pIEFuIG9iamVjdFxcdTIwMTlzIG11dGFiaWxpdHkgaXMgZGV0ZXJtaW5lZCBieSBpdHMgdHlwZTsgZm9yIGluc3RhbmNlLCBudW1iZXJzLCBzdHJpbmdzIGFuZCB0dXBsZXMgYXJlIGltbXV0YWJsZSwgd2hpbGUgZGljdGlvbmFyaWVzIGFuZCBsaXN0cyBhcmUgbXV0YWJsZS5cXG4uLi5cXG5Tb21lIG9iamVjdHMgY29udGFpbiByZWZlcmVuY2VzIHRvIG90aGVyIG9iamVjdHM7IHRoZXNlIGFyZSBjYWxsZWQgY29udGFpbmVycy4gRXhhbXBsZXMgb2YgY29udGFpbmVycyBhcmUgdHVwbGVzLCBsaXN0cyBhbmQgZGljdGlvbmFyaWVzLiBUaGUgcmVmZXJlbmNlcyBhcmUgcGFydCBvZiBhIGNvbnRhaW5lclxcdTIwMTlzIHZhbHVlLiBJbiBtb3N0IGNhc2VzLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIHZhbHVlIG9mIGEgY29udGFpbmVyLCB3ZSBpbXBseSB0aGUgdmFsdWVzLCBub3QgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGNvbnRhaW5lZCBvYmplY3RzOyBob3dldmVyLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIG11dGFiaWxpdHkgb2YgYSBjb250YWluZXIsIG9ubHkgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGltbWVkaWF0ZWx5IGNvbnRhaW5lZCBvYmplY3RzIGFyZSBpbXBsaWVkLiBTbywgaWYgYW4gaW1tdXRhYmxlIGNvbnRhaW5lciAobGlrZSBhIHR1cGxlKSBjb250YWlucyBhIHJlZmVyZW5jZSB0byBhIG11dGFibGUgb2JqZWN0LCBpdHMgdmFsdWUgY2hhbmdlcyBpZiB0aGF0IG11dGFibGUgb2JqZWN0IGlzIGNoYW5nZWQuXFxuLi4uXFxuIyMjIDMuMi41LiBTZXF1ZW5jZXNcXHUwMGI2XFxuLi4uXFxuIyMjIyAzLjIuNS4xLiBJbW11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbkFuIG9iamVjdCBvZiBhbiBpbW11dGFibGUgc2VxdWVuY2UgdHlwZSBjYW5ub3QgY2hhbmdlIG9uY2UgaXQgaXMgY3JlYXRlZC4gKElmIHRoZSBvYmplY3QgY29udGFpbnMgcmVmZXJlbmNlcyB0byBvdGhlciBvYmplY3RzLCB0aGVzZSBvdGhlciBvYmplY3RzIG1heSBiZSBtdXRhYmxlIGFuZCBtYXkgYmUgY2hhbmdlZDsgaG93ZXZlciwgdGhlIGNvbGxlY3Rpb24gb2Ygb2JqZWN0cyBkaXJlY3RseSByZWZlcmVuY2VkIGJ5IGFuIGltbXV0YWJsZSBvYmplY3QgY2Fubm90IGNoYW5nZS4pXFxuXFxuVGhlIGZvbGxvd2luZyB0eXBlcyBhcmUgaW1tdXRhYmxlIHNlcXVlbmNlczpcXG4uLi5cXG5UdXBsZXNcXG46IFRoZSBpdGVtcyBvZiBhIGB0dXBsZWAgYXJlIGFyYml0cmFyeSBQeXRob24gb2JqZWN0cy4gVHVwbGVzIG9mIHR3byBvciBtb3JlIGl0ZW1zIGFyZSBmb3JtZWQgYnkgY29tbWEtc2VwYXJhdGVkIGxpc3RzIG9mIGV4cHJlc3Npb25zLiBBIHR1cGxlIG9mIG9uZSBpdGVtIChhIFxcdTIwMThzaW5nbGV0b25cXHUyMDE5KSBjYW4gYmUgZm9ybWVkIGJ5IGFmZml4aW5nIGEgY29tbWEgdG8gYW4gZXhwcmVzc2lvbiAoYW4gZXhwcmVzc2lvbiBieSBpdHNlbGYgZG9lcyBub3QgY3JlYXRlIGEgdHVwbGUsIHNpbmNlIHBhcmVudGhlc2VzIG11c3QgYmUgdXNhYmxlIGZvciBncm91cGluZyBvZiBleHByZXNzaW9ucykuIEFuIGVtcHR5IHR1cGxlIGNhbiBiZSBmb3JtZWQgYnkgYW4gZW1wdHkgcGFpciBvZiBwYXJlbnRoZXNlcy5cXG4uLi5cXG4jIyMjIDMuMi41LjIuIE11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbk11dGFibGUgc2VxdWVuY2VzIGNhbiBiZSBjaGFuZ2VkIGFmdGVyIHRoZXkgYXJlIGNyZWF0ZWQuIFRoZSBzdWJzY3JpcHRpb24gYW5kIHNsaWNpbmcgbm90YXRpb25zIGNhbiBiZSB1c2VkIGFzIHRoZSB0YXJnZXQgb2YgYXNzaWdubWVudCBhbmQgYGRlbGAgKGRlbGV0ZSkgc3RhdGVtZW50cy5cXG4uLi5cXG5UaGVyZSBhcmUgY3VycmVudGx5IHR3byBpbnRyaW5zaWMgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlczpcXG5cXG5MaXN0c1xcbjogVGhlIGl0ZW1zIG9mIGEgbGlzdCBhcmUgYXJiaXRyYXJ5IFB5dGhvbiBvYmplY3RzLiBMaXN0cyBhcmUgZm9ybWVkIGJ5IHBsYWNpbmcgYSBjb21tYS1zZXBhcmF0ZWQgbGlzdCBvZiBleHByZXNzaW9ucyBpbiBzcXVhcmUgYnJhY2tldHMuIChOb3RlIHRoYXQgdGhlcmUgYXJlIG5vIHNwZWNpYWwgY2FzZXMgbmVlZGVkIHRvIGZvcm0gbGlzdHMgb2YgbGVuZ3RoIDAgb3IgMS4pXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjcgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvYnVpbHRpbnMvc3RkdHlwZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiY29sbGVjdGlvbiBjbGFzc2VzIGFyZSBtdXRhYmxlLiBUaGUgbWV0aG9kcyB0aGF0IGFkZCwgc3VidHJhY3QsIG9yIHJlYXJyYW5nZSB0aGVpciAuLi4gaW4gcGxhY2UsIGFuZCBkb25cXHUyMDE5dCByZXR1cm4gYSAuLi4gLCBuZXZlciByZXR1cm4gdGhlIGNvbGxlY3Rpb24gaW5zdGFuY2UgaXRzZWxmIGJ1dCBgTm9uZWAuXFxuLi4uXFxuIyMgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBgbGlzdGAsIGB0dXBsZWAsIGByYW5nZWBcXHUwMGI2XFxuXFxuVGhlcmUgYXJlIHRocmVlIGJhc2ljIHNlcXVlbmNlIHR5cGVzOiBsaXN0cywgdHVwbGVzLCBhbmQgcmFuZ2Ugb2JqZWN0cy4gQWRkaXRpb25hbCBzZXF1ZW5jZSB0eXBlcyB0YWlsb3JlZCBmb3IgcHJvY2Vzc2luZyBvZiBiaW5hcnkgZGF0YSBhbmQgdGV4dCBzdHJpbmdzIGFyZSBkZXNjcmliZWQgaW4gZGVkaWNhdGVkIHNlY3Rpb25zLlxcbi4uLlxcblRoZSBvcGVyYXRpb25zIGluIHRoZSBmb2xsb3dpbmcgdGFibGUgLi4uIG11dGFibGUgYW5kIGltbXV0YWJsZS4gVGhlIGBjb2xsZWN0aW9ucy4gLi4uIGlzIHByb3ZpZGVkIHRvIG1ha2UgLi4uIGVhc2llciB0byBjb3JyZWN0bHkgaW1wbGVtZW50IHRoZXNlIG9wZXJhdGlvbnMgb24gY3VzdG9tIHNlcXVlbmNlIHR5cGVzXFxuLi4uXFxuIyMjIEltbXV0YWJsZSBTZXF1ZW5jZSBUeXBlc1xcdTAwYjZcXG4uLi5cXG5tdXRhYmxlIHNlcXVlbmNlIHR5cGVzIGlzXFxuLi4uXFxuc3VwcG9ydCBhbGxvd3MgaW1tdXRhYmxlIHNlcXVlbmNlcywgLi4uICwgdG8gYmUgdXNlZCBhcyBgZGljdGAga2V5cyBhbmQgc3RvcmVkIGluIC4uLiBlbnNldGAgaW5zdGFuY2VzXFxuLi4uXFxuIyMjIE11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxuIyMjIExpc3RzXFx1MDBiNlxcblxcbkxpc3RzIGFyZSBtdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaG9tb2dlbmVvdXMgaXRlbXMgKHdoZXJlIHRoZSBwcmVjaXNlIGRlZ3JlZSBvZiBzaW1pbGFyaXR5IHdpbGwgdmFyeSBieSBhcHBsaWNhdGlvbikuXFxuLi4uXFxuIyMjIFR1cGxlc1xcdTAwYjZcXG5cXG5UdXBsZXMgYXJlIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhldGVyb2dlbmVvdXMgZGF0YSAoc3VjaCBhcyB0aGUgMi10dXBsZXMgcHJvZHVjZWQgYnkgdGhlIGBlbnVtZXJhdGUoKWAgYnVpbHQtaW4pLiBUdXBsZXMgYXJlIGFsc28gdXNlZCBmb3IgY2FzZXMgd2hlcmUgYW4gaW1tdXRhYmxlIHNlcXVlbmNlIG9mIGhvbW9nZW5lb3VzIGRhdGEgaXMgbmVlZGVkIChzdWNoIGFzIGFsbG93aW5nIHN0b3JhZ2UgaW4gYSBgc2V0YCBvciBgZGljdGAgaW5zdGFuY2UpLlxcbi4uLlxcbnR1cGxlKGl0ZXJhYmxlPVxcbi4uLlxcblRoZSBjb25zdHJ1Y3RvciBidWlsZHMgYSB0dXBsZSB3aG9zZSBpdGVtcyBhcmUgdGhlIHNhbWUgYW5kIGluIHRoZSBzYW1lIG9yZGVyIGFzIGl0ZXJhYmxlXFx1MjAxOXMgaXRlbXMuIGl0ZXJhYmxlIG1heSBiZSBlaXRoZXIgYSBzZXF1ZW5jZSwgYSBjb250YWluZXIgdGhhdCBzdXBwb3J0cyBpdGVyYXRpb24sIG9yIGFuIGl0ZXJhdG9yIG9iamVjdC4gSWYgaXRlcmFibGUgaXMgYWxyZWFkeSBhIHR1cGxlLCBpdCBpcyByZXR1cm5lZCB1bmNoYW5nZWQuIEZvciBleGFtcGxlLCBgdHVwbGUoJ2FiYycpYCByZXR1cm5zIGAoJ2EnLCAnYicsICdjJylgIGFuZCBgdHVwbGUoIFsxLCAyLCAzXSApYCByZXR1cm5zIGAoMSwgMiwgMylgLiBJZiBubyBhcmd1bWVudCBpcyBnaXZlbiwgdGhlIGNvbnN0cnVjdG9yIGNyZWF0ZXMgYSBuZXcgZW1wdHkgdHVwbGUsIGAoKWAuXFxuLi4uXFxuVHVwbGVzIGltcGxlbWVudCBhbGwgb2YgdGhlIGNvbW1vbiBzZXF1ZW5jZSBvcGVyYXRpb25zLlxcbi4uLlxcbkZvciBoZXRlcm9nZW5lb3VzIGNvbGxlY3Rpb25zIG9mIGRhdGEgd2hlcmUgYWNjZXNzIGJ5IG5hbWUgaXMgY2xlYXJlciB0aGFuIGFjY2VzcyBieSBpbmRleCwgYGNvbGxlY3Rpb25zLm5hbWVkdHVwbGUoKWAgbWF5IGJlIGEgbW9yZSBhcHByb3ByaWF0ZSBjaG9pY2UgdGhhbiBhIHNpbXBsZSB0dXBsZSBvYmplY3QuXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvbGlicmFyeS9zdGR0eXBlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJTb21lIGNvbGxlY3Rpb24gY2xhc3NlcyBhcmUgbXV0YWJsZS4gVGhlIG1ldGhvZHMgdGhhdCBhZGQsIHN1YnRyYWN0LCBvciByZWFycmFuZ2UgdGhlaXIgLi4uIGluIHBsYWNlLCBhbmQgZG9uXFx1MjAxOXQgcmV0dXJuIGEgLi4uICwgbmV2ZXIgcmV0dXJuIHRoZSBjb2xsZWN0aW9uIGluc3RhbmNlIGl0c2VsZiBidXQgYE5vbmVgLlxcbi4uLlxcbiMjIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgYGxpc3RgLCBgdHVwbGVgLCBgcmFuZ2VgXFx1MDBiNlxcblxcblRoZXJlIGFyZSB0aHJlZSBiYXNpYyBzZXF1ZW5jZSB0eXBlczogbGlzdHMsIHR1cGxlcywgYW5kIC4uLiBvYmplY3RzLiBBZGRpdGlvbmFsIHNlcXVlbmNlIHR5cGVzIHRhaWxvcmVkIGZvciBwcm9jZXNzaW5nIG9mIGJpbmFyeSBkYXRhIGFuZCB0ZXh0IHN0cmluZ3MgYXJlIGRlc2NyaWJlZCBpbiBkZWRpY2F0ZWQgc2VjdGlvbnMuXFxuLi4uXFxuVGhlIG9wZXJhdGlvbnMgaW4gdGhlIGZvbGxvd2luZyB0YWJsZSAuLi4gaW1tdXRhYmxlLiBUaGUgYCAuLi4gaXMgcHJvdmlkZWQgdG8gbWFrZSAuLi4gZWFzaWVyIHRvIGNvcnJlY3RseSBpbXBsZW1lbnQgdGhlc2Ugb3BlcmF0aW9ucyBvbiBjdXN0b20gc2VxdWVuY2UgdHlwZXNcXG4uLi5cXG4jIyMgSW1tdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbm9wZXJhdGlvbiB0aGF0IGltbXV0YWJsZSBzZXF1ZW5jZSAuLi4gYnkgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlcyBpcyAuLi4gYGhhc2goKWBcXG4uLi5cXG5UaGlzIHN1cHBvcnQgYWxsb3dzIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHN1Y2ggYXMgYHR1cGxlYCBpbnN0YW5jZXMsIHRvIGJlIHVzZWQgYXMgYGRpY3RgIGtleXMgYW5kIHN0b3JlZCBpbiBgc2V0YCBhbmQgYGZyb3plbnNldGAgaW5zdGFuY2VzLlxcbi4uLlxcbiMjIyBNdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbiMjIyBMaXN0c1xcdTAwYjZcXG5cXG5MaXN0cyBhcmUgbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhvbW9nZW5lb3VzIGl0ZW1zICh3aGVyZSB0aGUgcHJlY2lzZSBkZWdyZWUgb2Ygc2ltaWxhcml0eSB3aWxsIHZhcnkgYnkgYXBwbGljYXRpb24pLlxcbi4uLlxcbiMjIyBUdXBsZXNcXHUwMGI2XFxuXFxuVHVwbGVzIGFyZSBpbW11dGFibGUgc2VxdWVuY2VzLCB0eXBpY2FsbHkgdXNlZCB0byBzdG9yZSBjb2xsZWN0aW9ucyBvZiBoZXRlcm9nZW5lb3VzIGRhdGEgKHN1Y2ggYXMgdGhlIDItdHVwbGVzIHByb2R1Y2VkIGJ5IHRoZSBgZW51bWVyYXRlKClgIGJ1aWx0LWluKS4gVHVwbGVzIGFyZSBhbHNvIHVzZWQgZm9yIGNhc2VzIHdoZXJlIGFuIGltbXV0YWJsZSBzZXF1ZW5jZSBvZiBob21vZ2VuZW91cyBkYXRhIGlzIG5lZWRlZCAoc3VjaCBhcyBhbGxvd2luZyBzdG9yYWdlIGluIGEgYHNldGAgb3IgYGRpY3RgIGluc3RhbmNlKS5cXG4uLi5cXG5UaGUgY29uc3RydWN0b3IgYnVpbGRzIGEgdHVwbGUgd2hvc2UgaXRlbXMgYXJlIHRoZSBzYW1lIGFuZCBpbiB0aGUgc2FtZSBvcmRlciBhcyBpdGVyYWJsZVxcdTIwMTlzIGl0ZW1zLiBpdGVyYWJsZSBtYXkgYmUgZWl0aGVyIGEgc2VxdWVuY2UsIGEgY29udGFpbmVyIHRoYXQgc3VwcG9ydHMgaXRlcmF0aW9uLCBvciBhbiBpdGVyYXRvciBvYmplY3QuIElmIGl0ZXJhYmxlIGlzIGFscmVhZHkgYSB0dXBsZSwgaXQgaXMgcmV0dXJuZWQgdW5jaGFuZ2VkLiBGb3IgZXhhbXBsZSwgYHR1cGxlKCdhYmMnKWAgcmV0dXJucyBgKCdhJywgJ2InLCAnYycpYCBhbmQgYHR1cGxlKCBbMSwgMiwgM10gKWAgcmV0dXJucyBgKDEsIDIsIDMpYC4gSWYgbm8gYXJndW1lbnQgaXMgZ2l2ZW4sIHRoZSBjb25zdHJ1Y3RvciBjcmVhdGVzIGEgbmV3IGVtcHR5IHR1cGxlLCBgKClgLlxcbi4uLlxcblR1cGxlcyBpbXBsZW1lbnQgYWxsIG9mIHRoZSBjb21tb24gc2VxdWVuY2Ugb3BlcmF0aW9ucy5cXG4uLi5cXG5Gb3IgaGV0ZXJvZ2VuZW91cyBjb2xsZWN0aW9ucyBvZiBkYXRhIHdoZXJlIGFjY2VzcyBieSBuYW1lIGlzIGNsZWFyZXIgdGhhbiBhY2Nlc3MgYnkgaW5kZXgsIGBjb2xsZWN0aW9ucy5uYW1lZHR1cGxlKClgIG1heSBiZSBhIG1vcmUgYXBwcm9wcmlhdGUgY2hvaWNlIHRoYW4gYSBzaW1wbGUgdHVwbGUgb2JqZWN0LlwiXX0sIHtcInRpdGxlXCI6IFwiNS4gRGF0YSBTdHJ1Y3R1cmVzIFxcdTIwMTQgUHl0aG9uIDMuMTAuMjAgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMuMTAvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiWW91IG1pZ2h0IGhhdmUgbm90aWNlZCB0aGF0IG1ldGhvZHMgbGlrZSBgaW5zZXJ0YCwgYHJlbW92ZWAgb3IgYHNvcnRgIHRoYXQgb25seSBtb2RpZnkgdGhlIGxpc3QgaGF2ZSBubyByZXR1cm4gdmFsdWUgcHJpbnRlZCBcXHUyMDEzIHRoZXkgcmV0dXJuIHRoZSBkZWZhdWx0IGBOb25lYC4gMSBUaGlzIGlzIGEgZGVzaWduIHByaW5jaXBsZSBmb3IgYWxsIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIGluIFB5dGhvbi5cXG4uLi5cXG4jIyA1LjMuIFR1cGxlcyBhbmQgU2VxdWVuY2VzXFx1MDBiNlxcblxcbldlIHNhdyB0aGF0IGxpc3RzIGFuZCBzdHJpbmdzIGhhdmUgbWFueSBjb21tb24gcHJvcGVydGllcywgc3VjaCBhcyBpbmRleGluZyBhbmQgc2xpY2luZyBvcGVyYXRpb25zLiBUaGV5IGFyZSB0d28gZXhhbXBsZXMgb2Ygc2VxdWVuY2UgZGF0YSB0eXBlcyAoc2VlIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgbGlzdCwgdHVwbGUsIHJhbmdlKS4gU2luY2UgUHl0aG9uIGlzIGFuIGV2b2x2aW5nIGxhbmd1YWdlLCBvdGhlciBzZXF1ZW5jZSBkYXRhIHR5cGVzIG1heSBiZSBhZGRlZC4gVGhlcmUgaXMgYWxzbyBhbm90aGVyIHN0YW5kYXJkIHNlcXVlbmNlIGRhdGEgdHlwZTogdGhlIHR1cGxlLlxcblxcbkEgdHVwbGUgY29uc2lzdHMgb2YgYSBudW1iZXIgb2YgdmFsdWVzIHNlcGFyYXRlZCBieSBjb21tYXMsIGZvciBpbnN0YW5jZTpcXG4uLi5cXG4+Pj4gIyBUdXBsZXMgYXJlIGltbXV0YWJsZTpcXG4uLi4gdFswXSA9IDg4ODg4XFxuVHJhY2ViYWNrIChtb3N0IHJlY2VudCBjYWxsIGxhc3QpOlxcbiAgRmlsZSBcXFwiPHN0ZGluPlxcXCIsIGxpbmUgMSwgaW4gPG1vZHVsZT5cXG5UeXBlRXJyb3I6ICd0dXBsZScgb2JqZWN0IGRvZXMgbm90IHN1cHBvcnQgaXRlbSBhc3NpZ25tZW50XFxuPj4+ICMgYnV0IHRoZXkgY2FuIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzOlxcbi4uLiB2ID0gKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbj4+PiB2XFxuKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbi4uLlxcbkFzIHlvdSBzZWUsIG9uIG91dHB1dCB0dXBsZXMgYXJlIGFsd2F5cyBlbmNsb3NlZCBpbiBwYXJlbnRoZXNlcywgc28gdGhhdCBuZXN0ZWQgdHVwbGVzIGFyZSBpbnRlcnByZXRlZCBjb3JyZWN0bHk7IHRoZXkgbWF5IGJlIGlucHV0IHdpdGggb3Igd2l0aG91dCBzdXJyb3VuZGluZyBwYXJlbnRoZXNlcywgYWx0aG91Z2ggb2Z0ZW4gcGFyZW50aGVzZXMgYXJlIG5lY2Vzc2FyeSBhbnl3YXkgKGlmIHRoZSB0dXBsZSBpcyBwYXJ0IG9mIGEgbGFyZ2VyIGV4cHJlc3Npb24pLiBJdCBpcyBub3QgcG9zc2libGUgdG8gYXNzaWduIHRvIHRoZSBpbmRpdmlkdWFsIGl0ZW1zIG9mIGEgdHVwbGUsIGhvd2V2ZXIgaXQgaXMgcG9zc2libGUgdG8gY3JlYXRlIHR1cGxlcyB3aGljaCBjb250YWluIG11dGFibGUgb2JqZWN0cywgc3VjaCBhcyBsaXN0cy5cXG5cXG5UaG91Z2ggdHVwbGVzIG1heSBzZWVtIHNpbWlsYXIgdG8gbGlzdHMsIHRoZXkgYXJlIG9mdGVuIHVzZWQgaW4gZGlmZmVyZW50IHNpdHVhdGlvbnMgYW5kIGZvciBkaWZmZXJlbnQgcHVycG9zZXMuIFR1cGxlcyBhcmUgaW1tdXRhYmxlLCBhbmQgdXN1YWxseSBjb250YWluIGEgaGV0ZXJvZ2VuZW91cyBzZXF1ZW5jZSBvZiBlbGVtZW50cyB0aGF0IGFyZSBhY2Nlc3NlZCB2aWEgdW5wYWNraW5nIChzZWUgbGF0ZXIgaW4gdGhpcyBzZWN0aW9uKSBvciBpbmRleGluZyAob3IgZXZlbiBieSBhdHRyaWJ1dGUgaW4gdGhlIGNhc2Ugb2YgYG5hbWVkdHVwbGVzYCkuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG4jIyA1LjQuXFxuLi4uXFxuLiBTZXQgb2JqZWN0c1xcbi4uLlxcbiMjIDUuNS4gRGljdGlvbmFyaWVzXFx1MDBiNlxcblxcbkFub3RoZXIgdXNlZnVsIGRhdGEgdHlwZSBidWlsdCBpbnRvIFB5dGhvbiBpcyB0aGUgZGljdGlvbmFyeSAoc2VlIE1hcHBpbmcgVHlwZXMgXFx1MjAxNCBkaWN0KS4gRGljdGlvbmFyaWVzIGFyZSBzb21ldGltZXMgZm91bmQgaW4gb3RoZXIgbGFuZ3VhZ2VzIGFzIFxcdTIwMWNhc3NvY2lhdGl2ZSBtZW1vcmllc1xcdTIwMWQgb3IgXFx1MjAxY2Fzc29jaWF0aXZlIGFycmF5c1xcdTIwMWQuIFVubGlrZSBzZXF1ZW5jZXMsIHdoaWNoIGFyZSBpbmRleGVkIGJ5IGEgcmFuZ2Ugb2YgbnVtYmVycywgZGljdGlvbmFyaWVzIGFyZSBpbmRleGVkIGJ5IGtleXMsIHdoaWNoIGNhbiBiZSBhbnkgaW1tdXRhYmxlIHR5cGU7IHN0cmluZ3MgYW5kIG51bWJlcnMgY2FuIGFsd2F5cyBiZSBrZXlzLiBUdXBsZXMgY2FuIGJlIHVzZWQgYXMga2V5cyBpZiB0aGV5IGNvbnRhaW4gb25seSBzdHJpbmdzLCBudW1iZXJzLCBvciB0dXBsZXM7IGlmIGEgdHVwbGUgY29udGFpbnMgYW55IG11dGFibGUgb2JqZWN0IGVpdGhlciBkaXJlY3RseSBvciBpbmRpcmVjdGx5LCBpdCBjYW5ub3QgYmUgdXNlZCBhcyBhIGtleS4gWW91IGNhblxcdTIwMTl0IHVzZSBsaXN0cyBhcyBrZXlzLCBzaW5jZSBsaXN0cyBjYW4gYmUgbW9kaWZpZWQgaW4gcGxhY2UgdXNpbmcgaW5kZXggYXNzaWdubWVudHMsIHNsaWNlIGFzc2lnbm1lbnRzLCBvciBtZXRob2RzIGxpa2UgYGFwcGVuZCgpYCBhbmQgYGV4dGVuZCgpYC5cIl19XSIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ3ZWJfc2VhcmNoIiwiaWQiOiI2ZTUxMjVhYy0zMTY0LTRhNzYtOWM1Mi1hYzM0NjBjZTM2ZTQiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJzdGF0dXMiOiJzdWNjZXNzIn0seyJjb250ZW50IjpbeyJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3NrZXB0aWMiLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QTZGNGNDTjV5aEQ0Q3JzOU0iLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjB9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InJlc2VhcmNoZXIiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtM2NkOC03OWYxLTg4YTEtY2U2Y2VlMjk0ZTc5LTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoidHJhbnNmZXJfdG9fc2tlcHRpYyIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsInRvdGFsX3Rva2VucyI6NzQxMSwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fSx7ImNvbnRlbnQiOiJTdWNjZXNzZnVsbHkgdHJhbnNmZXJyZWQgdG8gc2tlcHRpYyIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ0cmFuc2Zlcl90b19za2VwdGljIiwiaWQiOiIzODkwNThkMS0wODNmLTQ2MzItYmJjNS0wOWJmZDNhMGZmZDIiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMVhqajNhUmRXeWhCSmZQbVRKQnhRYTkiLCJzdGF0dXMiOiJzdWNjZXNzIn1dfQ==" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJvdXRwdXQiOlt7ImdyYXBoIjpudWxsLCJ1cGRhdGUiOnsibWVzc2FnZXMiOlt7ImNvbnRlbnQiOlt7InNpZ25hdHVyZSI6IkNBUVMxUWdLRUFnU0dBSTRBVUlJZEdocGJtdHBibWNTRE42VGZVNk5ETTk3MWxFYnp4b012UlVBMUo5b3gzZkpRWVQ5SWpBckNRUUdIQlA0cFJIUGF2WHpSbGJNYmFZSEM1N1lqK0dieGdyMGhlbHZkMk9JTC9vNlJidDRQRkEzRmFJRlVsZ3E4Z2M3S1FJOUZ0R252U0FrTEJSeklzVXAvMmQ1UmQyemdFc2ZrRlZlWHNuOFdZWVpraW5BUVZWeTZBN0IzTGFFSlcweGIveDg4RGk2N2ZlZHd4MFErRmxadHdjbVQzRGNhYTExSnZleUhScjNkbSt6R1NaUDZDWGVYTko4bnJUWjdXMVlSVHlzRDZ3N283THA4aDFSd3E0ZjRvOGorUVVmWTB2amUrekpac3hIUG1RTnQzeWtlS2g5OWhnbjZkYml6Ni9FSHFtYkhqU0F1MU00U3FFRGZxSUtUaEhZZW5hbUFabWNRNFhBT282bWRRNis3N1ZncFYxbEMxL2s1MklpVW9KamtHbFFybkxDdnV0aUk3S04rc2dLOGMyKzJLQ0pyUFJDdlZWK0JrU3R2UDVqV1dxenl0cEtRK25IemhMNDQ4T3JJQTlnZEpDMzlDOGVEclhlUm5JN3Q0RnFsa1hHb01sWkFWOVVNY3JRSFJ4T01pOHRPeVR6aTlveUJGRFRTc1ErTEpxZXIwdHoxRnQ5Z2VrNnZ6ZzUwU3BWQUZRZExFUnY4UjFZcU1hZUkreHN2VEFFVzlNUmd2Uk1rekx1elJUMmd1dzNqRCtGamlwMzd4Z0s1UUkwUitDc09BbVFQb1RaaXp6Y0l5QVVBdUNKRkNncS9nRC9DaWJMeG95bWlWVUkxYW45U250M0twTlJvV2R2V0luZTMzVjBwdjhQSGc5eVcySnVMT3FRR2lsTENzSy9RM29RcjN1cEVNWHhYR2pDTDdXVWl6dlJaR0ZFRVYrVnBsWXQ3djR0S1pNajVwazN2ZGVNME1KNVhaZzdQdlVzUnp5Y3hjbnJUM0tub2MzcU0rdHRxWmZUc2I4ZDZVL3BRaWZxN0d1YWhGT0hydnJvcWdKTGorLzgxWXkxcFpGa2pmSmdZMFhrbHpBbkE2Q0ZxL1lUMGd4YlYrVEg1SVMrV2FZT1V6SzM2UzhzNXFxTXdobmd2NEl6RTNjdFhYcW5oSVpYTnpueVZCcWJZYS9HeDJiQ2pjRWoybjVSeUdZUlMvK01jdWxnTEJ6blE5TWtHMkQ3UVAzTXEvbndIWEtNSWFMUzhWSzhoRzNkMHJBamx2bmF1WFNoTnJKeHRrajRxa3dueEQ0THJFamsxdWUrUExjYkxxWVd3QXI4bjBzKzQxanJzSVBWeXlLOXZGYUVackE5S2FCeGtKeWlxeDUwR1pObjdja3grbWtJMThnSDUyaXlWUHRVOEZLTVhqdWdDbDZtUExpSFlpRFFkK0c2M0x1ZG1PS3lzOWl0Vm5CREJOTVAyYi9JNytiSVZSbGhlR2FaV3I2TUFUS2w4NkIzeSswa3VPckxSTXlXY2RpYmMyaVFrWm0rOGY3ZFRVd2JLYkJKc0tPRlk3My84bmxML0ppWXBqejZuVUlrK0lxU2phRnlLNlhGbENjdWtXcFdIcERKblN2YkV6ZXk3cUh5Q3FKK0krb244aVA4R090ZW1wUWFTMC80a1pHU3dsckU2eE9qNkV3R3BmamJEWktIa1AvMlJBZE5ZUFFxM3o3YmRWSk1KTm5GTXo2ZTM3bXpNcENCaWh3UGRTRlRSRTNTQ1k2Nnd2VGdKekdDVXloSGJTaTVpMWF6ek9YZDh1SENTcFQvNkwxbzZWVTBwam9SOEthYTR3Q1dIR2Y3UC9XRWQrYXZ5cDAySm51MWNtcFcwK0dxK3ZLOWgwdGRiMklDbE0vZU5mZG91UVArZXA4TUpVanJhRFJJQ21BSlFZNW9iUzcxdm5pT28rcmVhRnkwelJnQiIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJzaWduYXR1cmUiOiJDQVFTL1JFS0VRZ1NHQUk0QVVJSmJtRnljbUYwYVc5dUVnd0ZoM0FWSlpQM3M4eTFIcTBhREc3NnVoSE9OYVNjWEFHU2hpSXdHZ2pMWEhVeDF5WnJrTFBsamp6VE9ReWNwTEppS1Z3WGN4bS9FektKbTFzOE01V3h1UkVXRWxkWGNvYU4wL21zS3BrUk1UK2VMYjVpaDlyQUhVTFRvbVVOc1V3SmV1MGpySmU0R0xhenk3c1A1S3RqRU9FQktRc1hVZm9RN2ZEa1pPUVVIWmk5enpRTjRoRk5tWUpVZjNPVnFmOVRRTmRDMSttLy9IQVpQZldtQXk2SHREdm03a2dCSmtDRW9pRm5tN3RuL1BYMWhYR1BSZ1djNkJUWFVNcjJXQThhS3lxZ2dSVWJNSDIrSll2NWlXWmRkQ2MxdXAxTmJyM3NGN1dQY290OVpCaHlIUVhyNjkyemVKazZTSmd5Z0lWckFTYk1seTJiQnhUV0pCNjIzZ1ZnenBDbVZTTFpGcGtLWGdQY3A4aEd5REtPdkY2U2lRU0hxR1J4UW9mcjdWczFhUGhmWml4Uk9KR0QrTWxya2dnVkFuR015WUlEVVpwYXZ0Q2VpVTFKbUJTejAwODRiWFB0VC90bnBzS09uU1BZWlFvM0IwQlhLYnFBL1dzS25nOC9WM1o4VXVham82SVZaMmhnQTFXRGR1cVFPUWtFeEdVNnBGdUxoZWJJanVXeFNOSS9EZmZ2Ui8vTE5xQ2Z2ZG5mMGh2b1B0RHhmeC9TYXdJeDQvc3BxOXhid0Z6SHRMZ29IZzFGMndGdzdjbFBEbGcxUHlEL3YyeUpHM1Y5VDhHTDZqR1BFNnJoZmlsL3U0SE52ZjdCVzBkUXpDVFRYY3FaeTRuQnl5MCtwK1M3WnhmTWw0S3B4YTJnT2hoU05qZnBmdVgwUXBMNGtNRThKMW5saUZOMWQ4S3FFUHVESTE4QlFIVWhEL1VmWTI5b0EweDUxMEZqTS9Cc0xaNG41c3Bwa29JaTdDRm9RcTJhQUtWTmZDMUtpbGhJcFJEKzFkeUR0WlFrbE44Z1pwT0VTSHc2TUwxVHRVMHlmQWNibXFveEo2aGhqendINkNNc1RRRUV5OE1iUkJNK1FLS2tJUmRSQ1BIR0ZXMkxrdVI1MCtKMHBGL3RyNnBxVnZKZW96ZmlNbnJFaWxCMjdUWlA0bktUSkc4cmQvL3ZDV2VuMHk3Q0EvOGRCSjlnZHA1dHg3TlZBNDVrMWdkY3hFUjFnWW91TlgzbjF2RzMvQjFGNCs5M3dYVzlNcW82VXM1OVRRbXJiOU5xYTdwU1FOU3JIMDZ3MUd4eUJBbGtGZnFBbGFmazE2YU1wcTQxNlRKWXBHeTQzV0NVaUNjTFdsaGlXZ3oyc2dPTEFYUDlhNXdlV0FLUkM4SjZqa2NVa0RlWG5XK0hGU2lQTTYrOUJvNmtWaExVaE41cmM0bUhaT1pwRjdwdFBwc1FONFhDcUxQc3FwUkgvOG81WHB5QzZabmVUZUYyRXh0amh3WlZqWER3TWJJelZZSWJ3MXkyUFBHdzhkQzdwYzRzSVZTbkk0b1FBSDB2NjUwZ2hmaG1NZXZXVzRoNFB5bGJ3L0RHNGZkZVQzKzh6MGY4TU9TVHRmaXB4MDUvUXR0SGxBdk9EUnhhNXlEQ3ZPejZuM2YxQk8wTi80TWJHeU9NMjdxVzR4OG1CVm9yeVVaV2ZzbWhmaTAvR2ZOdkI4K3lsVEdiWU5YMjdPY0VGNVdadE5xYldFeTdWNUdibVpVU3hzcGNnN0ZCWjNqMy8zTDdtdXZaWG9aaTFmNWVycTR5M2xTSCtvRjdxUEZudmlleUxPQ3d1YmNReStveW93ajRjRDBpZFJlY25wK3MrNTJENyttekYxZ3BXK2YrY0phc294aWdieGtPbG9TNHllaUt2Z000bldTcEdFajEyUlB4NW84MEVtemlYTjl0NHpKelcxaHRBNWhYSkplRnltcE1ZdHlDVUNldlJXVm1yUllrdFE3VW5DYTZ0TC9BVlBpMW5pNVhDWEhmTE91cWZLNUtMWXhqd1Z0ZGhoZm1DTHZwYUtWTkNFT0NmNG5OR0hxRDk4d1ViMGVqV053RFlCbkpHa0JSdzlSVnNwZndOZG9Xb1ZjbjJxdjh5d0dVcDd5Yy9zazJMcEk2VnppUnl2V3FiR05HMVpKcVZWdXRSYzQ2K3NPRTRWU2VTMi9DVE5LTmE2Tk1DZi9oeHhhVVZzWWZLZFArUzFMYzRpYW5LcDBzVXVnUHBtdG95ZEMwQ1AyUndqbkhMY256eENudnF4bExRV1hNTlBUc3UyeUJjSGxXSkx0Y21kZy9NY0xhZ3k3bDVrRzZzTjdaeEpMRC9rQXg4RTZ1cW9oUCtLL2pNU3JZdVFKbmhyNHRmNGJpV0ZZM0p6Zi9LcEFZeVZTcStwYmZGS1hNYkw5bkhFb1pRVDE1V3lhYmQ2RE1BcTNRWk90aE01cFV6ZE5xVVhrY3JJMVB3akFTQWc0ZFZVLzJvKzk4bVRYbnloUDdWbEVrVG5kZHB3eUpGK1lDQ2MwMXBxYTZaWDk5cG5SK1lPcElyYjVEdFBjS3EwdDVjRkNEeVF4TjFsVXVxWmNZdWp2WC9HQzcrNklwUlR6L05rRVkvNWNudE9mWkp5ZDZTa1V4L0pSNUFKSVJQQ05laWIwcmNXbm5GbjBYSzhzMjJ4NDB3TGpFdHVmWERPOSt5cmFmNGtIeGxCSTNTQjBtZU1qZXBYNTBDSmhBeDdOZWtrUkZud29lY0xmcUl5cmpYcmdBWnNUTVBpWDk0c25qZHJ6aXNBWHRCS2FudWxpbFN4T2ZXMkhTbWZuazBMMkdYSEpKM1MrdkNiZ1JFbWw5ZGVrZm1IaG13RVl0djNrZmh0aDdFUkl3VlQ4ajY5T3RJQkNtZVRJSnVBN283M1BYYzFMSjJ3aHVaSzdGWmU2TlRPczYxTVA3SlZsSWM2dGJodE9CazNOWFU4cEJQT0NZNlE1NWQ2M0Z0cFVVL0tDYlEwSzJXUTlvNWpOR3dOTko2Q3c1RkRDdCt4NVlKZVAxZDJRekRTaXErY1NacXdxQ1pWY05vQWZ0R0d6Z3JSWjR2a3BiUDB4UVpIQ3RXc1V3SFZUanJZNE5mUkRqSGRWUFJuOEo5WHRvb2MvaElXa3F6RmFjellUbGFwdTJSQ0pxZThhRW8zanpXWkNFSjdBaUJ6QllyYVlEZ09mZWlReWVKVXNiYlhIZEdLaWl3MjJCZHA0TStZejY3MU1aMGllOUVXUmtzMFEyREhHVVRuZTlTNHRBQUpQR2JVZjR4dENNb0o0MFE0UVcwNVFMU1p5M3dORkFVRlltWEU0RUNERkI4K0lFZU5GRkYvdGdBbmZoOEd2RmgvMXhZK0xFK296TW1pbmpTUndocTA5elFBRWtORnl1MElLZ3ZEa3lPSStpY2NGN2RvV0xUVWFzUHhMSDF4R1BCU0trZVdDblNvWC80QzJjOWR1NjZCYlJYYW1IcWFlK1ZDaVM2Qk45NTNpWUtPdThyTWtuZVcrVEZqVjBDekFmOFBORzlMQmF5VWdZNVV3OXgwU2FwNnpJNFBwYVpLSDRkOWt0a0ZVKzkxeUJiTjVTUnRkZDgvRVFhdEpYbUJOZnc3a1JybGwrTGp3WndlV0xFYjBqVDFScEd1RHFITGRMVE1NZ293U2N6cnAyekNzSmRRSC9zZWFZcnB3a0NGUjhkUmpNN0pSVWtWYTFBMFN2ZFpBWDc1dnVCdnZMQnpOMmMrUmJ0NW9xeTkzbHFJQklqS2I3dzJVa1JUYjRwQldjTktOUFVndDNhei9QeFF4RWFzYWx0ZG11eXZzWFlidXIzVlBObjhKek4vNko1TitRQ256NTcxVGNjYXBDbWtQdkZFK2dheVhVK1VGMVZLWXlGY1V2bGV2N2xzUWxtanF6SzJRQThYN1EzVHpGYXBxaHZKS2x0N2R6QkhyYXFaQmdQZ2I5TjRvS05heWk4RlpUNzEra2FZaEloRnJFU0tZUkhmSGtqNW4za2ZoZ0x5K3grakxEYkt4TWdna2dIbkp6OVJ1RUhaWEJXdGtZQVE9PSIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJpZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3ZlcmlmaWVyIiwidHlwZSI6InRvb2xfdXNlIiwidG9vbHNldF9uYW1lIjpudWxsfV0sImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2Zkd0FNUmRTMjFGZUpmelFoTTNzIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjczOTYsIm91dHB1dF90b2tlbnMiOjc3MCwib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6NzM5fSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCIsIm1vZGVsX3Byb3ZpZGVyIjoiYW50aHJvcGljIn0sInR5cGUiOiJhaSIsIm5hbWUiOiJza2VwdGljIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTRhZTktNzZjMC04OWE2LTRmMGIxMTRlMzAzMC0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6InRyYW5zZmVyX3RvX3ZlcmlmaWVyIiwiYXJncyI6e30sImlkIjoidG9vbHVfMDFINmZWOEp2UkpOdlh5NDdlUm5kM2pIIiwidHlwZSI6InRvb2xfY2FsbCJ9XSwiaW52YWxpZF90b29sX2NhbGxzIjpbXSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjo3Mzk2LCJvdXRwdXRfdG9rZW5zIjo3NzAsInRvdGFsX3Rva2VucyI6ODE2NiwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6NzM5fX19XX0sInJlc3VtZSI6bnVsbCwiZ290byI6W119XX0=" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "87fbd88889f37ec8", + "parentSpanId": "709919ff60557ac4", + "name": "FilesystemMiddleware.wrap_model_call", + "kind": 1, + "startTimeUnixNano": "1790968023781179904", + "endTimeUnixNano": "1790968030834503936", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "e30=" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "FilesystemMiddleware.wrap_model_call" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "1" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "skeptic:3dfc4328-0d19-e1ea-bcda-3d3d36d7cda8|model:41713ca4-d95b-3dfd-185e-53f884946d93" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "skeptic" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "skeptic:3dfc4328-0d19-e1ea-bcda-3d3d36d7cda8" + } + }, + { + "key": "langsmith.metadata.ls_method", + "value": { + "stringValue": "traceable" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJvdXRwdXQiOnsicmVzdWx0IjpbeyJjb250ZW50IjpbeyJzaWduYXR1cmUiOiJDQVFTMVFnS0VBZ1NHQUk0QVVJSWRHaHBibXRwYm1jU0RONlRmVTZORE05NzFsRWJ6eG9NdlJVQTFKOW94M2ZKUVlUOUlqQXJDUVFHSEJQNHBSSFBhdlh6UmxiTWJhWUhDNTdZaitHYnhncjBoZWx2ZDJPSUwvbzZSYnQ0UEZBM0ZhSUZVbGdxOGdjN0tRSTlGdEdudlNBa0xCUnpJc1VwLzJkNVJkMnpnRXNma0ZWZVhzbjhXWVlaa2luQVFWVnk2QTdCM0xhRUpXMHhiL3g4OERpNjdmZWR3eDBRK0ZsWnR3Y21UM0RjYWExMUp2ZXlIUnIzZG0rekdTWlA2Q1hlWE5KOG5yVFo3VzFZUlR5c0Q2dzdvN0xwOGgxUndxNGY0bzhqK1FVZlkwdmplK3pKWnN4SFBtUU50M3lrZUtoOTloZ242ZGJpejYvRUhxbWJIalNBdTFNNFNxRURmcUlLVGhIWWVuYW1BWm1jUTRYQU9vNm1kUTYrNzdWZ3BWMWxDMS9rNTJJaVVvSmprR2xRcm5MQ3Z1dGlJN0tOK3NnSzhjMisyS0NKclBSQ3ZWVitCa1N0dlA1aldXcXp5dHBLUStuSHpoTDQ0OE9ySUE5Z2RKQzM5QzhlRHJYZVJuSTd0NEZxbGtYR29NbFpBVjlVTWNyUUhSeE9NaTh0T3lUemk5b3lCRkRUU3NRK0xKcWVyMHR6MUZ0OWdlazZ2emc1MFNwVkFGUWRMRVJ2OFIxWXFNYWVJK3hzdlRBRVc5TVJndlJNa3pMdXpSVDJndXczakQrRmppcDM3eGdLNVFJMFIrQ3NPQW1RUG9UWml6emNJeUFVQXVDSkZDZ3EvZ0QvQ2liTHhveW1pVlVJMWFuOVNudDNLcE5Sb1dkdldJbmUzM1YwcHY4UEhnOXlXMkp1TE9xUUdpbExDc0svUTNvUXIzdXBFTVh4WEdqQ0w3V1VpenZSWkdGRUVWK1ZwbFl0N3Y0dEtaTWo1cGszdmRlTTBNSjVYWmc3UHZVc1J6eWN4Y25yVDNLbm9jM3FNK3R0cVpmVHNiOGQ2VS9wUWlmcTdHdWFoRk9IcnZyb3FnSkxqKy84MVl5MXBaRmtqZkpnWTBYa2x6QW5BNkNGcS9ZVDBneGJWK1RINUlTK1dhWU9VekszNlM4czVxcU13aG5ndjRJekUzY3RYWHFuaElaWE56bnlWQnFiWWEvR3gyYkNqY0VqMm41UnlHWVJTLytNY3VsZ0xCem5ROU1rRzJEN1FQM01xL253SFhLTUlhTFM4Vks4aEczZDByQWpsdm5hdVhTaE5ySnh0a2o0cWt3bnhENExyRWprMXVlK1BMY2JMcVlXd0FyOG4wcys0MWpyc0lQVnl5Szl2RmFFWnJBOUthQnhrSnlpcXg1MEdaTm43Y2t4K21rSTE4Z0g1Mml5VlB0VThGS01YanVnQ2w2bVBMaUhZaURRZCtHNjNMdWRtT0t5czlpdFZuQkRCTk1QMmIvSTcrYklWUmxoZUdhWldyNk1BVEtsODZCM3krMGt1T3JMUk15V2NkaWJjMmlRa1ptKzhmN2RUVXdiS2JCSnNLT0ZZNzMvOG5sTC9KaVlwano2blVJaytJcVNqYUZ5SzZYRmxDY3VrV3BXSHBESm5TdmJFemV5N3FIeUNxSitJK29uOGlQOEdPdGVtcFFhUzAvNGtaR1N3bHJFNnhPajZFd0dwZmpiRFpLSGtQLzJSQWROWVBRcTN6N2JkVkpNSk5uRk16NmUzN216TXBDQmlod1BkU0ZUUkUzU0NZNjZ3dlRnSnpHQ1V5aEhiU2k1aTFhenpPWGQ4dUhDU3BULzZMMW82VlUwcGpvUjhLYWE0d0NXSEdmN1AvV0VkK2F2eXAwMkpudTFjbXBXMCtHcSt2SzloMHRkYjJJQ2xNL2VOZmRvdVFQK2VwOE1KVWpyYURSSUNtQUpRWTVvYlM3MXZuaU9vK3JlYUZ5MHpSZ0IiLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsic2lnbmF0dXJlIjoiQ0FRUy9SRUtFUWdTR0FJNEFVSUpibUZ5Y21GMGFXOXVFZ3dGaDNBVkpaUDNzOHkxSHEwYURHNzZ1aEhPTmFTY1hBR1NoaUl3R2dqTFhIVXgxeVpya0xQbGpqelRPUXljcExKaUtWd1hjeG0vRXpLSm0xczhNNVd4dVJFV0VsZFhjb2FOMC9tc0twa1JNVCtlTGI1aWg5ckFIVUxUb21VTnNVd0pldTBqckplNEdMYXp5N3NQNUt0akVPRUJLUXNYVWZvUTdmRGtaT1FVSFppOXp6UU40aEZObVlKVWYzT1ZxZjlUUU5kQzErbS8vSEFaUGZXbUF5Nkh0RHZtN2tnQkprQ0VvaUZubTd0bi9QWDFoWEdQUmdXYzZCVFhVTXIyV0E4YUt5cWdnUlViTUgyK0pZdjVpV1pkZENjMXVwMU5icjNzRjdXUGNvdDlaQmh5SFFYcjY5MnplSms2U0pneWdJVnJBU2JNbHkyYkJ4VFdKQjYyM2dWZ3pwQ21WU0xaRnBrS1hnUGNwOGhHeURLT3ZGNlNpUVNIcUdSeFFvZnI3VnMxYVBoZlppeFJPSkdEK01scmtnZ1ZBbkdNeVlJRFVacGF2dENlaVUxSm1CU3owMDg0YlhQdFQvdG5wc0tPblNQWVpRbzNCMEJYS2JxQS9Xc0tuZzgvVjNaOFV1YWpvNklWWjJoZ0ExV0RkdXFRT1FrRXhHVTZwRnVMaGViSWp1V3hTTkkvRGZmdlIvL0xOcUNmdmRuZjBodm9QdER4ZngvU2F3SXg0L3NwcTl4YndGekh0TGdvSGcxRjJ3Rnc3Y2xQRGxnMVB5RC92MnlKRzNWOVQ4R0w2akdQRTZyaGZpbC91NEhOdmY3QlcwZFF6Q1RUWGNxWnk0bkJ5eTArcCtTN1p4Zk1sNEtweGEyZ09oaFNOamZwZnVYMFFwTDRrTUU4SjFubGlGTjFkOEtxRVB1REkxOEJRSFVoRC9VZlkyOW9BMHg1MTBGak0vQnNMWjRuNXNwcGtvSWk3Q0ZvUXEyYUFLVk5mQzFLaWxoSXBSRCsxZHlEdFpRa2xOOGdacE9FU0h3Nk1MMVR0VTB5ZkFjYm1xb3hKNmhoanp3SDZDTXNUUUVFeThNYlJCTStRS0trSVJkUkNQSEdGVzJMa3VSNTArSjBwRi90cjZwcVZ2SmVvemZpTW5yRWlsQjI3VFpQNG5LVEpHOHJkLy92Q1dlbjB5N0NBLzhkQko5Z2RwNXR4N05WQTQ1azFnZGN4RVIxZ1lvdU5YM24xdkczL0IxRjQrOTN3WFc5TXFvNlVzNTlUUW1yYjlOcWE3cFNRTlNySDA2dzFHeHlCQWxrRmZxQWxhZmsxNmFNcHE0MTZUSllwR3k0M1dDVWlDY0xXbGhpV2d6MnNnT0xBWFA5YTV3ZVdBS1JDOEo2amtjVWtEZVhuVytIRlNpUE02KzlCbzZrVmhMVWhONXJjNG1IWk9acEY3cHRQcHNRTjRYQ3FMUHNxcFJILzhvNVhweUM2Wm5lVGVGMkV4dGpod1pWalhEd01iSXpWWUlidzF5MlBQR3c4ZEM3cGM0c0lWU25JNG9RQUgwdjY1MGdoZmhtTWV2V1c0aDRQeWxidy9ERzRmZGVUMys4ejBmOE1PU1R0ZmlweDA1L1F0dEhsQXZPRFJ4YTV5REN2T3o2bjNmMUJPME4vNE1iR3lPTTI3cVc0eDhtQlZvcnlVWldmc21oZmkwL0dmTnZCOCt5bFRHYllOWDI3T2NFRjVXWnROcWJXRXk3VjVHYm1aVVN4c3BjZzdGQlozajMvM0w3bXV2WlhvWmkxZjVlcnE0eTNsU0grb0Y3cVBGbnZpZXlMT0N3dWJjUXkrb3lvd2o0Y0QwaWRSZWNucCtzKzUyRDcrbXpGMWdwVytmK2NKYXNveGlnYnhrT2xvUzR5ZWlLdmdNNG5XU3BHRWoxMlJQeDVvODBFbXppWE45dDR6SnpXMWh0QTVoWEpKZUZ5bXBNWXR5Q1VDZXZSV1ZtclJZa3RRN1VuQ2E2dEwvQVZQaTFuaTVYQ1hIZkxPdXFmSzVLTFl4andWdGRoaGZtQ0x2cGFLVk5DRU9DZjRuTkdIcUQ5OHdVYjBlaldOd0RZQm5KR2tCUnc5UlZzcGZ3TmRvV29WY24ycXY4eXdHVXA3eWMvc2syTHBJNlZ6aVJ5dldxYkdORzFaSnFWVnV0UmM0NitzT0U0VlNlUzIvQ1ROS05hNk5NQ2YvaHh4YVVWc1lmS2RQK1MxTGM0aWFuS3Awc1V1Z1BwbXRveWRDMENQMlJ3am5ITGNuenhDbnZxeGxMUVdYTU5QVHN1MnlCY0hsV0pMdGNtZGcvTWNMYWd5N2w1a0c2c043WnhKTEQva0F4OEU2dXFvaFArSy9qTVNyWXVRSm5ocjR0ZjRiaVdGWTNKemYvS3BBWXlWU3ErcGJmRktYTWJMOW5IRW9aUVQxNVd5YWJkNkRNQXEzUVpPdGhNNXBVemROcVVYa2NySTFQd2pBU0FnNGRWVS8ybys5OG1UWG55aFA3VmxFa1RuZGRwd3lKRitZQ0NjMDFwcWE2Wlg5OXBuUitZT3BJcmI1RHRQY0txMHQ1Y0ZDRHlReE4xbFV1cVpjWXVqdlgvR0M3KzZJcFJUei9Oa0VZLzVjbnRPZlpKeWQ2U2tVeC9KUjVBSklSUENOZWliMHJjV25uRm4wWEs4czIyeDQwd0xqRXR1ZlhETzkreXJhZjRrSHhsQkkzU0IwbWVNamVwWDUwQ0poQXg3TmVra1JGbndvZWNMZnFJeXJqWHJnQVpzVE1QaVg5NHNuamRyemlzQVh0QkthbnVsaWxTeE9mVzJIU21mbmswTDJHWEhKSjNTK3ZDYmdSRW1sOWRla2ZtSGhtd0VZdHYza2ZodGg3RVJJd1ZUOGo2OU90SUJDbWVUSUp1QTdvNzNQWGMxTEoyd2h1Wks3RlplNk5UT3M2MU1QN0pWbEljNnRiaHRPQmszTlhVOHBCUE9DWTZRNTVkNjNGdHBVVS9LQ2JRMEsyV1E5bzVqTkd3Tk5KNkN3NUZEQ3QreDVZSmVQMWQyUXpEU2lxK2NTWnF3cUNaVmNOb0FmdEdHemdyUlo0dmtwYlAweFFaSEN0V3NVd0hWVGpyWTROZlJEakhkVlBSbjhKOVh0b29jL2hJV2txekZhY3pZVGxhcHUyUkNKcWU4YUVvM2p6V1pDRUo3QWlCekJZcmFZRGdPZmVpUXllSlVzYmJYSGRHS2lpdzIyQmRwNE0rWXo2NzFNWjBpZTlFV1JrczBRMkRIR1VUbmU5UzR0QUFKUEdiVWY0eHRDTW9KNDBRNFFXMDVRTFNaeTN3TkZBVUZZbVhFNEVDREZCOCtJRWVORkZGL3RnQW5maDhHdkZoLzF4WStMRStvek1taW5qU1J3aHEwOXpRQUVrTkZ5dTBJS2d2RGt5T0kraWNjRjdkb1dMVFVhc1B4TEgxeEdQQlNLa2VXQ25Tb1gvNEMyYzlkdTY2QmJSWGFtSHFhZStWQ2lTNkJOOTUzaVlLT3U4ck1rbmVXK1RGalYwQ3pBZjhQTkc5TEJheVVnWTVVdzl4MFNhcDZ6STRQcGFaS0g0ZDlrdGtGVSs5MXlCYk41U1J0ZGQ4L0VRYXRKWG1CTmZ3N2tScmxsK0xqd1p3ZVdMRWIwalQxUnBHdURxSExkTFRNTWdvd1NjenJwMnpDc0pkUUgvc2VhWXJwd2tDRlI4ZFJqTTdKUlVrVmExQTBTdmRaQVg3NXZ1QnZ2TEJ6TjJjK1JidDVvcXk5M2xxSUJJaktiN3cyVWtSVGI0cEJXY05LTlBVZ3QzYXovUHhReEVhc2FsdGRtdXl2c1hZYnVyM1ZQTm44SnpOLzZKNU4rUUNuejU3MVRjY2FwQ21rUHZGRStnYXlYVStVRjFWS1l5RmNVdmxldjdsc1FsbWpxeksyUUE4WDdRM1R6RmFwcWh2SktsdDdkekJIcmFxWkJnUGdiOU40b0tOYXlpOEZaVDcxK2thWWhJaEZyRVNLWVJIZkhrajVuM2tmaGdMeSt4K2pMRGJLeE1nZ2tnSG5KejlSdUVIWlhCV3RrWUFRPT0iLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsiaWQiOiJ0b29sdV8wMUg2ZlY4SnZSSk52WHk0N2VSbmQzakgiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6e30sIm5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHdBTVJkUzIxRmVKZnpRaE0zcyIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo3Mzk2LCJvdXRwdXRfdG9rZW5zIjo3NzAsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjczOX0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoic2tlcHRpYyIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy00YWU5LTc2YzAtODlhNi00ZjBiMTE0ZTMwMzAtMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzM5Niwib3V0cHV0X3Rva2VucyI6NzcwLCJ0b3RhbF90b2tlbnMiOjgxNjYsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjczOX19fV0sInN0cnVjdHVyZWRfcmVzcG9uc2UiOm51bGx9fQ==" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "381fc39d7d99fceb", + "parentSpanId": "87fbd88889f37ec8", + "name": "UnsupportedContentMiddleware.wrap_model_call", + "kind": 1, + "startTimeUnixNano": "1790968023781553152", + "endTimeUnixNano": "1790968030834259968", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "e30=" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "UnsupportedContentMiddleware.wrap_model_call" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "1" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "skeptic:3dfc4328-0d19-e1ea-bcda-3d3d36d7cda8|model:41713ca4-d95b-3dfd-185e-53f884946d93" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "skeptic" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "skeptic:3dfc4328-0d19-e1ea-bcda-3d3d36d7cda8" + } + }, + { + "key": "langsmith.metadata.ls_method", + "value": { + "stringValue": "traceable" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJvdXRwdXQiOnsicmVzdWx0IjpbeyJjb250ZW50IjpbeyJzaWduYXR1cmUiOiJDQVFTMVFnS0VBZ1NHQUk0QVVJSWRHaHBibXRwYm1jU0RONlRmVTZORE05NzFsRWJ6eG9NdlJVQTFKOW94M2ZKUVlUOUlqQXJDUVFHSEJQNHBSSFBhdlh6UmxiTWJhWUhDNTdZaitHYnhncjBoZWx2ZDJPSUwvbzZSYnQ0UEZBM0ZhSUZVbGdxOGdjN0tRSTlGdEdudlNBa0xCUnpJc1VwLzJkNVJkMnpnRXNma0ZWZVhzbjhXWVlaa2luQVFWVnk2QTdCM0xhRUpXMHhiL3g4OERpNjdmZWR3eDBRK0ZsWnR3Y21UM0RjYWExMUp2ZXlIUnIzZG0rekdTWlA2Q1hlWE5KOG5yVFo3VzFZUlR5c0Q2dzdvN0xwOGgxUndxNGY0bzhqK1FVZlkwdmplK3pKWnN4SFBtUU50M3lrZUtoOTloZ242ZGJpejYvRUhxbWJIalNBdTFNNFNxRURmcUlLVGhIWWVuYW1BWm1jUTRYQU9vNm1kUTYrNzdWZ3BWMWxDMS9rNTJJaVVvSmprR2xRcm5MQ3Z1dGlJN0tOK3NnSzhjMisyS0NKclBSQ3ZWVitCa1N0dlA1aldXcXp5dHBLUStuSHpoTDQ0OE9ySUE5Z2RKQzM5QzhlRHJYZVJuSTd0NEZxbGtYR29NbFpBVjlVTWNyUUhSeE9NaTh0T3lUemk5b3lCRkRUU3NRK0xKcWVyMHR6MUZ0OWdlazZ2emc1MFNwVkFGUWRMRVJ2OFIxWXFNYWVJK3hzdlRBRVc5TVJndlJNa3pMdXpSVDJndXczakQrRmppcDM3eGdLNVFJMFIrQ3NPQW1RUG9UWml6emNJeUFVQXVDSkZDZ3EvZ0QvQ2liTHhveW1pVlVJMWFuOVNudDNLcE5Sb1dkdldJbmUzM1YwcHY4UEhnOXlXMkp1TE9xUUdpbExDc0svUTNvUXIzdXBFTVh4WEdqQ0w3V1VpenZSWkdGRUVWK1ZwbFl0N3Y0dEtaTWo1cGszdmRlTTBNSjVYWmc3UHZVc1J6eWN4Y25yVDNLbm9jM3FNK3R0cVpmVHNiOGQ2VS9wUWlmcTdHdWFoRk9IcnZyb3FnSkxqKy84MVl5MXBaRmtqZkpnWTBYa2x6QW5BNkNGcS9ZVDBneGJWK1RINUlTK1dhWU9VekszNlM4czVxcU13aG5ndjRJekUzY3RYWHFuaElaWE56bnlWQnFiWWEvR3gyYkNqY0VqMm41UnlHWVJTLytNY3VsZ0xCem5ROU1rRzJEN1FQM01xL253SFhLTUlhTFM4Vks4aEczZDByQWpsdm5hdVhTaE5ySnh0a2o0cWt3bnhENExyRWprMXVlK1BMY2JMcVlXd0FyOG4wcys0MWpyc0lQVnl5Szl2RmFFWnJBOUthQnhrSnlpcXg1MEdaTm43Y2t4K21rSTE4Z0g1Mml5VlB0VThGS01YanVnQ2w2bVBMaUhZaURRZCtHNjNMdWRtT0t5czlpdFZuQkRCTk1QMmIvSTcrYklWUmxoZUdhWldyNk1BVEtsODZCM3krMGt1T3JMUk15V2NkaWJjMmlRa1ptKzhmN2RUVXdiS2JCSnNLT0ZZNzMvOG5sTC9KaVlwano2blVJaytJcVNqYUZ5SzZYRmxDY3VrV3BXSHBESm5TdmJFemV5N3FIeUNxSitJK29uOGlQOEdPdGVtcFFhUzAvNGtaR1N3bHJFNnhPajZFd0dwZmpiRFpLSGtQLzJSQWROWVBRcTN6N2JkVkpNSk5uRk16NmUzN216TXBDQmlod1BkU0ZUUkUzU0NZNjZ3dlRnSnpHQ1V5aEhiU2k1aTFhenpPWGQ4dUhDU3BULzZMMW82VlUwcGpvUjhLYWE0d0NXSEdmN1AvV0VkK2F2eXAwMkpudTFjbXBXMCtHcSt2SzloMHRkYjJJQ2xNL2VOZmRvdVFQK2VwOE1KVWpyYURSSUNtQUpRWTVvYlM3MXZuaU9vK3JlYUZ5MHpSZ0IiLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsic2lnbmF0dXJlIjoiQ0FRUy9SRUtFUWdTR0FJNEFVSUpibUZ5Y21GMGFXOXVFZ3dGaDNBVkpaUDNzOHkxSHEwYURHNzZ1aEhPTmFTY1hBR1NoaUl3R2dqTFhIVXgxeVpya0xQbGpqelRPUXljcExKaUtWd1hjeG0vRXpLSm0xczhNNVd4dVJFV0VsZFhjb2FOMC9tc0twa1JNVCtlTGI1aWg5ckFIVUxUb21VTnNVd0pldTBqckplNEdMYXp5N3NQNUt0akVPRUJLUXNYVWZvUTdmRGtaT1FVSFppOXp6UU40aEZObVlKVWYzT1ZxZjlUUU5kQzErbS8vSEFaUGZXbUF5Nkh0RHZtN2tnQkprQ0VvaUZubTd0bi9QWDFoWEdQUmdXYzZCVFhVTXIyV0E4YUt5cWdnUlViTUgyK0pZdjVpV1pkZENjMXVwMU5icjNzRjdXUGNvdDlaQmh5SFFYcjY5MnplSms2U0pneWdJVnJBU2JNbHkyYkJ4VFdKQjYyM2dWZ3pwQ21WU0xaRnBrS1hnUGNwOGhHeURLT3ZGNlNpUVNIcUdSeFFvZnI3VnMxYVBoZlppeFJPSkdEK01scmtnZ1ZBbkdNeVlJRFVacGF2dENlaVUxSm1CU3owMDg0YlhQdFQvdG5wc0tPblNQWVpRbzNCMEJYS2JxQS9Xc0tuZzgvVjNaOFV1YWpvNklWWjJoZ0ExV0RkdXFRT1FrRXhHVTZwRnVMaGViSWp1V3hTTkkvRGZmdlIvL0xOcUNmdmRuZjBodm9QdER4ZngvU2F3SXg0L3NwcTl4YndGekh0TGdvSGcxRjJ3Rnc3Y2xQRGxnMVB5RC92MnlKRzNWOVQ4R0w2akdQRTZyaGZpbC91NEhOdmY3QlcwZFF6Q1RUWGNxWnk0bkJ5eTArcCtTN1p4Zk1sNEtweGEyZ09oaFNOamZwZnVYMFFwTDRrTUU4SjFubGlGTjFkOEtxRVB1REkxOEJRSFVoRC9VZlkyOW9BMHg1MTBGak0vQnNMWjRuNXNwcGtvSWk3Q0ZvUXEyYUFLVk5mQzFLaWxoSXBSRCsxZHlEdFpRa2xOOGdacE9FU0h3Nk1MMVR0VTB5ZkFjYm1xb3hKNmhoanp3SDZDTXNUUUVFeThNYlJCTStRS0trSVJkUkNQSEdGVzJMa3VSNTArSjBwRi90cjZwcVZ2SmVvemZpTW5yRWlsQjI3VFpQNG5LVEpHOHJkLy92Q1dlbjB5N0NBLzhkQko5Z2RwNXR4N05WQTQ1azFnZGN4RVIxZ1lvdU5YM24xdkczL0IxRjQrOTN3WFc5TXFvNlVzNTlUUW1yYjlOcWE3cFNRTlNySDA2dzFHeHlCQWxrRmZxQWxhZmsxNmFNcHE0MTZUSllwR3k0M1dDVWlDY0xXbGhpV2d6MnNnT0xBWFA5YTV3ZVdBS1JDOEo2amtjVWtEZVhuVytIRlNpUE02KzlCbzZrVmhMVWhONXJjNG1IWk9acEY3cHRQcHNRTjRYQ3FMUHNxcFJILzhvNVhweUM2Wm5lVGVGMkV4dGpod1pWalhEd01iSXpWWUlidzF5MlBQR3c4ZEM3cGM0c0lWU25JNG9RQUgwdjY1MGdoZmhtTWV2V1c0aDRQeWxidy9ERzRmZGVUMys4ejBmOE1PU1R0ZmlweDA1L1F0dEhsQXZPRFJ4YTV5REN2T3o2bjNmMUJPME4vNE1iR3lPTTI3cVc0eDhtQlZvcnlVWldmc21oZmkwL0dmTnZCOCt5bFRHYllOWDI3T2NFRjVXWnROcWJXRXk3VjVHYm1aVVN4c3BjZzdGQlozajMvM0w3bXV2WlhvWmkxZjVlcnE0eTNsU0grb0Y3cVBGbnZpZXlMT0N3dWJjUXkrb3lvd2o0Y0QwaWRSZWNucCtzKzUyRDcrbXpGMWdwVytmK2NKYXNveGlnYnhrT2xvUzR5ZWlLdmdNNG5XU3BHRWoxMlJQeDVvODBFbXppWE45dDR6SnpXMWh0QTVoWEpKZUZ5bXBNWXR5Q1VDZXZSV1ZtclJZa3RRN1VuQ2E2dEwvQVZQaTFuaTVYQ1hIZkxPdXFmSzVLTFl4andWdGRoaGZtQ0x2cGFLVk5DRU9DZjRuTkdIcUQ5OHdVYjBlaldOd0RZQm5KR2tCUnc5UlZzcGZ3TmRvV29WY24ycXY4eXdHVXA3eWMvc2syTHBJNlZ6aVJ5dldxYkdORzFaSnFWVnV0UmM0NitzT0U0VlNlUzIvQ1ROS05hNk5NQ2YvaHh4YVVWc1lmS2RQK1MxTGM0aWFuS3Awc1V1Z1BwbXRveWRDMENQMlJ3am5ITGNuenhDbnZxeGxMUVdYTU5QVHN1MnlCY0hsV0pMdGNtZGcvTWNMYWd5N2w1a0c2c043WnhKTEQva0F4OEU2dXFvaFArSy9qTVNyWXVRSm5ocjR0ZjRiaVdGWTNKemYvS3BBWXlWU3ErcGJmRktYTWJMOW5IRW9aUVQxNVd5YWJkNkRNQXEzUVpPdGhNNXBVemROcVVYa2NySTFQd2pBU0FnNGRWVS8ybys5OG1UWG55aFA3VmxFa1RuZGRwd3lKRitZQ0NjMDFwcWE2Wlg5OXBuUitZT3BJcmI1RHRQY0txMHQ1Y0ZDRHlReE4xbFV1cVpjWXVqdlgvR0M3KzZJcFJUei9Oa0VZLzVjbnRPZlpKeWQ2U2tVeC9KUjVBSklSUENOZWliMHJjV25uRm4wWEs4czIyeDQwd0xqRXR1ZlhETzkreXJhZjRrSHhsQkkzU0IwbWVNamVwWDUwQ0poQXg3TmVra1JGbndvZWNMZnFJeXJqWHJnQVpzVE1QaVg5NHNuamRyemlzQVh0QkthbnVsaWxTeE9mVzJIU21mbmswTDJHWEhKSjNTK3ZDYmdSRW1sOWRla2ZtSGhtd0VZdHYza2ZodGg3RVJJd1ZUOGo2OU90SUJDbWVUSUp1QTdvNzNQWGMxTEoyd2h1Wks3RlplNk5UT3M2MU1QN0pWbEljNnRiaHRPQmszTlhVOHBCUE9DWTZRNTVkNjNGdHBVVS9LQ2JRMEsyV1E5bzVqTkd3Tk5KNkN3NUZEQ3QreDVZSmVQMWQyUXpEU2lxK2NTWnF3cUNaVmNOb0FmdEdHemdyUlo0dmtwYlAweFFaSEN0V3NVd0hWVGpyWTROZlJEakhkVlBSbjhKOVh0b29jL2hJV2txekZhY3pZVGxhcHUyUkNKcWU4YUVvM2p6V1pDRUo3QWlCekJZcmFZRGdPZmVpUXllSlVzYmJYSGRHS2lpdzIyQmRwNE0rWXo2NzFNWjBpZTlFV1JrczBRMkRIR1VUbmU5UzR0QUFKUEdiVWY0eHRDTW9KNDBRNFFXMDVRTFNaeTN3TkZBVUZZbVhFNEVDREZCOCtJRWVORkZGL3RnQW5maDhHdkZoLzF4WStMRStvek1taW5qU1J3aHEwOXpRQUVrTkZ5dTBJS2d2RGt5T0kraWNjRjdkb1dMVFVhc1B4TEgxeEdQQlNLa2VXQ25Tb1gvNEMyYzlkdTY2QmJSWGFtSHFhZStWQ2lTNkJOOTUzaVlLT3U4ck1rbmVXK1RGalYwQ3pBZjhQTkc5TEJheVVnWTVVdzl4MFNhcDZ6STRQcGFaS0g0ZDlrdGtGVSs5MXlCYk41U1J0ZGQ4L0VRYXRKWG1CTmZ3N2tScmxsK0xqd1p3ZVdMRWIwalQxUnBHdURxSExkTFRNTWdvd1NjenJwMnpDc0pkUUgvc2VhWXJwd2tDRlI4ZFJqTTdKUlVrVmExQTBTdmRaQVg3NXZ1QnZ2TEJ6TjJjK1JidDVvcXk5M2xxSUJJaktiN3cyVWtSVGI0cEJXY05LTlBVZ3QzYXovUHhReEVhc2FsdGRtdXl2c1hZYnVyM1ZQTm44SnpOLzZKNU4rUUNuejU3MVRjY2FwQ21rUHZGRStnYXlYVStVRjFWS1l5RmNVdmxldjdsc1FsbWpxeksyUUE4WDdRM1R6RmFwcWh2SktsdDdkekJIcmFxWkJnUGdiOU40b0tOYXlpOEZaVDcxK2thWWhJaEZyRVNLWVJIZkhrajVuM2tmaGdMeSt4K2pMRGJLeE1nZ2tnSG5KejlSdUVIWlhCV3RrWUFRPT0iLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsiaWQiOiJ0b29sdV8wMUg2ZlY4SnZSSk52WHk0N2VSbmQzakgiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6e30sIm5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHdBTVJkUzIxRmVKZnpRaE0zcyIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo3Mzk2LCJvdXRwdXRfdG9rZW5zIjo3NzAsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjczOX0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoic2tlcHRpYyIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy00YWU5LTc2YzAtODlhNi00ZjBiMTE0ZTMwMzAtMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzM5Niwib3V0cHV0X3Rva2VucyI6NzcwLCJ0b3RhbF90b2tlbnMiOjgxNjYsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjczOX19fV0sInN0cnVjdHVyZWRfcmVzcG9uc2UiOm51bGx9fQ==" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "f2bbd85b9e41b171", + "parentSpanId": "381fc39d7d99fceb", + "name": "ChatAnthropic", + "kind": 1, + "startTimeUnixNano": "1790968023785360128", + "endTimeUnixNano": "1790968030833796864", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chat" + } + }, + { + "key": "gen_ai.serialized.name", + "value": { + "stringValue": "ChatAnthropic" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "llm" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "ChatAnthropic" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "anthropic" + } + }, + { + "key": "gen_ai.request.model", + "value": { + "stringValue": "claude-sonnet" + } + }, + { + "key": "gen_ai.tool.definitions", + "value": { + "stringValue": "[{\"name\":\"ls\",\"input_schema\":{\"properties\":{\"path\":{\"description\":\"Absolute path to the directory to list. Must be absolute, not relative.\",\"type\":\"string\"}},\"required\":[\"path\"],\"type\":\"object\"},\"description\":\"Lists all files in a directory.\\n\\nThis is useful for exploring the filesystem and finding the right file to read or edit.\\nYou should almost ALWAYS use this tool before using the read_file or edit_file tools.\"},{\"name\":\"read_file\",\"input_schema\":{\"properties\":{\"file_path\":{\"description\":\"Absolute path to the file to read. Must be absolute, not relative.\",\"type\":\"string\"},\"offset\":{\"default\":0,\"description\":\"Line number to start reading from (0-indexed). Use for pagination of large files.\",\"type\":\"integer\"},\"limit\":{\"default\":100,\"description\":\"Maximum number of lines to read. Use for pagination of large files.\",\"type\":\"integer\"}},\"required\":[\"file_path\"],\"type\":\"object\"},\"description\":\"Reads a file from the filesystem. Assume any path the user provides is valid; reading a missing file returns an error.\\n\\nUsage:\\n- By default, it reads up to 100 lines starting from the beginning of the file. Use `offset`/`limit` to page through large files instead of reading them whole.\\n- A status header, `@@ field | field | ... @@`, sits above the file content, and every line after it is verbatim file content. When content is truncated, there may be an explanation before the header. Never include the header when editing.\\n- Speculatively batch multiple `read_file` calls in one response when several files may be useful.\\n- An empty file returns a system-reminder warning in place of contents.\\n- Large tool results may be offloaded to a file; the tool message gives the path. Read that path here, paging with `offset`/`limit`.\\n- Images (`.png`, `.jpg`, etc.), audio, video, and PDFs return multimodal content blocks (https://docs.langchain.com/oss/python/langchain/messages#multimodal).\\n- For images and PDFs, pagination via `offset`/`limit` is text-only - supply `file_path` only\\n- Always read a file before editing it.\"},{\"name\":\"glob\",\"input_schema\":{\"properties\":{\"pattern\":{\"description\":\"Glob pattern to match files (e.g., '*.py', '**/*.py', '/subdir/**/*.md'). A pattern without '/' matches the file name at any depth; a pattern containing '/' matches the search-root-relative path; a leading '/' anchors to the search root ('/*.py' matches only top-level files). Leading-dot names are excluded unless the pattern segment starts with '.', so prefer the bare form '*.py' over '**/*.py' -- '**' will not descend into dot-directories like '.github'.\",\"type\":\"string\"},\"path\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"null\"}],\"default\":null,\"description\":\"Base directory to search from. Defaults to the backend's default root.\"}},\"required\":[\"pattern\"],\"type\":\"object\"},\"description\":\"Find files matching a glob pattern, returning absolute paths.\\n\\nSupports `*` (any characters within a path segment), `**` (any directories), `?` (single character), `[abc]` (one character from a set), and `{a,b}` (alternatives), e.g. `*.py`, `src/**/*.py`, `*.{yml,yaml}`.\\n\\nA pattern without `/` matches the file name at any depth under the search root (`*.py` matches `src/app/main.py`). A pattern containing `/` matches the search-root-relative path (`src/**/*.py`). A leading `/` anchors to the search root (`/*.py` matches only top-level Python files).\\n\\nLeading-dot names are only matched when the pattern segment itself starts with `.` (use `.env`, or `.github/**/*.yml`). Because `**` will not descend into dot-directories, the bare form `*.yml` is *broader* than `**/*.yml` and is usually what you want.\"},{\"name\":\"grep\",\"input_schema\":{\"properties\":{\"pattern\":{\"description\":\"Text pattern to search for (literal string, not regex).\",\"type\":\"string\"},\"path\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"null\"}],\"default\":null,\"description\":\"Directory to search in. Defaults to current working directory.\"},\"glob\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"null\"}],\"default\":null,\"description\":\"Glob pattern (NOT regex) limiting which files are searched (e.g. '*.py', '*.ts'). A pattern without '/' matches the file name at any depth; a pattern containing '/' matches the search-root-relative path (e.g. 'src/**/*.py'). This is an in-tool file filter, not a call to the separate glob tool. Brace expansion (e.g. '*.{ts,tsx}') is not supported on all backends; run a separate search per extension for reliable results.\"},\"output_mode\":{\"default\":\"files_with_matches\",\"description\":\"Shape of the returned text. 'files_with_matches' (default): newline-separated matching file paths. 'content': matching lines grouped by file under a ':' header, each line indented and formatted ': ' (only the matched line, no surrounding context). 'count': one ': ' line per file.\",\"enum\":[\"files_with_matches\",\"content\",\"count\"],\"type\":\"string\"},\"max_count\":{\"anyOf\":[{\"exclusiveMinimum\":0,\"type\":\"integer\"},{\"type\":\"null\"}],\"default\":null,\"description\":\"Optional cap on the total number of matches returned across all files. Leave unset to use the configured default. When the cap is hit, results are truncated and a note says so; narrow the pattern or path to see the rest.\"}},\"required\":[\"pattern\"],\"type\":\"object\"},\"description\":\"Search for a LITERAL text pattern across files (NOT regex).\\n\\nThe pattern is matched verbatim: regex metacharacters are ordinary characters, not operators. To match any of several strings, run a separate grep for each; `grep(pattern=\\\"foo|bar\\\")` searches for the literal text \\\"foo|bar\\\", and `.*` or `\\\\.` match those characters literally.\\n\\nReturns matching files or content per `output_mode`. Offloaded large tool results live under the artifacts root (`/large_tool_results/` by default); grep that directory to search them when you do not know the exact path.\"},{\"name\":\"transfer_to_researcher\",\"input_schema\":{\"properties\":{},\"type\":\"object\"},\"description\":\"Ask the researcher to address a concrete gap\"},{\"name\":\"transfer_to_verifier\",\"input_schema\":{\"properties\":{},\"type\":\"object\"},\"description\":\"Send critique for independent verification\"},{\"name\":\"transfer_to_red_team\",\"input_schema\":{\"properties\":{},\"type\":\"object\"},\"description\":\"Send checked claims for adversarial review\"}]" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_chat_model" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "1" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "skeptic:3dfc4328-0d19-e1ea-bcda-3d3d36d7cda8|model:41713ca4-d95b-3dfd-185e-53f884946d93" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "skeptic" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "skeptic:3dfc4328-0d19-e1ea-bcda-3d3d36d7cda8" + } + }, + { + "key": "langsmith.metadata.ls_provider", + "value": { + "stringValue": "anthropic" + } + }, + { + "key": "langsmith.metadata.ls_model_name", + "value": { + "stringValue": "claude-sonnet" + } + }, + { + "key": "langsmith.metadata.ls_model_type", + "value": { + "stringValue": "chat" + } + }, + { + "key": "langsmith.metadata.ls_max_tokens", + "value": { + "intValue": "4096" + } + }, + { + "key": "langsmith.metadata.lc_versions", + "value": { + "stringValue": "{\"langchain-core\":\"1.6.6\",\"langchain\":\"1.4.3\",\"langchain-anthropic\":\"1.7.5\"}" + } + }, + { + "key": "langsmith.metadata.ls_method", + "value": { + "stringValue": "traceable" + } + }, + { + "key": "langsmith.metadata.model", + "value": { + "stringValue": "claude-sonnet" + } + }, + { + "key": "langsmith.metadata.max_tokens", + "value": { + "intValue": "4096" + } + }, + { + "key": "langsmith.metadata.model_kwargs", + "value": { + "stringValue": "{\"extra_body\":{\"extra_headers\":{\"anthropic-workspace-id\":\"[redacted workspace]\"}}}" + } + }, + { + "key": "langsmith.metadata.streaming", + "value": { + "boolValue": false + } + }, + { + "key": "langsmith.metadata.max_retries", + "value": { + "intValue": "2" + } + }, + { + "key": "langsmith.metadata._type", + "value": { + "stringValue": "anthropic-chat" + } + }, + { + "key": "langsmith.metadata.usage_metadata", + "value": { + "stringValue": "{\"input_tokens\":7396,\"output_tokens\":770,\"total_tokens\":8166,\"input_token_details\":{\"cache_read\":0,\"cache_creation\":0,\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_token_details\":{\"reasoning\":739}}" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "seq:step:1" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W1t7ImxjIjoxLCJ0eXBlIjoiY29uc3RydWN0b3IiLCJpZCI6WyJsYW5nY2hhaW4iLCJzY2hlbWEiLCJtZXNzYWdlcyIsIlN5c3RlbU1lc3NhZ2UiXSwia3dhcmdzIjp7ImNvbnRlbnQiOiJZb3UgYXJlIHRoZSBza2VwdGljLiBDaGFsbGVuZ2UgdGhlIHJlc2VhcmNoLCBuYW1lIHVuc3VwcG9ydGVkIGNsYWltcywgYW5kIHNlbmQgeW91ciBjcml0aXF1ZSB0byB0aGUgdmVyaWZpZXIuIElmIHRoZSBlZGl0b3Igc2VuZHMgYSByZXZpc2lvbiBiYWNrLCBhc2sgdGhlIHJlc2VhcmNoZXIgdG8gZml4IGNvbmNyZXRlIGdhcHMgb3Igc2VuZCB0aGUgcmVzdWx0IHRvIHJlZF90ZWFtLiBEbyBub3QgZ2l2ZSB0aGUgZmluYWwgYW5zd2VyLiIsInR5cGUiOiJzeXN0ZW0ifX0seyJsYyI6MSwidHlwZSI6ImNvbnN0cnVjdG9yIiwiaWQiOlsibGFuZ2NoYWluIiwic2NoZW1hIiwibWVzc2FnZXMiLCJIdW1hbk1lc3NhZ2UiXSwia3dhcmdzIjp7ImNvbnRlbnQiOiJGb3IgYSBzeW50aGV0aWMgdHJhY2luZyBmaXh0dXJlLCBleHBsYWluIFB5dGhvbiB0dXBsZSB2ZXJzdXMgbGlzdCBpbW11dGFiaWxpdHkgaW4gYXQgbW9zdCAxMjAgd29yZHMuIFVzZSBvbmx5IG9mZmljaWFsIFB5dGhvbiBkb2N1bWVudGF0aW9uIGlmIHlvdSBzZWFyY2guIEhhdmUgdGhlIHNrZXB0aWMgYW5kIHZlcmlmaWVyIGNoZWNrIGl0LCB0aGVuIGhhdmUgdGhlIGVkaXRvciB3cml0ZSAvYW5zd2VyLm1kLiBLZWVwIHRoZSBydW4gc2hvcnQgYW5kIGRvIG5vdCBhc2sgZm9sbG93LXVwIHF1ZXN0aW9ucy4iLCJ0eXBlIjoiaHVtYW4iLCJpZCI6IjQ2MjA0MzA1LWY4ZWUtNDM4NC05Y2U4LWZjZDgwYmZjZGY3NSJ9fSx7ImxjIjoxLCJ0eXBlIjoiY29uc3RydWN0b3IiLCJpZCI6WyJsYW5nY2hhaW4iLCJzY2hlbWEiLCJtZXNzYWdlcyIsIkFJTWVzc2FnZSJdLCJrd2FyZ3MiOnsiY29udGVudCI6W3siaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9LCJuYW1lIjoid2ViX3NlYXJjaCIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3OWFXOWJyZW5QcWd5YmZOd1AiLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6Mjg0NSwib3V0cHV0X3Rva2VucyI6NzMsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjB9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InJlc2VhcmNoZXIiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtMjFhMy03MDAwLWEzODEtMjg0YTZhMGMwM2Y3LTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoid2ViX3NlYXJjaCIsImFyZ3MiOnsicXVlcnkiOiJkb2NzLnB5dGhvbi5vcmcgdHVwbGVzIGltbXV0YWJsZSBzZXF1ZW5jZXMgbGlzdHMgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgdHV0b3JpYWwifSwiaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJ0eXBlIjoidG9vbF9jYWxsIn1dLCJ1c2FnZV9tZXRhZGF0YSI6eyJpbnB1dF90b2tlbnMiOjI4NDUsIm91dHB1dF90b2tlbnMiOjczLCJ0b3RhbF90b2tlbnMiOjI5MTgsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjB9fSwiaW52YWxpZF90b29sX2NhbGxzIjpbXX19LHsibGMiOjEsInR5cGUiOiJjb25zdHJ1Y3RvciIsImlkIjpbImxhbmdjaGFpbiIsInNjaGVtYSIsIm1lc3NhZ2VzIiwiVG9vbE1lc3NhZ2UiXSwia3dhcmdzIjp7ImNvbnRlbnQiOiJbe1widGl0bGVcIjogXCI1LiBEYXRhIFN0cnVjdHVyZXMgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3R1dG9yaWFsL2RhdGFzdHJ1Y3R1cmVzLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcIllvdSBtaWdodCBoYXZlIG5vdGljZWQgdGhhdCBtZXRob2RzIGxpa2VgaW5zZXJ0YCxgcmVtb3ZlYCBvcmBzb3J0YCB0aGF0IG9ubHkgbW9kaWZ5IHRoZSBsaXN0IGhhdmUgbm8gcmV0dXJuIHZhbHVlIHByaW50ZWQgXFx1MjAxMyB0aGV5IHJldHVybiB0aGUgZGVmYXVsdGBOb25lYC4gWzFdIFRoaXMgaXMgYSBkZXNpZ24gcHJpbmNpcGxlIGZvciBhbGwgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgaW4gUHl0aG9uLlxcbi4uLlxcbiMjIDUuMy4gVHVwbGVzIGFuZCBTZXF1ZW5jZXNcXHUwMGI2XFxuXFxuV2Ugc2F3IHRoYXQgbGlzdHMgYW5kIHN0cmluZ3MgaGF2ZSBtYW55IGNvbW1vbiBwcm9wZXJ0aWVzLCBzdWNoIGFzIGluZGV4aW5nIGFuZCBzbGljaW5nIG9wZXJhdGlvbnMuIFRoZXkgYXJlIHR3byBleGFtcGxlcyBvZiBzZXF1ZW5jZSBkYXRhIHR5cGVzIChzZWUgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBsaXN0LCB0dXBsZSwgcmFuZ2UpLiBTaW5jZSBQeXRob24gaXMgYW4gZXZvbHZpbmcgbGFuZ3VhZ2UsIG90aGVyIHNlcXVlbmNlIGRhdGEgdHlwZXMgbWF5IGJlIGFkZGVkLiBUaGVyZSBpcyBhbHNvIGFub3RoZXIgc3RhbmRhcmQgc2VxdWVuY2UgZGF0YSB0eXBlOiB0aGUgdHVwbGUuXFxuXFxuQSB0dXBsZSBjb25zaXN0cyBvZiBhIG51bWJlciBvZiB2YWx1ZXMgc2VwYXJhdGVkIGJ5IGNvbW1hcywgZm9yIGluc3RhbmNlOlxcbi4uLlxcbj4+PiAjIFR1cGxlcyBhcmUgaW1tdXRhYmxlOlxcbj4+PiB0WzBdID0gODg4ODhcXG5UcmFjZWJhY2sgKG1vc3QgcmVjZW50IGNhbGwgbGFzdCk6XFxuICBGaWxlIFxcXCI8c3RkaW4+XFxcIiwgbGluZSAxLCBpbiA8bW9kdWxlPlxcblR5cGVFcnJvcjogJ3R1cGxlJyBvYmplY3QgZG9lcyBub3Qgc3VwcG9ydCBpdGVtIGFzc2lnbm1lbnRcXG4+Pj4gIyBidXQgdGhleSBjYW4gY29udGFpbiBtdXRhYmxlIG9iamVjdHM6XFxuPj4+IHYgPSAoWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuPj4+IHZcXG4oWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuLi4uXFxuQXMgeW91IHNlZSwgb24gb3V0cHV0IHR1cGxlcyBhcmUgYWx3YXlzIGVuY2xvc2VkIGluIHBhcmVudGhlc2VzLCBzbyB0aGF0IG5lc3RlZCB0dXBsZXMgYXJlIGludGVycHJldGVkIGNvcnJlY3RseTsgdGhleSBtYXkgYmUgaW5wdXQgd2l0aCBvciB3aXRob3V0IHN1cnJvdW5kaW5nIHBhcmVudGhlc2VzLCBhbHRob3VnaCBvZnRlbiBwYXJlbnRoZXNlcyBhcmUgbmVjZXNzYXJ5IGFueXdheSAoaWYgdGhlIHR1cGxlIGlzIHBhcnQgb2YgYSBsYXJnZXIgZXhwcmVzc2lvbikuIEl0IGlzIG5vdCBwb3NzaWJsZSB0byBhc3NpZ24gdG8gdGhlIGluZGl2aWR1YWwgaXRlbXMgb2YgYSB0dXBsZSwgaG93ZXZlciBpdCBpcyBwb3NzaWJsZSB0byBjcmVhdGUgdHVwbGVzIHdoaWNoIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzLCBzdWNoIGFzIGxpc3RzLlxcblxcblRob3VnaCB0dXBsZXMgbWF5IHNlZW0gc2ltaWxhciB0byBsaXN0cywgdGhleSBhcmUgb2Z0ZW4gdXNlZCBpbiBkaWZmZXJlbnQgc2l0dWF0aW9ucyBhbmQgZm9yIGRpZmZlcmVudCBwdXJwb3Nlcy4gVHVwbGVzIGFyZSBpbW11dGFibGUsIGFuZCB1c3VhbGx5IGNvbnRhaW4gYSBoZXRlcm9nZW5lb3VzIHNlcXVlbmNlIG9mIGVsZW1lbnRzIHRoYXQgYXJlIGFjY2Vzc2VkIHZpYSB1bnBhY2tpbmcgKHNlZSBsYXRlciBpbiB0aGlzIHNlY3Rpb24pIG9yIGluZGV4aW5nIChvciBldmVuIGJ5IGF0dHJpYnV0ZSBpbiB0aGUgY2FzZSBvZiBuYW1lZHR1cGxlcykuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG5Bbm90aGVyIHVzZWZ1bCBkYXRhIHR5cGUgYnVpbHQgaW50byBQeXRob24gaXMgdGhlIGRpY3Rpb25hcnkgKHNlZSBNYXBwaW5nIFR5cGVzIFxcdTIwMTQgZGljdCkuIERpY3Rpb25hcmllcyBhcmUgc29tZXRpbWVzIGZvdW5kIGluIG90aGVyIGxhbmd1YWdlcyBhcyBcXHUyMDFjYXNzb2NpYXRpdmUgbWVtb3JpZXNcXHUyMDFkIG9yIFxcdTIwMWNhc3NvY2lhdGl2ZSBhcnJheXNcXHUyMDFkLiBVbmxpa2Ugc2VxdWVuY2VzLCB3aGljaCBhcmUgaW5kZXhlZCBieSBhIHJhbmdlIG9mIG51bWJlcnMsIGRpY3Rpb25hcmllcyBhcmUgaW5kZXhlZCBieSBrZXlzLCB3aGljaCBjYW4gYmUgYW55IGltbXV0YWJsZSB0eXBlOyBzdHJpbmdzIGFuZCBudW1iZXJzIGNhbiBhbHdheXMgYmUga2V5cy4gVHVwbGVzIGNhbiBiZSB1c2VkIGFzIGtleXMgaWYgdGhleSBjb250YWluIG9ubHkgc3RyaW5ncywgbnVtYmVycywgb3IgdHVwbGVzOyBpZiBhIHR1cGxlIGNvbnRhaW5zIGFueSBtdXRhYmxlIG9iamVjdCBlaXRoZXIgZGlyZWN0bHkgb3IgaW5kaXJlY3RseSwgaXQgY2Fubm90IGJlIHVzZWQgYXMgYSBrZXkuIFlvdSBjYW5cXHUyMDE5dCB1c2UgbGlzdHMgYXMga2V5cywgc2luY2UgbGlzdHMgY2FuIGJlIG1vZGlmaWVkIGluIHBsYWNlIHVzaW5nIGluZGV4IGFzc2lnbm1lbnRzLCBzbGljZSBhc3NpZ25tZW50cywgb3IgbWV0aG9kcyBsaWtlIGFwcGVuZCgpIGFuZCBleHRlbmQoKS5cIl19LCB7XCJ0aXRsZVwiOiBcIjMuIERhdGEgbW9kZWwgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3JlZmVyZW5jZS9kYXRhbW9kZWwuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiVGhlIHZhbHVlIG9mIHNvbWUgb2JqZWN0cyBjYW4gY2hhbmdlLiBPYmplY3RzIHdob3NlIHZhbHVlIGNhbiBjaGFuZ2UgYXJlIHNhaWQgdG8gYmUgbXV0YWJsZTsgb2JqZWN0cyB3aG9zZSB2YWx1ZSBpcyB1bmNoYW5nZWFibGUgb25jZSB0aGV5IGFyZSBjcmVhdGVkIGFyZSBjYWxsZWQgaW1tdXRhYmxlLiAoVGhlIHZhbHVlIG9mIGFuIGltbXV0YWJsZSBjb250YWluZXIgb2JqZWN0IHRoYXQgY29udGFpbnMgYSByZWZlcmVuY2UgdG8gYSBtdXRhYmxlIG9iamVjdCBjYW4gY2hhbmdlIHdoZW4gdGhlIGxhdHRlclxcdTIwMTlzIHZhbHVlIGlzIGNoYW5nZWQ7IGhvd2V2ZXIgdGhlIGNvbnRhaW5lciBpcyBzdGlsbCBjb25zaWRlcmVkIGltbXV0YWJsZSwgYmVjYXVzZSB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGl0IGNvbnRhaW5zIGNhbm5vdCBiZSBjaGFuZ2VkLiBTbywgaW1tdXRhYmlsaXR5IGlzIG5vdCBzdHJpY3RseSB0aGUgc2FtZSBhcyBoYXZpbmcgYW4gdW5jaGFuZ2VhYmxlIHZhbHVlLCBpdCBpcyBtb3JlIHN1YnRsZS4pIEFuIG9iamVjdFxcdTIwMTlzIG11dGFiaWxpdHkgaXMgZGV0ZXJtaW5lZCBieSBpdHMgdHlwZTsgZm9yIGluc3RhbmNlLCBudW1iZXJzLCBzdHJpbmdzIGFuZCB0dXBsZXMgYXJlIGltbXV0YWJsZSwgd2hpbGUgZGljdGlvbmFyaWVzIGFuZCBsaXN0cyBhcmUgbXV0YWJsZS5cXG4uLi5cXG5Tb21lIG9iamVjdHMgY29udGFpbiByZWZlcmVuY2VzIHRvIG90aGVyIG9iamVjdHM7IHRoZXNlIGFyZSBjYWxsZWQgY29udGFpbmVycy4gRXhhbXBsZXMgb2YgY29udGFpbmVycyBhcmUgdHVwbGVzLCBsaXN0cyBhbmQgZGljdGlvbmFyaWVzLiBUaGUgcmVmZXJlbmNlcyBhcmUgcGFydCBvZiBhIGNvbnRhaW5lclxcdTIwMTlzIHZhbHVlLiBJbiBtb3N0IGNhc2VzLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIHZhbHVlIG9mIGEgY29udGFpbmVyLCB3ZSBpbXBseSB0aGUgdmFsdWVzLCBub3QgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGNvbnRhaW5lZCBvYmplY3RzOyBob3dldmVyLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIG11dGFiaWxpdHkgb2YgYSBjb250YWluZXIsIG9ubHkgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGltbWVkaWF0ZWx5IGNvbnRhaW5lZCBvYmplY3RzIGFyZSBpbXBsaWVkLiBTbywgaWYgYW4gaW1tdXRhYmxlIGNvbnRhaW5lciAobGlrZSBhIHR1cGxlKSBjb250YWlucyBhIHJlZmVyZW5jZSB0byBhIG11dGFibGUgb2JqZWN0LCBpdHMgdmFsdWUgY2hhbmdlcyBpZiB0aGF0IG11dGFibGUgb2JqZWN0IGlzIGNoYW5nZWQuXFxuLi4uXFxuIyMjIDMuMi41LiBTZXF1ZW5jZXNcXHUwMGI2XFxuLi4uXFxuIyMjIyAzLjIuNS4xLiBJbW11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbkFuIG9iamVjdCBvZiBhbiBpbW11dGFibGUgc2VxdWVuY2UgdHlwZSBjYW5ub3QgY2hhbmdlIG9uY2UgaXQgaXMgY3JlYXRlZC4gKElmIHRoZSBvYmplY3QgY29udGFpbnMgcmVmZXJlbmNlcyB0byBvdGhlciBvYmplY3RzLCB0aGVzZSBvdGhlciBvYmplY3RzIG1heSBiZSBtdXRhYmxlIGFuZCBtYXkgYmUgY2hhbmdlZDsgaG93ZXZlciwgdGhlIGNvbGxlY3Rpb24gb2Ygb2JqZWN0cyBkaXJlY3RseSByZWZlcmVuY2VkIGJ5IGFuIGltbXV0YWJsZSBvYmplY3QgY2Fubm90IGNoYW5nZS4pXFxuXFxuVGhlIGZvbGxvd2luZyB0eXBlcyBhcmUgaW1tdXRhYmxlIHNlcXVlbmNlczpcXG4uLi5cXG5UdXBsZXNcXG46IFRoZSBpdGVtcyBvZiBhIGB0dXBsZWAgYXJlIGFyYml0cmFyeSBQeXRob24gb2JqZWN0cy4gVHVwbGVzIG9mIHR3byBvciBtb3JlIGl0ZW1zIGFyZSBmb3JtZWQgYnkgY29tbWEtc2VwYXJhdGVkIGxpc3RzIG9mIGV4cHJlc3Npb25zLiBBIHR1cGxlIG9mIG9uZSBpdGVtIChhIFxcdTIwMThzaW5nbGV0b25cXHUyMDE5KSBjYW4gYmUgZm9ybWVkIGJ5IGFmZml4aW5nIGEgY29tbWEgdG8gYW4gZXhwcmVzc2lvbiAoYW4gZXhwcmVzc2lvbiBieSBpdHNlbGYgZG9lcyBub3QgY3JlYXRlIGEgdHVwbGUsIHNpbmNlIHBhcmVudGhlc2VzIG11c3QgYmUgdXNhYmxlIGZvciBncm91cGluZyBvZiBleHByZXNzaW9ucykuIEFuIGVtcHR5IHR1cGxlIGNhbiBiZSBmb3JtZWQgYnkgYW4gZW1wdHkgcGFpciBvZiBwYXJlbnRoZXNlcy5cXG4uLi5cXG4jIyMjIDMuMi41LjIuIE11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbk11dGFibGUgc2VxdWVuY2VzIGNhbiBiZSBjaGFuZ2VkIGFmdGVyIHRoZXkgYXJlIGNyZWF0ZWQuIFRoZSBzdWJzY3JpcHRpb24gYW5kIHNsaWNpbmcgbm90YXRpb25zIGNhbiBiZSB1c2VkIGFzIHRoZSB0YXJnZXQgb2YgYXNzaWdubWVudCBhbmQgYGRlbGAgKGRlbGV0ZSkgc3RhdGVtZW50cy5cXG4uLi5cXG5UaGVyZSBhcmUgY3VycmVudGx5IHR3byBpbnRyaW5zaWMgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlczpcXG5cXG5MaXN0c1xcbjogVGhlIGl0ZW1zIG9mIGEgbGlzdCBhcmUgYXJiaXRyYXJ5IFB5dGhvbiBvYmplY3RzLiBMaXN0cyBhcmUgZm9ybWVkIGJ5IHBsYWNpbmcgYSBjb21tYS1zZXBhcmF0ZWQgbGlzdCBvZiBleHByZXNzaW9ucyBpbiBzcXVhcmUgYnJhY2tldHMuIChOb3RlIHRoYXQgdGhlcmUgYXJlIG5vIHNwZWNpYWwgY2FzZXMgbmVlZGVkIHRvIGZvcm0gbGlzdHMgb2YgbGVuZ3RoIDAgb3IgMS4pXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjcgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvYnVpbHRpbnMvc3RkdHlwZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiY29sbGVjdGlvbiBjbGFzc2VzIGFyZSBtdXRhYmxlLiBUaGUgbWV0aG9kcyB0aGF0IGFkZCwgc3VidHJhY3QsIG9yIHJlYXJyYW5nZSB0aGVpciAuLi4gaW4gcGxhY2UsIGFuZCBkb25cXHUyMDE5dCByZXR1cm4gYSAuLi4gLCBuZXZlciByZXR1cm4gdGhlIGNvbGxlY3Rpb24gaW5zdGFuY2UgaXRzZWxmIGJ1dCBgTm9uZWAuXFxuLi4uXFxuIyMgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBgbGlzdGAsIGB0dXBsZWAsIGByYW5nZWBcXHUwMGI2XFxuXFxuVGhlcmUgYXJlIHRocmVlIGJhc2ljIHNlcXVlbmNlIHR5cGVzOiBsaXN0cywgdHVwbGVzLCBhbmQgcmFuZ2Ugb2JqZWN0cy4gQWRkaXRpb25hbCBzZXF1ZW5jZSB0eXBlcyB0YWlsb3JlZCBmb3IgcHJvY2Vzc2luZyBvZiBiaW5hcnkgZGF0YSBhbmQgdGV4dCBzdHJpbmdzIGFyZSBkZXNjcmliZWQgaW4gZGVkaWNhdGVkIHNlY3Rpb25zLlxcbi4uLlxcblRoZSBvcGVyYXRpb25zIGluIHRoZSBmb2xsb3dpbmcgdGFibGUgLi4uIG11dGFibGUgYW5kIGltbXV0YWJsZS4gVGhlIGBjb2xsZWN0aW9ucy4gLi4uIGlzIHByb3ZpZGVkIHRvIG1ha2UgLi4uIGVhc2llciB0byBjb3JyZWN0bHkgaW1wbGVtZW50IHRoZXNlIG9wZXJhdGlvbnMgb24gY3VzdG9tIHNlcXVlbmNlIHR5cGVzXFxuLi4uXFxuIyMjIEltbXV0YWJsZSBTZXF1ZW5jZSBUeXBlc1xcdTAwYjZcXG4uLi5cXG5tdXRhYmxlIHNlcXVlbmNlIHR5cGVzIGlzXFxuLi4uXFxuc3VwcG9ydCBhbGxvd3MgaW1tdXRhYmxlIHNlcXVlbmNlcywgLi4uICwgdG8gYmUgdXNlZCBhcyBgZGljdGAga2V5cyBhbmQgc3RvcmVkIGluIC4uLiBlbnNldGAgaW5zdGFuY2VzXFxuLi4uXFxuIyMjIE11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxuIyMjIExpc3RzXFx1MDBiNlxcblxcbkxpc3RzIGFyZSBtdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaG9tb2dlbmVvdXMgaXRlbXMgKHdoZXJlIHRoZSBwcmVjaXNlIGRlZ3JlZSBvZiBzaW1pbGFyaXR5IHdpbGwgdmFyeSBieSBhcHBsaWNhdGlvbikuXFxuLi4uXFxuIyMjIFR1cGxlc1xcdTAwYjZcXG5cXG5UdXBsZXMgYXJlIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhldGVyb2dlbmVvdXMgZGF0YSAoc3VjaCBhcyB0aGUgMi10dXBsZXMgcHJvZHVjZWQgYnkgdGhlIGBlbnVtZXJhdGUoKWAgYnVpbHQtaW4pLiBUdXBsZXMgYXJlIGFsc28gdXNlZCBmb3IgY2FzZXMgd2hlcmUgYW4gaW1tdXRhYmxlIHNlcXVlbmNlIG9mIGhvbW9nZW5lb3VzIGRhdGEgaXMgbmVlZGVkIChzdWNoIGFzIGFsbG93aW5nIHN0b3JhZ2UgaW4gYSBgc2V0YCBvciBgZGljdGAgaW5zdGFuY2UpLlxcbi4uLlxcbnR1cGxlKGl0ZXJhYmxlPVxcbi4uLlxcblRoZSBjb25zdHJ1Y3RvciBidWlsZHMgYSB0dXBsZSB3aG9zZSBpdGVtcyBhcmUgdGhlIHNhbWUgYW5kIGluIHRoZSBzYW1lIG9yZGVyIGFzIGl0ZXJhYmxlXFx1MjAxOXMgaXRlbXMuIGl0ZXJhYmxlIG1heSBiZSBlaXRoZXIgYSBzZXF1ZW5jZSwgYSBjb250YWluZXIgdGhhdCBzdXBwb3J0cyBpdGVyYXRpb24sIG9yIGFuIGl0ZXJhdG9yIG9iamVjdC4gSWYgaXRlcmFibGUgaXMgYWxyZWFkeSBhIHR1cGxlLCBpdCBpcyByZXR1cm5lZCB1bmNoYW5nZWQuIEZvciBleGFtcGxlLCBgdHVwbGUoJ2FiYycpYCByZXR1cm5zIGAoJ2EnLCAnYicsICdjJylgIGFuZCBgdHVwbGUoIFsxLCAyLCAzXSApYCByZXR1cm5zIGAoMSwgMiwgMylgLiBJZiBubyBhcmd1bWVudCBpcyBnaXZlbiwgdGhlIGNvbnN0cnVjdG9yIGNyZWF0ZXMgYSBuZXcgZW1wdHkgdHVwbGUsIGAoKWAuXFxuLi4uXFxuVHVwbGVzIGltcGxlbWVudCBhbGwgb2YgdGhlIGNvbW1vbiBzZXF1ZW5jZSBvcGVyYXRpb25zLlxcbi4uLlxcbkZvciBoZXRlcm9nZW5lb3VzIGNvbGxlY3Rpb25zIG9mIGRhdGEgd2hlcmUgYWNjZXNzIGJ5IG5hbWUgaXMgY2xlYXJlciB0aGFuIGFjY2VzcyBieSBpbmRleCwgYGNvbGxlY3Rpb25zLm5hbWVkdHVwbGUoKWAgbWF5IGJlIGEgbW9yZSBhcHByb3ByaWF0ZSBjaG9pY2UgdGhhbiBhIHNpbXBsZSB0dXBsZSBvYmplY3QuXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvbGlicmFyeS9zdGR0eXBlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJTb21lIGNvbGxlY3Rpb24gY2xhc3NlcyBhcmUgbXV0YWJsZS4gVGhlIG1ldGhvZHMgdGhhdCBhZGQsIHN1YnRyYWN0LCBvciByZWFycmFuZ2UgdGhlaXIgLi4uIGluIHBsYWNlLCBhbmQgZG9uXFx1MjAxOXQgcmV0dXJuIGEgLi4uICwgbmV2ZXIgcmV0dXJuIHRoZSBjb2xsZWN0aW9uIGluc3RhbmNlIGl0c2VsZiBidXQgYE5vbmVgLlxcbi4uLlxcbiMjIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgYGxpc3RgLCBgdHVwbGVgLCBgcmFuZ2VgXFx1MDBiNlxcblxcblRoZXJlIGFyZSB0aHJlZSBiYXNpYyBzZXF1ZW5jZSB0eXBlczogbGlzdHMsIHR1cGxlcywgYW5kIC4uLiBvYmplY3RzLiBBZGRpdGlvbmFsIHNlcXVlbmNlIHR5cGVzIHRhaWxvcmVkIGZvciBwcm9jZXNzaW5nIG9mIGJpbmFyeSBkYXRhIGFuZCB0ZXh0IHN0cmluZ3MgYXJlIGRlc2NyaWJlZCBpbiBkZWRpY2F0ZWQgc2VjdGlvbnMuXFxuLi4uXFxuVGhlIG9wZXJhdGlvbnMgaW4gdGhlIGZvbGxvd2luZyB0YWJsZSAuLi4gaW1tdXRhYmxlLiBUaGUgYCAuLi4gaXMgcHJvdmlkZWQgdG8gbWFrZSAuLi4gZWFzaWVyIHRvIGNvcnJlY3RseSBpbXBsZW1lbnQgdGhlc2Ugb3BlcmF0aW9ucyBvbiBjdXN0b20gc2VxdWVuY2UgdHlwZXNcXG4uLi5cXG4jIyMgSW1tdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbm9wZXJhdGlvbiB0aGF0IGltbXV0YWJsZSBzZXF1ZW5jZSAuLi4gYnkgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlcyBpcyAuLi4gYGhhc2goKWBcXG4uLi5cXG5UaGlzIHN1cHBvcnQgYWxsb3dzIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHN1Y2ggYXMgYHR1cGxlYCBpbnN0YW5jZXMsIHRvIGJlIHVzZWQgYXMgYGRpY3RgIGtleXMgYW5kIHN0b3JlZCBpbiBgc2V0YCBhbmQgYGZyb3plbnNldGAgaW5zdGFuY2VzLlxcbi4uLlxcbiMjIyBNdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbiMjIyBMaXN0c1xcdTAwYjZcXG5cXG5MaXN0cyBhcmUgbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhvbW9nZW5lb3VzIGl0ZW1zICh3aGVyZSB0aGUgcHJlY2lzZSBkZWdyZWUgb2Ygc2ltaWxhcml0eSB3aWxsIHZhcnkgYnkgYXBwbGljYXRpb24pLlxcbi4uLlxcbiMjIyBUdXBsZXNcXHUwMGI2XFxuXFxuVHVwbGVzIGFyZSBpbW11dGFibGUgc2VxdWVuY2VzLCB0eXBpY2FsbHkgdXNlZCB0byBzdG9yZSBjb2xsZWN0aW9ucyBvZiBoZXRlcm9nZW5lb3VzIGRhdGEgKHN1Y2ggYXMgdGhlIDItdHVwbGVzIHByb2R1Y2VkIGJ5IHRoZSBgZW51bWVyYXRlKClgIGJ1aWx0LWluKS4gVHVwbGVzIGFyZSBhbHNvIHVzZWQgZm9yIGNhc2VzIHdoZXJlIGFuIGltbXV0YWJsZSBzZXF1ZW5jZSBvZiBob21vZ2VuZW91cyBkYXRhIGlzIG5lZWRlZCAoc3VjaCBhcyBhbGxvd2luZyBzdG9yYWdlIGluIGEgYHNldGAgb3IgYGRpY3RgIGluc3RhbmNlKS5cXG4uLi5cXG5UaGUgY29uc3RydWN0b3IgYnVpbGRzIGEgdHVwbGUgd2hvc2UgaXRlbXMgYXJlIHRoZSBzYW1lIGFuZCBpbiB0aGUgc2FtZSBvcmRlciBhcyBpdGVyYWJsZVxcdTIwMTlzIGl0ZW1zLiBpdGVyYWJsZSBtYXkgYmUgZWl0aGVyIGEgc2VxdWVuY2UsIGEgY29udGFpbmVyIHRoYXQgc3VwcG9ydHMgaXRlcmF0aW9uLCBvciBhbiBpdGVyYXRvciBvYmplY3QuIElmIGl0ZXJhYmxlIGlzIGFscmVhZHkgYSB0dXBsZSwgaXQgaXMgcmV0dXJuZWQgdW5jaGFuZ2VkLiBGb3IgZXhhbXBsZSwgYHR1cGxlKCdhYmMnKWAgcmV0dXJucyBgKCdhJywgJ2InLCAnYycpYCBhbmQgYHR1cGxlKCBbMSwgMiwgM10gKWAgcmV0dXJucyBgKDEsIDIsIDMpYC4gSWYgbm8gYXJndW1lbnQgaXMgZ2l2ZW4sIHRoZSBjb25zdHJ1Y3RvciBjcmVhdGVzIGEgbmV3IGVtcHR5IHR1cGxlLCBgKClgLlxcbi4uLlxcblR1cGxlcyBpbXBsZW1lbnQgYWxsIG9mIHRoZSBjb21tb24gc2VxdWVuY2Ugb3BlcmF0aW9ucy5cXG4uLi5cXG5Gb3IgaGV0ZXJvZ2VuZW91cyBjb2xsZWN0aW9ucyBvZiBkYXRhIHdoZXJlIGFjY2VzcyBieSBuYW1lIGlzIGNsZWFyZXIgdGhhbiBhY2Nlc3MgYnkgaW5kZXgsIGBjb2xsZWN0aW9ucy5uYW1lZHR1cGxlKClgIG1heSBiZSBhIG1vcmUgYXBwcm9wcmlhdGUgY2hvaWNlIHRoYW4gYSBzaW1wbGUgdHVwbGUgb2JqZWN0LlwiXX0sIHtcInRpdGxlXCI6IFwiNS4gRGF0YSBTdHJ1Y3R1cmVzIFxcdTIwMTQgUHl0aG9uIDMuMTAuMjAgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMuMTAvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiWW91IG1pZ2h0IGhhdmUgbm90aWNlZCB0aGF0IG1ldGhvZHMgbGlrZSBgaW5zZXJ0YCwgYHJlbW92ZWAgb3IgYHNvcnRgIHRoYXQgb25seSBtb2RpZnkgdGhlIGxpc3QgaGF2ZSBubyByZXR1cm4gdmFsdWUgcHJpbnRlZCBcXHUyMDEzIHRoZXkgcmV0dXJuIHRoZSBkZWZhdWx0IGBOb25lYC4gMSBUaGlzIGlzIGEgZGVzaWduIHByaW5jaXBsZSBmb3IgYWxsIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIGluIFB5dGhvbi5cXG4uLi5cXG4jIyA1LjMuIFR1cGxlcyBhbmQgU2VxdWVuY2VzXFx1MDBiNlxcblxcbldlIHNhdyB0aGF0IGxpc3RzIGFuZCBzdHJpbmdzIGhhdmUgbWFueSBjb21tb24gcHJvcGVydGllcywgc3VjaCBhcyBpbmRleGluZyBhbmQgc2xpY2luZyBvcGVyYXRpb25zLiBUaGV5IGFyZSB0d28gZXhhbXBsZXMgb2Ygc2VxdWVuY2UgZGF0YSB0eXBlcyAoc2VlIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgbGlzdCwgdHVwbGUsIHJhbmdlKS4gU2luY2UgUHl0aG9uIGlzIGFuIGV2b2x2aW5nIGxhbmd1YWdlLCBvdGhlciBzZXF1ZW5jZSBkYXRhIHR5cGVzIG1heSBiZSBhZGRlZC4gVGhlcmUgaXMgYWxzbyBhbm90aGVyIHN0YW5kYXJkIHNlcXVlbmNlIGRhdGEgdHlwZTogdGhlIHR1cGxlLlxcblxcbkEgdHVwbGUgY29uc2lzdHMgb2YgYSBudW1iZXIgb2YgdmFsdWVzIHNlcGFyYXRlZCBieSBjb21tYXMsIGZvciBpbnN0YW5jZTpcXG4uLi5cXG4+Pj4gIyBUdXBsZXMgYXJlIGltbXV0YWJsZTpcXG4uLi4gdFswXSA9IDg4ODg4XFxuVHJhY2ViYWNrIChtb3N0IHJlY2VudCBjYWxsIGxhc3QpOlxcbiAgRmlsZSBcXFwiPHN0ZGluPlxcXCIsIGxpbmUgMSwgaW4gPG1vZHVsZT5cXG5UeXBlRXJyb3I6ICd0dXBsZScgb2JqZWN0IGRvZXMgbm90IHN1cHBvcnQgaXRlbSBhc3NpZ25tZW50XFxuPj4+ICMgYnV0IHRoZXkgY2FuIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzOlxcbi4uLiB2ID0gKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbj4+PiB2XFxuKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbi4uLlxcbkFzIHlvdSBzZWUsIG9uIG91dHB1dCB0dXBsZXMgYXJlIGFsd2F5cyBlbmNsb3NlZCBpbiBwYXJlbnRoZXNlcywgc28gdGhhdCBuZXN0ZWQgdHVwbGVzIGFyZSBpbnRlcnByZXRlZCBjb3JyZWN0bHk7IHRoZXkgbWF5IGJlIGlucHV0IHdpdGggb3Igd2l0aG91dCBzdXJyb3VuZGluZyBwYXJlbnRoZXNlcywgYWx0aG91Z2ggb2Z0ZW4gcGFyZW50aGVzZXMgYXJlIG5lY2Vzc2FyeSBhbnl3YXkgKGlmIHRoZSB0dXBsZSBpcyBwYXJ0IG9mIGEgbGFyZ2VyIGV4cHJlc3Npb24pLiBJdCBpcyBub3QgcG9zc2libGUgdG8gYXNzaWduIHRvIHRoZSBpbmRpdmlkdWFsIGl0ZW1zIG9mIGEgdHVwbGUsIGhvd2V2ZXIgaXQgaXMgcG9zc2libGUgdG8gY3JlYXRlIHR1cGxlcyB3aGljaCBjb250YWluIG11dGFibGUgb2JqZWN0cywgc3VjaCBhcyBsaXN0cy5cXG5cXG5UaG91Z2ggdHVwbGVzIG1heSBzZWVtIHNpbWlsYXIgdG8gbGlzdHMsIHRoZXkgYXJlIG9mdGVuIHVzZWQgaW4gZGlmZmVyZW50IHNpdHVhdGlvbnMgYW5kIGZvciBkaWZmZXJlbnQgcHVycG9zZXMuIFR1cGxlcyBhcmUgaW1tdXRhYmxlLCBhbmQgdXN1YWxseSBjb250YWluIGEgaGV0ZXJvZ2VuZW91cyBzZXF1ZW5jZSBvZiBlbGVtZW50cyB0aGF0IGFyZSBhY2Nlc3NlZCB2aWEgdW5wYWNraW5nIChzZWUgbGF0ZXIgaW4gdGhpcyBzZWN0aW9uKSBvciBpbmRleGluZyAob3IgZXZlbiBieSBhdHRyaWJ1dGUgaW4gdGhlIGNhc2Ugb2YgYG5hbWVkdHVwbGVzYCkuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG4jIyA1LjQuXFxuLi4uXFxuLiBTZXQgb2JqZWN0c1xcbi4uLlxcbiMjIDUuNS4gRGljdGlvbmFyaWVzXFx1MDBiNlxcblxcbkFub3RoZXIgdXNlZnVsIGRhdGEgdHlwZSBidWlsdCBpbnRvIFB5dGhvbiBpcyB0aGUgZGljdGlvbmFyeSAoc2VlIE1hcHBpbmcgVHlwZXMgXFx1MjAxNCBkaWN0KS4gRGljdGlvbmFyaWVzIGFyZSBzb21ldGltZXMgZm91bmQgaW4gb3RoZXIgbGFuZ3VhZ2VzIGFzIFxcdTIwMWNhc3NvY2lhdGl2ZSBtZW1vcmllc1xcdTIwMWQgb3IgXFx1MjAxY2Fzc29jaWF0aXZlIGFycmF5c1xcdTIwMWQuIFVubGlrZSBzZXF1ZW5jZXMsIHdoaWNoIGFyZSBpbmRleGVkIGJ5IGEgcmFuZ2Ugb2YgbnVtYmVycywgZGljdGlvbmFyaWVzIGFyZSBpbmRleGVkIGJ5IGtleXMsIHdoaWNoIGNhbiBiZSBhbnkgaW1tdXRhYmxlIHR5cGU7IHN0cmluZ3MgYW5kIG51bWJlcnMgY2FuIGFsd2F5cyBiZSBrZXlzLiBUdXBsZXMgY2FuIGJlIHVzZWQgYXMga2V5cyBpZiB0aGV5IGNvbnRhaW4gb25seSBzdHJpbmdzLCBudW1iZXJzLCBvciB0dXBsZXM7IGlmIGEgdHVwbGUgY29udGFpbnMgYW55IG11dGFibGUgb2JqZWN0IGVpdGhlciBkaXJlY3RseSBvciBpbmRpcmVjdGx5LCBpdCBjYW5ub3QgYmUgdXNlZCBhcyBhIGtleS4gWW91IGNhblxcdTIwMTl0IHVzZSBsaXN0cyBhcyBrZXlzLCBzaW5jZSBsaXN0cyBjYW4gYmUgbW9kaWZpZWQgaW4gcGxhY2UgdXNpbmcgaW5kZXggYXNzaWdubWVudHMsIHNsaWNlIGFzc2lnbm1lbnRzLCBvciBtZXRob2RzIGxpa2UgYGFwcGVuZCgpYCBhbmQgYGV4dGVuZCgpYC5cIl19XSIsInR5cGUiOiJ0b29sIiwibmFtZSI6IndlYl9zZWFyY2giLCJpZCI6IjZlNTEyNWFjLTMxNjQtNGE3Ni05YzUyLWFjMzQ2MGNlMzZlNCIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsInN0YXR1cyI6InN1Y2Nlc3MifX0seyJsYyI6MSwidHlwZSI6ImNvbnN0cnVjdG9yIiwiaWQiOlsibGFuZ2NoYWluIiwic2NoZW1hIiwibWVzc2FnZXMiLCJBSU1lc3NhZ2UiXSwia3dhcmdzIjp7ImNvbnRlbnQiOlt7ImlkIjoidG9vbHVfMDFYamozYVJkV3loQkpmUG1USkJ4UWE5IiwiY2FsbGVyIjp7InR5cGUiOiJkaXJlY3QifSwiaW5wdXQiOnt9LCJuYW1lIjoidHJhbnNmZXJfdG9fc2tlcHRpYyIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QTZGNGNDTjV5aEQ0Q3JzOU0iLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjB9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InJlc2VhcmNoZXIiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtM2NkOC03OWYxLTg4YTEtY2U2Y2VlMjk0ZTc5LTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoidHJhbnNmZXJfdG9fc2tlcHRpYyIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsInR5cGUiOiJ0b29sX2NhbGwifV0sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsInRvdGFsX3Rva2VucyI6NzQxMSwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19LCJpbnZhbGlkX3Rvb2xfY2FsbHMiOltdfX0seyJsYyI6MSwidHlwZSI6ImNvbnN0cnVjdG9yIiwiaWQiOlsibGFuZ2NoYWluIiwic2NoZW1hIiwibWVzc2FnZXMiLCJUb29sTWVzc2FnZSJdLCJrd2FyZ3MiOnsiY29udGVudCI6IlN1Y2Nlc3NmdWxseSB0cmFuc2ZlcnJlZCB0byBza2VwdGljIiwidHlwZSI6InRvb2wiLCJuYW1lIjoidHJhbnNmZXJfdG9fc2tlcHRpYyIsImlkIjoiMzg5MDU4ZDEtMDgzZi00NjMyLWJiYzUtMDliZmQzYTBmZmQyIiwidG9vbF9jYWxsX2lkIjoidG9vbHVfMDFYamozYVJkV3loQkpmUG1USkJ4UWE5Iiwic3RhdHVzIjoic3VjY2VzcyJ9fV1dfQ==" + } + }, + { + "key": "gen_ai.usage.input_tokens", + "value": { + "intValue": "7396" + } + }, + { + "key": "gen_ai.usage.output_tokens", + "value": { + "intValue": "770" + } + }, + { + "key": "gen_ai.usage.total_tokens", + "value": { + "intValue": "8166" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJnZW5lcmF0aW9ucyI6W1t7InRleHQiOiIiLCJnZW5lcmF0aW9uX2luZm8iOm51bGwsInR5cGUiOiJDaGF0R2VuZXJhdGlvbiIsIm1lc3NhZ2UiOnsibGMiOjEsInR5cGUiOiJjb25zdHJ1Y3RvciIsImlkIjpbImxhbmdjaGFpbiIsInNjaGVtYSIsIm1lc3NhZ2VzIiwiQUlNZXNzYWdlIl0sImt3YXJncyI6eyJjb250ZW50IjpbeyJzaWduYXR1cmUiOiJDQVFTMVFnS0VBZ1NHQUk0QVVJSWRHaHBibXRwYm1jU0RONlRmVTZORE05NzFsRWJ6eG9NdlJVQTFKOW94M2ZKUVlUOUlqQXJDUVFHSEJQNHBSSFBhdlh6UmxiTWJhWUhDNTdZaitHYnhncjBoZWx2ZDJPSUwvbzZSYnQ0UEZBM0ZhSUZVbGdxOGdjN0tRSTlGdEdudlNBa0xCUnpJc1VwLzJkNVJkMnpnRXNma0ZWZVhzbjhXWVlaa2luQVFWVnk2QTdCM0xhRUpXMHhiL3g4OERpNjdmZWR3eDBRK0ZsWnR3Y21UM0RjYWExMUp2ZXlIUnIzZG0rekdTWlA2Q1hlWE5KOG5yVFo3VzFZUlR5c0Q2dzdvN0xwOGgxUndxNGY0bzhqK1FVZlkwdmplK3pKWnN4SFBtUU50M3lrZUtoOTloZ242ZGJpejYvRUhxbWJIalNBdTFNNFNxRURmcUlLVGhIWWVuYW1BWm1jUTRYQU9vNm1kUTYrNzdWZ3BWMWxDMS9rNTJJaVVvSmprR2xRcm5MQ3Z1dGlJN0tOK3NnSzhjMisyS0NKclBSQ3ZWVitCa1N0dlA1aldXcXp5dHBLUStuSHpoTDQ0OE9ySUE5Z2RKQzM5QzhlRHJYZVJuSTd0NEZxbGtYR29NbFpBVjlVTWNyUUhSeE9NaTh0T3lUemk5b3lCRkRUU3NRK0xKcWVyMHR6MUZ0OWdlazZ2emc1MFNwVkFGUWRMRVJ2OFIxWXFNYWVJK3hzdlRBRVc5TVJndlJNa3pMdXpSVDJndXczakQrRmppcDM3eGdLNVFJMFIrQ3NPQW1RUG9UWml6emNJeUFVQXVDSkZDZ3EvZ0QvQ2liTHhveW1pVlVJMWFuOVNudDNLcE5Sb1dkdldJbmUzM1YwcHY4UEhnOXlXMkp1TE9xUUdpbExDc0svUTNvUXIzdXBFTVh4WEdqQ0w3V1VpenZSWkdGRUVWK1ZwbFl0N3Y0dEtaTWo1cGszdmRlTTBNSjVYWmc3UHZVc1J6eWN4Y25yVDNLbm9jM3FNK3R0cVpmVHNiOGQ2VS9wUWlmcTdHdWFoRk9IcnZyb3FnSkxqKy84MVl5MXBaRmtqZkpnWTBYa2x6QW5BNkNGcS9ZVDBneGJWK1RINUlTK1dhWU9VekszNlM4czVxcU13aG5ndjRJekUzY3RYWHFuaElaWE56bnlWQnFiWWEvR3gyYkNqY0VqMm41UnlHWVJTLytNY3VsZ0xCem5ROU1rRzJEN1FQM01xL253SFhLTUlhTFM4Vks4aEczZDByQWpsdm5hdVhTaE5ySnh0a2o0cWt3bnhENExyRWprMXVlK1BMY2JMcVlXd0FyOG4wcys0MWpyc0lQVnl5Szl2RmFFWnJBOUthQnhrSnlpcXg1MEdaTm43Y2t4K21rSTE4Z0g1Mml5VlB0VThGS01YanVnQ2w2bVBMaUhZaURRZCtHNjNMdWRtT0t5czlpdFZuQkRCTk1QMmIvSTcrYklWUmxoZUdhWldyNk1BVEtsODZCM3krMGt1T3JMUk15V2NkaWJjMmlRa1ptKzhmN2RUVXdiS2JCSnNLT0ZZNzMvOG5sTC9KaVlwano2blVJaytJcVNqYUZ5SzZYRmxDY3VrV3BXSHBESm5TdmJFemV5N3FIeUNxSitJK29uOGlQOEdPdGVtcFFhUzAvNGtaR1N3bHJFNnhPajZFd0dwZmpiRFpLSGtQLzJSQWROWVBRcTN6N2JkVkpNSk5uRk16NmUzN216TXBDQmlod1BkU0ZUUkUzU0NZNjZ3dlRnSnpHQ1V5aEhiU2k1aTFhenpPWGQ4dUhDU3BULzZMMW82VlUwcGpvUjhLYWE0d0NXSEdmN1AvV0VkK2F2eXAwMkpudTFjbXBXMCtHcSt2SzloMHRkYjJJQ2xNL2VOZmRvdVFQK2VwOE1KVWpyYURSSUNtQUpRWTVvYlM3MXZuaU9vK3JlYUZ5MHpSZ0IiLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsic2lnbmF0dXJlIjoiQ0FRUy9SRUtFUWdTR0FJNEFVSUpibUZ5Y21GMGFXOXVFZ3dGaDNBVkpaUDNzOHkxSHEwYURHNzZ1aEhPTmFTY1hBR1NoaUl3R2dqTFhIVXgxeVpya0xQbGpqelRPUXljcExKaUtWd1hjeG0vRXpLSm0xczhNNVd4dVJFV0VsZFhjb2FOMC9tc0twa1JNVCtlTGI1aWg5ckFIVUxUb21VTnNVd0pldTBqckplNEdMYXp5N3NQNUt0akVPRUJLUXNYVWZvUTdmRGtaT1FVSFppOXp6UU40aEZObVlKVWYzT1ZxZjlUUU5kQzErbS8vSEFaUGZXbUF5Nkh0RHZtN2tnQkprQ0VvaUZubTd0bi9QWDFoWEdQUmdXYzZCVFhVTXIyV0E4YUt5cWdnUlViTUgyK0pZdjVpV1pkZENjMXVwMU5icjNzRjdXUGNvdDlaQmh5SFFYcjY5MnplSms2U0pneWdJVnJBU2JNbHkyYkJ4VFdKQjYyM2dWZ3pwQ21WU0xaRnBrS1hnUGNwOGhHeURLT3ZGNlNpUVNIcUdSeFFvZnI3VnMxYVBoZlppeFJPSkdEK01scmtnZ1ZBbkdNeVlJRFVacGF2dENlaVUxSm1CU3owMDg0YlhQdFQvdG5wc0tPblNQWVpRbzNCMEJYS2JxQS9Xc0tuZzgvVjNaOFV1YWpvNklWWjJoZ0ExV0RkdXFRT1FrRXhHVTZwRnVMaGViSWp1V3hTTkkvRGZmdlIvL0xOcUNmdmRuZjBodm9QdER4ZngvU2F3SXg0L3NwcTl4YndGekh0TGdvSGcxRjJ3Rnc3Y2xQRGxnMVB5RC92MnlKRzNWOVQ4R0w2akdQRTZyaGZpbC91NEhOdmY3QlcwZFF6Q1RUWGNxWnk0bkJ5eTArcCtTN1p4Zk1sNEtweGEyZ09oaFNOamZwZnVYMFFwTDRrTUU4SjFubGlGTjFkOEtxRVB1REkxOEJRSFVoRC9VZlkyOW9BMHg1MTBGak0vQnNMWjRuNXNwcGtvSWk3Q0ZvUXEyYUFLVk5mQzFLaWxoSXBSRCsxZHlEdFpRa2xOOGdacE9FU0h3Nk1MMVR0VTB5ZkFjYm1xb3hKNmhoanp3SDZDTXNUUUVFeThNYlJCTStRS0trSVJkUkNQSEdGVzJMa3VSNTArSjBwRi90cjZwcVZ2SmVvemZpTW5yRWlsQjI3VFpQNG5LVEpHOHJkLy92Q1dlbjB5N0NBLzhkQko5Z2RwNXR4N05WQTQ1azFnZGN4RVIxZ1lvdU5YM24xdkczL0IxRjQrOTN3WFc5TXFvNlVzNTlUUW1yYjlOcWE3cFNRTlNySDA2dzFHeHlCQWxrRmZxQWxhZmsxNmFNcHE0MTZUSllwR3k0M1dDVWlDY0xXbGhpV2d6MnNnT0xBWFA5YTV3ZVdBS1JDOEo2amtjVWtEZVhuVytIRlNpUE02KzlCbzZrVmhMVWhONXJjNG1IWk9acEY3cHRQcHNRTjRYQ3FMUHNxcFJILzhvNVhweUM2Wm5lVGVGMkV4dGpod1pWalhEd01iSXpWWUlidzF5MlBQR3c4ZEM3cGM0c0lWU25JNG9RQUgwdjY1MGdoZmhtTWV2V1c0aDRQeWxidy9ERzRmZGVUMys4ejBmOE1PU1R0ZmlweDA1L1F0dEhsQXZPRFJ4YTV5REN2T3o2bjNmMUJPME4vNE1iR3lPTTI3cVc0eDhtQlZvcnlVWldmc21oZmkwL0dmTnZCOCt5bFRHYllOWDI3T2NFRjVXWnROcWJXRXk3VjVHYm1aVVN4c3BjZzdGQlozajMvM0w3bXV2WlhvWmkxZjVlcnE0eTNsU0grb0Y3cVBGbnZpZXlMT0N3dWJjUXkrb3lvd2o0Y0QwaWRSZWNucCtzKzUyRDcrbXpGMWdwVytmK2NKYXNveGlnYnhrT2xvUzR5ZWlLdmdNNG5XU3BHRWoxMlJQeDVvODBFbXppWE45dDR6SnpXMWh0QTVoWEpKZUZ5bXBNWXR5Q1VDZXZSV1ZtclJZa3RRN1VuQ2E2dEwvQVZQaTFuaTVYQ1hIZkxPdXFmSzVLTFl4andWdGRoaGZtQ0x2cGFLVk5DRU9DZjRuTkdIcUQ5OHdVYjBlaldOd0RZQm5KR2tCUnc5UlZzcGZ3TmRvV29WY24ycXY4eXdHVXA3eWMvc2syTHBJNlZ6aVJ5dldxYkdORzFaSnFWVnV0UmM0NitzT0U0VlNlUzIvQ1ROS05hNk5NQ2YvaHh4YVVWc1lmS2RQK1MxTGM0aWFuS3Awc1V1Z1BwbXRveWRDMENQMlJ3am5ITGNuenhDbnZxeGxMUVdYTU5QVHN1MnlCY0hsV0pMdGNtZGcvTWNMYWd5N2w1a0c2c043WnhKTEQva0F4OEU2dXFvaFArSy9qTVNyWXVRSm5ocjR0ZjRiaVdGWTNKemYvS3BBWXlWU3ErcGJmRktYTWJMOW5IRW9aUVQxNVd5YWJkNkRNQXEzUVpPdGhNNXBVemROcVVYa2NySTFQd2pBU0FnNGRWVS8ybys5OG1UWG55aFA3VmxFa1RuZGRwd3lKRitZQ0NjMDFwcWE2Wlg5OXBuUitZT3BJcmI1RHRQY0txMHQ1Y0ZDRHlReE4xbFV1cVpjWXVqdlgvR0M3KzZJcFJUei9Oa0VZLzVjbnRPZlpKeWQ2U2tVeC9KUjVBSklSUENOZWliMHJjV25uRm4wWEs4czIyeDQwd0xqRXR1ZlhETzkreXJhZjRrSHhsQkkzU0IwbWVNamVwWDUwQ0poQXg3TmVra1JGbndvZWNMZnFJeXJqWHJnQVpzVE1QaVg5NHNuamRyemlzQVh0QkthbnVsaWxTeE9mVzJIU21mbmswTDJHWEhKSjNTK3ZDYmdSRW1sOWRla2ZtSGhtd0VZdHYza2ZodGg3RVJJd1ZUOGo2OU90SUJDbWVUSUp1QTdvNzNQWGMxTEoyd2h1Wks3RlplNk5UT3M2MU1QN0pWbEljNnRiaHRPQmszTlhVOHBCUE9DWTZRNTVkNjNGdHBVVS9LQ2JRMEsyV1E5bzVqTkd3Tk5KNkN3NUZEQ3QreDVZSmVQMWQyUXpEU2lxK2NTWnF3cUNaVmNOb0FmdEdHemdyUlo0dmtwYlAweFFaSEN0V3NVd0hWVGpyWTROZlJEakhkVlBSbjhKOVh0b29jL2hJV2txekZhY3pZVGxhcHUyUkNKcWU4YUVvM2p6V1pDRUo3QWlCekJZcmFZRGdPZmVpUXllSlVzYmJYSGRHS2lpdzIyQmRwNE0rWXo2NzFNWjBpZTlFV1JrczBRMkRIR1VUbmU5UzR0QUFKUEdiVWY0eHRDTW9KNDBRNFFXMDVRTFNaeTN3TkZBVUZZbVhFNEVDREZCOCtJRWVORkZGL3RnQW5maDhHdkZoLzF4WStMRStvek1taW5qU1J3aHEwOXpRQUVrTkZ5dTBJS2d2RGt5T0kraWNjRjdkb1dMVFVhc1B4TEgxeEdQQlNLa2VXQ25Tb1gvNEMyYzlkdTY2QmJSWGFtSHFhZStWQ2lTNkJOOTUzaVlLT3U4ck1rbmVXK1RGalYwQ3pBZjhQTkc5TEJheVVnWTVVdzl4MFNhcDZ6STRQcGFaS0g0ZDlrdGtGVSs5MXlCYk41U1J0ZGQ4L0VRYXRKWG1CTmZ3N2tScmxsK0xqd1p3ZVdMRWIwalQxUnBHdURxSExkTFRNTWdvd1NjenJwMnpDc0pkUUgvc2VhWXJwd2tDRlI4ZFJqTTdKUlVrVmExQTBTdmRaQVg3NXZ1QnZ2TEJ6TjJjK1JidDVvcXk5M2xxSUJJaktiN3cyVWtSVGI0cEJXY05LTlBVZ3QzYXovUHhReEVhc2FsdGRtdXl2c1hZYnVyM1ZQTm44SnpOLzZKNU4rUUNuejU3MVRjY2FwQ21rUHZGRStnYXlYVStVRjFWS1l5RmNVdmxldjdsc1FsbWpxeksyUUE4WDdRM1R6RmFwcWh2SktsdDdkekJIcmFxWkJnUGdiOU40b0tOYXlpOEZaVDcxK2thWWhJaEZyRVNLWVJIZkhrajVuM2tmaGdMeSt4K2pMRGJLeE1nZ2tnSG5KejlSdUVIWlhCV3RrWUFRPT0iLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsiaWQiOiJ0b29sdV8wMUg2ZlY4SnZSSk52WHk0N2VSbmQzakgiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6e30sIm5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QU1SZFMyMUZlSmZ6UWhNM3MiLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzM5Niwib3V0cHV0X3Rva2VucyI6NzcwLCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjo3Mzl9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTRhZTktNzZjMC04OWE2LTRmMGIxMTRlMzAzMC0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6InRyYW5zZmVyX3RvX3ZlcmlmaWVyIiwiYXJncyI6e30sImlkIjoidG9vbHVfMDFINmZWOEp2UkpOdlh5NDdlUm5kM2pIIiwidHlwZSI6InRvb2xfY2FsbCJ9XSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjo3Mzk2LCJvdXRwdXRfdG9rZW5zIjo3NzAsInRvdGFsX3Rva2VucyI6ODE2NiwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6NzM5fX0sImludmFsaWRfdG9vbF9jYWxscyI6W119fX1dXSwibGxtX291dHB1dCI6eyJpZCI6Im1zZ18wMTFDZmR3QU1SZFMyMUZlSmZ6UWhNM3MiLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzM5Niwib3V0cHV0X3Rva2VucyI6NzcwLCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjo3Mzl9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0In0sInJ1biI6bnVsbCwidHlwZSI6IkxMTVJlc3VsdCJ9" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "419c86d999598365", + "parentSpanId": "0ff4846461350af9", + "name": "skeptic", + "kind": 1, + "startTimeUnixNano": "1790968023780292096", + "endTimeUnixNano": "1790968030843193088", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "e30=" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "skeptic" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "2" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "skeptic" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:skeptic\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"skeptic\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "skeptic:3dfc4328-0d19-e1ea-bcda-3d3d36d7cda8" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "skeptic" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "skeptic:3dfc4328-0d19-e1ea-bcda-3d3d36d7cda8" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "seq:step:1" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W3siY29udGVudCI6IkZvciBhIHN5bnRoZXRpYyB0cmFjaW5nIGZpeHR1cmUsIGV4cGxhaW4gUHl0aG9uIHR1cGxlIHZlcnN1cyBsaXN0IGltbXV0YWJpbGl0eSBpbiBhdCBtb3N0IDEyMCB3b3Jkcy4gVXNlIG9ubHkgb2ZmaWNpYWwgUHl0aG9uIGRvY3VtZW50YXRpb24gaWYgeW91IHNlYXJjaC4gSGF2ZSB0aGUgc2tlcHRpYyBhbmQgdmVyaWZpZXIgY2hlY2sgaXQsIHRoZW4gaGF2ZSB0aGUgZWRpdG9yIHdyaXRlIC9hbnN3ZXIubWQuIEtlZXAgdGhlIHJ1biBzaG9ydCBhbmQgZG8gbm90IGFzayBmb2xsb3ctdXAgcXVlc3Rpb25zLiIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoiaHVtYW4iLCJpZCI6IjQ2MjA0MzA1LWY4ZWUtNDM4NC05Y2U4LWZjZDgwYmZjZGY3NSJ9LHsiY29udGVudCI6W3siaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9LCJuYW1lIjoid2ViX3NlYXJjaCIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHc5YVc5YnJlblBxZ3liZk53UCIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjoyODQ1LCJvdXRwdXRfdG9rZW5zIjo3Mywib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6MH0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoicmVzZWFyY2hlciIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy0yMWEzLTcwMDAtYTM4MS0yODRhNmEwYzAzZjctMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ3ZWJfc2VhcmNoIiwiYXJncyI6eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9LCJpZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6Mjg0NSwib3V0cHV0X3Rva2VucyI6NzMsInRvdGFsX3Rva2VucyI6MjkxOCwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fSx7ImNvbnRlbnQiOiJbe1widGl0bGVcIjogXCI1LiBEYXRhIFN0cnVjdHVyZXMgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3R1dG9yaWFsL2RhdGFzdHJ1Y3R1cmVzLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcIllvdSBtaWdodCBoYXZlIG5vdGljZWQgdGhhdCBtZXRob2RzIGxpa2VgaW5zZXJ0YCxgcmVtb3ZlYCBvcmBzb3J0YCB0aGF0IG9ubHkgbW9kaWZ5IHRoZSBsaXN0IGhhdmUgbm8gcmV0dXJuIHZhbHVlIHByaW50ZWQgXFx1MjAxMyB0aGV5IHJldHVybiB0aGUgZGVmYXVsdGBOb25lYC4gWzFdIFRoaXMgaXMgYSBkZXNpZ24gcHJpbmNpcGxlIGZvciBhbGwgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgaW4gUHl0aG9uLlxcbi4uLlxcbiMjIDUuMy4gVHVwbGVzIGFuZCBTZXF1ZW5jZXNcXHUwMGI2XFxuXFxuV2Ugc2F3IHRoYXQgbGlzdHMgYW5kIHN0cmluZ3MgaGF2ZSBtYW55IGNvbW1vbiBwcm9wZXJ0aWVzLCBzdWNoIGFzIGluZGV4aW5nIGFuZCBzbGljaW5nIG9wZXJhdGlvbnMuIFRoZXkgYXJlIHR3byBleGFtcGxlcyBvZiBzZXF1ZW5jZSBkYXRhIHR5cGVzIChzZWUgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBsaXN0LCB0dXBsZSwgcmFuZ2UpLiBTaW5jZSBQeXRob24gaXMgYW4gZXZvbHZpbmcgbGFuZ3VhZ2UsIG90aGVyIHNlcXVlbmNlIGRhdGEgdHlwZXMgbWF5IGJlIGFkZGVkLiBUaGVyZSBpcyBhbHNvIGFub3RoZXIgc3RhbmRhcmQgc2VxdWVuY2UgZGF0YSB0eXBlOiB0aGUgdHVwbGUuXFxuXFxuQSB0dXBsZSBjb25zaXN0cyBvZiBhIG51bWJlciBvZiB2YWx1ZXMgc2VwYXJhdGVkIGJ5IGNvbW1hcywgZm9yIGluc3RhbmNlOlxcbi4uLlxcbj4+PiAjIFR1cGxlcyBhcmUgaW1tdXRhYmxlOlxcbj4+PiB0WzBdID0gODg4ODhcXG5UcmFjZWJhY2sgKG1vc3QgcmVjZW50IGNhbGwgbGFzdCk6XFxuICBGaWxlIFxcXCI8c3RkaW4+XFxcIiwgbGluZSAxLCBpbiA8bW9kdWxlPlxcblR5cGVFcnJvcjogJ3R1cGxlJyBvYmplY3QgZG9lcyBub3Qgc3VwcG9ydCBpdGVtIGFzc2lnbm1lbnRcXG4+Pj4gIyBidXQgdGhleSBjYW4gY29udGFpbiBtdXRhYmxlIG9iamVjdHM6XFxuPj4+IHYgPSAoWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuPj4+IHZcXG4oWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuLi4uXFxuQXMgeW91IHNlZSwgb24gb3V0cHV0IHR1cGxlcyBhcmUgYWx3YXlzIGVuY2xvc2VkIGluIHBhcmVudGhlc2VzLCBzbyB0aGF0IG5lc3RlZCB0dXBsZXMgYXJlIGludGVycHJldGVkIGNvcnJlY3RseTsgdGhleSBtYXkgYmUgaW5wdXQgd2l0aCBvciB3aXRob3V0IHN1cnJvdW5kaW5nIHBhcmVudGhlc2VzLCBhbHRob3VnaCBvZnRlbiBwYXJlbnRoZXNlcyBhcmUgbmVjZXNzYXJ5IGFueXdheSAoaWYgdGhlIHR1cGxlIGlzIHBhcnQgb2YgYSBsYXJnZXIgZXhwcmVzc2lvbikuIEl0IGlzIG5vdCBwb3NzaWJsZSB0byBhc3NpZ24gdG8gdGhlIGluZGl2aWR1YWwgaXRlbXMgb2YgYSB0dXBsZSwgaG93ZXZlciBpdCBpcyBwb3NzaWJsZSB0byBjcmVhdGUgdHVwbGVzIHdoaWNoIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzLCBzdWNoIGFzIGxpc3RzLlxcblxcblRob3VnaCB0dXBsZXMgbWF5IHNlZW0gc2ltaWxhciB0byBsaXN0cywgdGhleSBhcmUgb2Z0ZW4gdXNlZCBpbiBkaWZmZXJlbnQgc2l0dWF0aW9ucyBhbmQgZm9yIGRpZmZlcmVudCBwdXJwb3Nlcy4gVHVwbGVzIGFyZSBpbW11dGFibGUsIGFuZCB1c3VhbGx5IGNvbnRhaW4gYSBoZXRlcm9nZW5lb3VzIHNlcXVlbmNlIG9mIGVsZW1lbnRzIHRoYXQgYXJlIGFjY2Vzc2VkIHZpYSB1bnBhY2tpbmcgKHNlZSBsYXRlciBpbiB0aGlzIHNlY3Rpb24pIG9yIGluZGV4aW5nIChvciBldmVuIGJ5IGF0dHJpYnV0ZSBpbiB0aGUgY2FzZSBvZiBuYW1lZHR1cGxlcykuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG5Bbm90aGVyIHVzZWZ1bCBkYXRhIHR5cGUgYnVpbHQgaW50byBQeXRob24gaXMgdGhlIGRpY3Rpb25hcnkgKHNlZSBNYXBwaW5nIFR5cGVzIFxcdTIwMTQgZGljdCkuIERpY3Rpb25hcmllcyBhcmUgc29tZXRpbWVzIGZvdW5kIGluIG90aGVyIGxhbmd1YWdlcyBhcyBcXHUyMDFjYXNzb2NpYXRpdmUgbWVtb3JpZXNcXHUyMDFkIG9yIFxcdTIwMWNhc3NvY2lhdGl2ZSBhcnJheXNcXHUyMDFkLiBVbmxpa2Ugc2VxdWVuY2VzLCB3aGljaCBhcmUgaW5kZXhlZCBieSBhIHJhbmdlIG9mIG51bWJlcnMsIGRpY3Rpb25hcmllcyBhcmUgaW5kZXhlZCBieSBrZXlzLCB3aGljaCBjYW4gYmUgYW55IGltbXV0YWJsZSB0eXBlOyBzdHJpbmdzIGFuZCBudW1iZXJzIGNhbiBhbHdheXMgYmUga2V5cy4gVHVwbGVzIGNhbiBiZSB1c2VkIGFzIGtleXMgaWYgdGhleSBjb250YWluIG9ubHkgc3RyaW5ncywgbnVtYmVycywgb3IgdHVwbGVzOyBpZiBhIHR1cGxlIGNvbnRhaW5zIGFueSBtdXRhYmxlIG9iamVjdCBlaXRoZXIgZGlyZWN0bHkgb3IgaW5kaXJlY3RseSwgaXQgY2Fubm90IGJlIHVzZWQgYXMgYSBrZXkuIFlvdSBjYW5cXHUyMDE5dCB1c2UgbGlzdHMgYXMga2V5cywgc2luY2UgbGlzdHMgY2FuIGJlIG1vZGlmaWVkIGluIHBsYWNlIHVzaW5nIGluZGV4IGFzc2lnbm1lbnRzLCBzbGljZSBhc3NpZ25tZW50cywgb3IgbWV0aG9kcyBsaWtlIGFwcGVuZCgpIGFuZCBleHRlbmQoKS5cIl19LCB7XCJ0aXRsZVwiOiBcIjMuIERhdGEgbW9kZWwgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3JlZmVyZW5jZS9kYXRhbW9kZWwuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiVGhlIHZhbHVlIG9mIHNvbWUgb2JqZWN0cyBjYW4gY2hhbmdlLiBPYmplY3RzIHdob3NlIHZhbHVlIGNhbiBjaGFuZ2UgYXJlIHNhaWQgdG8gYmUgbXV0YWJsZTsgb2JqZWN0cyB3aG9zZSB2YWx1ZSBpcyB1bmNoYW5nZWFibGUgb25jZSB0aGV5IGFyZSBjcmVhdGVkIGFyZSBjYWxsZWQgaW1tdXRhYmxlLiAoVGhlIHZhbHVlIG9mIGFuIGltbXV0YWJsZSBjb250YWluZXIgb2JqZWN0IHRoYXQgY29udGFpbnMgYSByZWZlcmVuY2UgdG8gYSBtdXRhYmxlIG9iamVjdCBjYW4gY2hhbmdlIHdoZW4gdGhlIGxhdHRlclxcdTIwMTlzIHZhbHVlIGlzIGNoYW5nZWQ7IGhvd2V2ZXIgdGhlIGNvbnRhaW5lciBpcyBzdGlsbCBjb25zaWRlcmVkIGltbXV0YWJsZSwgYmVjYXVzZSB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGl0IGNvbnRhaW5zIGNhbm5vdCBiZSBjaGFuZ2VkLiBTbywgaW1tdXRhYmlsaXR5IGlzIG5vdCBzdHJpY3RseSB0aGUgc2FtZSBhcyBoYXZpbmcgYW4gdW5jaGFuZ2VhYmxlIHZhbHVlLCBpdCBpcyBtb3JlIHN1YnRsZS4pIEFuIG9iamVjdFxcdTIwMTlzIG11dGFiaWxpdHkgaXMgZGV0ZXJtaW5lZCBieSBpdHMgdHlwZTsgZm9yIGluc3RhbmNlLCBudW1iZXJzLCBzdHJpbmdzIGFuZCB0dXBsZXMgYXJlIGltbXV0YWJsZSwgd2hpbGUgZGljdGlvbmFyaWVzIGFuZCBsaXN0cyBhcmUgbXV0YWJsZS5cXG4uLi5cXG5Tb21lIG9iamVjdHMgY29udGFpbiByZWZlcmVuY2VzIHRvIG90aGVyIG9iamVjdHM7IHRoZXNlIGFyZSBjYWxsZWQgY29udGFpbmVycy4gRXhhbXBsZXMgb2YgY29udGFpbmVycyBhcmUgdHVwbGVzLCBsaXN0cyBhbmQgZGljdGlvbmFyaWVzLiBUaGUgcmVmZXJlbmNlcyBhcmUgcGFydCBvZiBhIGNvbnRhaW5lclxcdTIwMTlzIHZhbHVlLiBJbiBtb3N0IGNhc2VzLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIHZhbHVlIG9mIGEgY29udGFpbmVyLCB3ZSBpbXBseSB0aGUgdmFsdWVzLCBub3QgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGNvbnRhaW5lZCBvYmplY3RzOyBob3dldmVyLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIG11dGFiaWxpdHkgb2YgYSBjb250YWluZXIsIG9ubHkgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGltbWVkaWF0ZWx5IGNvbnRhaW5lZCBvYmplY3RzIGFyZSBpbXBsaWVkLiBTbywgaWYgYW4gaW1tdXRhYmxlIGNvbnRhaW5lciAobGlrZSBhIHR1cGxlKSBjb250YWlucyBhIHJlZmVyZW5jZSB0byBhIG11dGFibGUgb2JqZWN0LCBpdHMgdmFsdWUgY2hhbmdlcyBpZiB0aGF0IG11dGFibGUgb2JqZWN0IGlzIGNoYW5nZWQuXFxuLi4uXFxuIyMjIDMuMi41LiBTZXF1ZW5jZXNcXHUwMGI2XFxuLi4uXFxuIyMjIyAzLjIuNS4xLiBJbW11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbkFuIG9iamVjdCBvZiBhbiBpbW11dGFibGUgc2VxdWVuY2UgdHlwZSBjYW5ub3QgY2hhbmdlIG9uY2UgaXQgaXMgY3JlYXRlZC4gKElmIHRoZSBvYmplY3QgY29udGFpbnMgcmVmZXJlbmNlcyB0byBvdGhlciBvYmplY3RzLCB0aGVzZSBvdGhlciBvYmplY3RzIG1heSBiZSBtdXRhYmxlIGFuZCBtYXkgYmUgY2hhbmdlZDsgaG93ZXZlciwgdGhlIGNvbGxlY3Rpb24gb2Ygb2JqZWN0cyBkaXJlY3RseSByZWZlcmVuY2VkIGJ5IGFuIGltbXV0YWJsZSBvYmplY3QgY2Fubm90IGNoYW5nZS4pXFxuXFxuVGhlIGZvbGxvd2luZyB0eXBlcyBhcmUgaW1tdXRhYmxlIHNlcXVlbmNlczpcXG4uLi5cXG5UdXBsZXNcXG46IFRoZSBpdGVtcyBvZiBhIGB0dXBsZWAgYXJlIGFyYml0cmFyeSBQeXRob24gb2JqZWN0cy4gVHVwbGVzIG9mIHR3byBvciBtb3JlIGl0ZW1zIGFyZSBmb3JtZWQgYnkgY29tbWEtc2VwYXJhdGVkIGxpc3RzIG9mIGV4cHJlc3Npb25zLiBBIHR1cGxlIG9mIG9uZSBpdGVtIChhIFxcdTIwMThzaW5nbGV0b25cXHUyMDE5KSBjYW4gYmUgZm9ybWVkIGJ5IGFmZml4aW5nIGEgY29tbWEgdG8gYW4gZXhwcmVzc2lvbiAoYW4gZXhwcmVzc2lvbiBieSBpdHNlbGYgZG9lcyBub3QgY3JlYXRlIGEgdHVwbGUsIHNpbmNlIHBhcmVudGhlc2VzIG11c3QgYmUgdXNhYmxlIGZvciBncm91cGluZyBvZiBleHByZXNzaW9ucykuIEFuIGVtcHR5IHR1cGxlIGNhbiBiZSBmb3JtZWQgYnkgYW4gZW1wdHkgcGFpciBvZiBwYXJlbnRoZXNlcy5cXG4uLi5cXG4jIyMjIDMuMi41LjIuIE11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbk11dGFibGUgc2VxdWVuY2VzIGNhbiBiZSBjaGFuZ2VkIGFmdGVyIHRoZXkgYXJlIGNyZWF0ZWQuIFRoZSBzdWJzY3JpcHRpb24gYW5kIHNsaWNpbmcgbm90YXRpb25zIGNhbiBiZSB1c2VkIGFzIHRoZSB0YXJnZXQgb2YgYXNzaWdubWVudCBhbmQgYGRlbGAgKGRlbGV0ZSkgc3RhdGVtZW50cy5cXG4uLi5cXG5UaGVyZSBhcmUgY3VycmVudGx5IHR3byBpbnRyaW5zaWMgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlczpcXG5cXG5MaXN0c1xcbjogVGhlIGl0ZW1zIG9mIGEgbGlzdCBhcmUgYXJiaXRyYXJ5IFB5dGhvbiBvYmplY3RzLiBMaXN0cyBhcmUgZm9ybWVkIGJ5IHBsYWNpbmcgYSBjb21tYS1zZXBhcmF0ZWQgbGlzdCBvZiBleHByZXNzaW9ucyBpbiBzcXVhcmUgYnJhY2tldHMuIChOb3RlIHRoYXQgdGhlcmUgYXJlIG5vIHNwZWNpYWwgY2FzZXMgbmVlZGVkIHRvIGZvcm0gbGlzdHMgb2YgbGVuZ3RoIDAgb3IgMS4pXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjcgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvYnVpbHRpbnMvc3RkdHlwZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiY29sbGVjdGlvbiBjbGFzc2VzIGFyZSBtdXRhYmxlLiBUaGUgbWV0aG9kcyB0aGF0IGFkZCwgc3VidHJhY3QsIG9yIHJlYXJyYW5nZSB0aGVpciAuLi4gaW4gcGxhY2UsIGFuZCBkb25cXHUyMDE5dCByZXR1cm4gYSAuLi4gLCBuZXZlciByZXR1cm4gdGhlIGNvbGxlY3Rpb24gaW5zdGFuY2UgaXRzZWxmIGJ1dCBgTm9uZWAuXFxuLi4uXFxuIyMgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBgbGlzdGAsIGB0dXBsZWAsIGByYW5nZWBcXHUwMGI2XFxuXFxuVGhlcmUgYXJlIHRocmVlIGJhc2ljIHNlcXVlbmNlIHR5cGVzOiBsaXN0cywgdHVwbGVzLCBhbmQgcmFuZ2Ugb2JqZWN0cy4gQWRkaXRpb25hbCBzZXF1ZW5jZSB0eXBlcyB0YWlsb3JlZCBmb3IgcHJvY2Vzc2luZyBvZiBiaW5hcnkgZGF0YSBhbmQgdGV4dCBzdHJpbmdzIGFyZSBkZXNjcmliZWQgaW4gZGVkaWNhdGVkIHNlY3Rpb25zLlxcbi4uLlxcblRoZSBvcGVyYXRpb25zIGluIHRoZSBmb2xsb3dpbmcgdGFibGUgLi4uIG11dGFibGUgYW5kIGltbXV0YWJsZS4gVGhlIGBjb2xsZWN0aW9ucy4gLi4uIGlzIHByb3ZpZGVkIHRvIG1ha2UgLi4uIGVhc2llciB0byBjb3JyZWN0bHkgaW1wbGVtZW50IHRoZXNlIG9wZXJhdGlvbnMgb24gY3VzdG9tIHNlcXVlbmNlIHR5cGVzXFxuLi4uXFxuIyMjIEltbXV0YWJsZSBTZXF1ZW5jZSBUeXBlc1xcdTAwYjZcXG4uLi5cXG5tdXRhYmxlIHNlcXVlbmNlIHR5cGVzIGlzXFxuLi4uXFxuc3VwcG9ydCBhbGxvd3MgaW1tdXRhYmxlIHNlcXVlbmNlcywgLi4uICwgdG8gYmUgdXNlZCBhcyBgZGljdGAga2V5cyBhbmQgc3RvcmVkIGluIC4uLiBlbnNldGAgaW5zdGFuY2VzXFxuLi4uXFxuIyMjIE11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxuIyMjIExpc3RzXFx1MDBiNlxcblxcbkxpc3RzIGFyZSBtdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaG9tb2dlbmVvdXMgaXRlbXMgKHdoZXJlIHRoZSBwcmVjaXNlIGRlZ3JlZSBvZiBzaW1pbGFyaXR5IHdpbGwgdmFyeSBieSBhcHBsaWNhdGlvbikuXFxuLi4uXFxuIyMjIFR1cGxlc1xcdTAwYjZcXG5cXG5UdXBsZXMgYXJlIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhldGVyb2dlbmVvdXMgZGF0YSAoc3VjaCBhcyB0aGUgMi10dXBsZXMgcHJvZHVjZWQgYnkgdGhlIGBlbnVtZXJhdGUoKWAgYnVpbHQtaW4pLiBUdXBsZXMgYXJlIGFsc28gdXNlZCBmb3IgY2FzZXMgd2hlcmUgYW4gaW1tdXRhYmxlIHNlcXVlbmNlIG9mIGhvbW9nZW5lb3VzIGRhdGEgaXMgbmVlZGVkIChzdWNoIGFzIGFsbG93aW5nIHN0b3JhZ2UgaW4gYSBgc2V0YCBvciBgZGljdGAgaW5zdGFuY2UpLlxcbi4uLlxcbnR1cGxlKGl0ZXJhYmxlPVxcbi4uLlxcblRoZSBjb25zdHJ1Y3RvciBidWlsZHMgYSB0dXBsZSB3aG9zZSBpdGVtcyBhcmUgdGhlIHNhbWUgYW5kIGluIHRoZSBzYW1lIG9yZGVyIGFzIGl0ZXJhYmxlXFx1MjAxOXMgaXRlbXMuIGl0ZXJhYmxlIG1heSBiZSBlaXRoZXIgYSBzZXF1ZW5jZSwgYSBjb250YWluZXIgdGhhdCBzdXBwb3J0cyBpdGVyYXRpb24sIG9yIGFuIGl0ZXJhdG9yIG9iamVjdC4gSWYgaXRlcmFibGUgaXMgYWxyZWFkeSBhIHR1cGxlLCBpdCBpcyByZXR1cm5lZCB1bmNoYW5nZWQuIEZvciBleGFtcGxlLCBgdHVwbGUoJ2FiYycpYCByZXR1cm5zIGAoJ2EnLCAnYicsICdjJylgIGFuZCBgdHVwbGUoIFsxLCAyLCAzXSApYCByZXR1cm5zIGAoMSwgMiwgMylgLiBJZiBubyBhcmd1bWVudCBpcyBnaXZlbiwgdGhlIGNvbnN0cnVjdG9yIGNyZWF0ZXMgYSBuZXcgZW1wdHkgdHVwbGUsIGAoKWAuXFxuLi4uXFxuVHVwbGVzIGltcGxlbWVudCBhbGwgb2YgdGhlIGNvbW1vbiBzZXF1ZW5jZSBvcGVyYXRpb25zLlxcbi4uLlxcbkZvciBoZXRlcm9nZW5lb3VzIGNvbGxlY3Rpb25zIG9mIGRhdGEgd2hlcmUgYWNjZXNzIGJ5IG5hbWUgaXMgY2xlYXJlciB0aGFuIGFjY2VzcyBieSBpbmRleCwgYGNvbGxlY3Rpb25zLm5hbWVkdHVwbGUoKWAgbWF5IGJlIGEgbW9yZSBhcHByb3ByaWF0ZSBjaG9pY2UgdGhhbiBhIHNpbXBsZSB0dXBsZSBvYmplY3QuXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvbGlicmFyeS9zdGR0eXBlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJTb21lIGNvbGxlY3Rpb24gY2xhc3NlcyBhcmUgbXV0YWJsZS4gVGhlIG1ldGhvZHMgdGhhdCBhZGQsIHN1YnRyYWN0LCBvciByZWFycmFuZ2UgdGhlaXIgLi4uIGluIHBsYWNlLCBhbmQgZG9uXFx1MjAxOXQgcmV0dXJuIGEgLi4uICwgbmV2ZXIgcmV0dXJuIHRoZSBjb2xsZWN0aW9uIGluc3RhbmNlIGl0c2VsZiBidXQgYE5vbmVgLlxcbi4uLlxcbiMjIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgYGxpc3RgLCBgdHVwbGVgLCBgcmFuZ2VgXFx1MDBiNlxcblxcblRoZXJlIGFyZSB0aHJlZSBiYXNpYyBzZXF1ZW5jZSB0eXBlczogbGlzdHMsIHR1cGxlcywgYW5kIC4uLiBvYmplY3RzLiBBZGRpdGlvbmFsIHNlcXVlbmNlIHR5cGVzIHRhaWxvcmVkIGZvciBwcm9jZXNzaW5nIG9mIGJpbmFyeSBkYXRhIGFuZCB0ZXh0IHN0cmluZ3MgYXJlIGRlc2NyaWJlZCBpbiBkZWRpY2F0ZWQgc2VjdGlvbnMuXFxuLi4uXFxuVGhlIG9wZXJhdGlvbnMgaW4gdGhlIGZvbGxvd2luZyB0YWJsZSAuLi4gaW1tdXRhYmxlLiBUaGUgYCAuLi4gaXMgcHJvdmlkZWQgdG8gbWFrZSAuLi4gZWFzaWVyIHRvIGNvcnJlY3RseSBpbXBsZW1lbnQgdGhlc2Ugb3BlcmF0aW9ucyBvbiBjdXN0b20gc2VxdWVuY2UgdHlwZXNcXG4uLi5cXG4jIyMgSW1tdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbm9wZXJhdGlvbiB0aGF0IGltbXV0YWJsZSBzZXF1ZW5jZSAuLi4gYnkgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlcyBpcyAuLi4gYGhhc2goKWBcXG4uLi5cXG5UaGlzIHN1cHBvcnQgYWxsb3dzIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHN1Y2ggYXMgYHR1cGxlYCBpbnN0YW5jZXMsIHRvIGJlIHVzZWQgYXMgYGRpY3RgIGtleXMgYW5kIHN0b3JlZCBpbiBgc2V0YCBhbmQgYGZyb3plbnNldGAgaW5zdGFuY2VzLlxcbi4uLlxcbiMjIyBNdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbiMjIyBMaXN0c1xcdTAwYjZcXG5cXG5MaXN0cyBhcmUgbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhvbW9nZW5lb3VzIGl0ZW1zICh3aGVyZSB0aGUgcHJlY2lzZSBkZWdyZWUgb2Ygc2ltaWxhcml0eSB3aWxsIHZhcnkgYnkgYXBwbGljYXRpb24pLlxcbi4uLlxcbiMjIyBUdXBsZXNcXHUwMGI2XFxuXFxuVHVwbGVzIGFyZSBpbW11dGFibGUgc2VxdWVuY2VzLCB0eXBpY2FsbHkgdXNlZCB0byBzdG9yZSBjb2xsZWN0aW9ucyBvZiBoZXRlcm9nZW5lb3VzIGRhdGEgKHN1Y2ggYXMgdGhlIDItdHVwbGVzIHByb2R1Y2VkIGJ5IHRoZSBgZW51bWVyYXRlKClgIGJ1aWx0LWluKS4gVHVwbGVzIGFyZSBhbHNvIHVzZWQgZm9yIGNhc2VzIHdoZXJlIGFuIGltbXV0YWJsZSBzZXF1ZW5jZSBvZiBob21vZ2VuZW91cyBkYXRhIGlzIG5lZWRlZCAoc3VjaCBhcyBhbGxvd2luZyBzdG9yYWdlIGluIGEgYHNldGAgb3IgYGRpY3RgIGluc3RhbmNlKS5cXG4uLi5cXG5UaGUgY29uc3RydWN0b3IgYnVpbGRzIGEgdHVwbGUgd2hvc2UgaXRlbXMgYXJlIHRoZSBzYW1lIGFuZCBpbiB0aGUgc2FtZSBvcmRlciBhcyBpdGVyYWJsZVxcdTIwMTlzIGl0ZW1zLiBpdGVyYWJsZSBtYXkgYmUgZWl0aGVyIGEgc2VxdWVuY2UsIGEgY29udGFpbmVyIHRoYXQgc3VwcG9ydHMgaXRlcmF0aW9uLCBvciBhbiBpdGVyYXRvciBvYmplY3QuIElmIGl0ZXJhYmxlIGlzIGFscmVhZHkgYSB0dXBsZSwgaXQgaXMgcmV0dXJuZWQgdW5jaGFuZ2VkLiBGb3IgZXhhbXBsZSwgYHR1cGxlKCdhYmMnKWAgcmV0dXJucyBgKCdhJywgJ2InLCAnYycpYCBhbmQgYHR1cGxlKCBbMSwgMiwgM10gKWAgcmV0dXJucyBgKDEsIDIsIDMpYC4gSWYgbm8gYXJndW1lbnQgaXMgZ2l2ZW4sIHRoZSBjb25zdHJ1Y3RvciBjcmVhdGVzIGEgbmV3IGVtcHR5IHR1cGxlLCBgKClgLlxcbi4uLlxcblR1cGxlcyBpbXBsZW1lbnQgYWxsIG9mIHRoZSBjb21tb24gc2VxdWVuY2Ugb3BlcmF0aW9ucy5cXG4uLi5cXG5Gb3IgaGV0ZXJvZ2VuZW91cyBjb2xsZWN0aW9ucyBvZiBkYXRhIHdoZXJlIGFjY2VzcyBieSBuYW1lIGlzIGNsZWFyZXIgdGhhbiBhY2Nlc3MgYnkgaW5kZXgsIGBjb2xsZWN0aW9ucy5uYW1lZHR1cGxlKClgIG1heSBiZSBhIG1vcmUgYXBwcm9wcmlhdGUgY2hvaWNlIHRoYW4gYSBzaW1wbGUgdHVwbGUgb2JqZWN0LlwiXX0sIHtcInRpdGxlXCI6IFwiNS4gRGF0YSBTdHJ1Y3R1cmVzIFxcdTIwMTQgUHl0aG9uIDMuMTAuMjAgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMuMTAvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiWW91IG1pZ2h0IGhhdmUgbm90aWNlZCB0aGF0IG1ldGhvZHMgbGlrZSBgaW5zZXJ0YCwgYHJlbW92ZWAgb3IgYHNvcnRgIHRoYXQgb25seSBtb2RpZnkgdGhlIGxpc3QgaGF2ZSBubyByZXR1cm4gdmFsdWUgcHJpbnRlZCBcXHUyMDEzIHRoZXkgcmV0dXJuIHRoZSBkZWZhdWx0IGBOb25lYC4gMSBUaGlzIGlzIGEgZGVzaWduIHByaW5jaXBsZSBmb3IgYWxsIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIGluIFB5dGhvbi5cXG4uLi5cXG4jIyA1LjMuIFR1cGxlcyBhbmQgU2VxdWVuY2VzXFx1MDBiNlxcblxcbldlIHNhdyB0aGF0IGxpc3RzIGFuZCBzdHJpbmdzIGhhdmUgbWFueSBjb21tb24gcHJvcGVydGllcywgc3VjaCBhcyBpbmRleGluZyBhbmQgc2xpY2luZyBvcGVyYXRpb25zLiBUaGV5IGFyZSB0d28gZXhhbXBsZXMgb2Ygc2VxdWVuY2UgZGF0YSB0eXBlcyAoc2VlIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgbGlzdCwgdHVwbGUsIHJhbmdlKS4gU2luY2UgUHl0aG9uIGlzIGFuIGV2b2x2aW5nIGxhbmd1YWdlLCBvdGhlciBzZXF1ZW5jZSBkYXRhIHR5cGVzIG1heSBiZSBhZGRlZC4gVGhlcmUgaXMgYWxzbyBhbm90aGVyIHN0YW5kYXJkIHNlcXVlbmNlIGRhdGEgdHlwZTogdGhlIHR1cGxlLlxcblxcbkEgdHVwbGUgY29uc2lzdHMgb2YgYSBudW1iZXIgb2YgdmFsdWVzIHNlcGFyYXRlZCBieSBjb21tYXMsIGZvciBpbnN0YW5jZTpcXG4uLi5cXG4+Pj4gIyBUdXBsZXMgYXJlIGltbXV0YWJsZTpcXG4uLi4gdFswXSA9IDg4ODg4XFxuVHJhY2ViYWNrIChtb3N0IHJlY2VudCBjYWxsIGxhc3QpOlxcbiAgRmlsZSBcXFwiPHN0ZGluPlxcXCIsIGxpbmUgMSwgaW4gPG1vZHVsZT5cXG5UeXBlRXJyb3I6ICd0dXBsZScgb2JqZWN0IGRvZXMgbm90IHN1cHBvcnQgaXRlbSBhc3NpZ25tZW50XFxuPj4+ICMgYnV0IHRoZXkgY2FuIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzOlxcbi4uLiB2ID0gKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbj4+PiB2XFxuKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbi4uLlxcbkFzIHlvdSBzZWUsIG9uIG91dHB1dCB0dXBsZXMgYXJlIGFsd2F5cyBlbmNsb3NlZCBpbiBwYXJlbnRoZXNlcywgc28gdGhhdCBuZXN0ZWQgdHVwbGVzIGFyZSBpbnRlcnByZXRlZCBjb3JyZWN0bHk7IHRoZXkgbWF5IGJlIGlucHV0IHdpdGggb3Igd2l0aG91dCBzdXJyb3VuZGluZyBwYXJlbnRoZXNlcywgYWx0aG91Z2ggb2Z0ZW4gcGFyZW50aGVzZXMgYXJlIG5lY2Vzc2FyeSBhbnl3YXkgKGlmIHRoZSB0dXBsZSBpcyBwYXJ0IG9mIGEgbGFyZ2VyIGV4cHJlc3Npb24pLiBJdCBpcyBub3QgcG9zc2libGUgdG8gYXNzaWduIHRvIHRoZSBpbmRpdmlkdWFsIGl0ZW1zIG9mIGEgdHVwbGUsIGhvd2V2ZXIgaXQgaXMgcG9zc2libGUgdG8gY3JlYXRlIHR1cGxlcyB3aGljaCBjb250YWluIG11dGFibGUgb2JqZWN0cywgc3VjaCBhcyBsaXN0cy5cXG5cXG5UaG91Z2ggdHVwbGVzIG1heSBzZWVtIHNpbWlsYXIgdG8gbGlzdHMsIHRoZXkgYXJlIG9mdGVuIHVzZWQgaW4gZGlmZmVyZW50IHNpdHVhdGlvbnMgYW5kIGZvciBkaWZmZXJlbnQgcHVycG9zZXMuIFR1cGxlcyBhcmUgaW1tdXRhYmxlLCBhbmQgdXN1YWxseSBjb250YWluIGEgaGV0ZXJvZ2VuZW91cyBzZXF1ZW5jZSBvZiBlbGVtZW50cyB0aGF0IGFyZSBhY2Nlc3NlZCB2aWEgdW5wYWNraW5nIChzZWUgbGF0ZXIgaW4gdGhpcyBzZWN0aW9uKSBvciBpbmRleGluZyAob3IgZXZlbiBieSBhdHRyaWJ1dGUgaW4gdGhlIGNhc2Ugb2YgYG5hbWVkdHVwbGVzYCkuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG4jIyA1LjQuXFxuLi4uXFxuLiBTZXQgb2JqZWN0c1xcbi4uLlxcbiMjIDUuNS4gRGljdGlvbmFyaWVzXFx1MDBiNlxcblxcbkFub3RoZXIgdXNlZnVsIGRhdGEgdHlwZSBidWlsdCBpbnRvIFB5dGhvbiBpcyB0aGUgZGljdGlvbmFyeSAoc2VlIE1hcHBpbmcgVHlwZXMgXFx1MjAxNCBkaWN0KS4gRGljdGlvbmFyaWVzIGFyZSBzb21ldGltZXMgZm91bmQgaW4gb3RoZXIgbGFuZ3VhZ2VzIGFzIFxcdTIwMWNhc3NvY2lhdGl2ZSBtZW1vcmllc1xcdTIwMWQgb3IgXFx1MjAxY2Fzc29jaWF0aXZlIGFycmF5c1xcdTIwMWQuIFVubGlrZSBzZXF1ZW5jZXMsIHdoaWNoIGFyZSBpbmRleGVkIGJ5IGEgcmFuZ2Ugb2YgbnVtYmVycywgZGljdGlvbmFyaWVzIGFyZSBpbmRleGVkIGJ5IGtleXMsIHdoaWNoIGNhbiBiZSBhbnkgaW1tdXRhYmxlIHR5cGU7IHN0cmluZ3MgYW5kIG51bWJlcnMgY2FuIGFsd2F5cyBiZSBrZXlzLiBUdXBsZXMgY2FuIGJlIHVzZWQgYXMga2V5cyBpZiB0aGV5IGNvbnRhaW4gb25seSBzdHJpbmdzLCBudW1iZXJzLCBvciB0dXBsZXM7IGlmIGEgdHVwbGUgY29udGFpbnMgYW55IG11dGFibGUgb2JqZWN0IGVpdGhlciBkaXJlY3RseSBvciBpbmRpcmVjdGx5LCBpdCBjYW5ub3QgYmUgdXNlZCBhcyBhIGtleS4gWW91IGNhblxcdTIwMTl0IHVzZSBsaXN0cyBhcyBrZXlzLCBzaW5jZSBsaXN0cyBjYW4gYmUgbW9kaWZpZWQgaW4gcGxhY2UgdXNpbmcgaW5kZXggYXNzaWdubWVudHMsIHNsaWNlIGFzc2lnbm1lbnRzLCBvciBtZXRob2RzIGxpa2UgYGFwcGVuZCgpYCBhbmQgYGV4dGVuZCgpYC5cIl19XSIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ3ZWJfc2VhcmNoIiwiaWQiOiI2ZTUxMjVhYy0zMTY0LTRhNzYtOWM1Mi1hYzM0NjBjZTM2ZTQiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJzdGF0dXMiOiJzdWNjZXNzIn0seyJjb250ZW50IjpbeyJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3NrZXB0aWMiLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QTZGNGNDTjV5aEQ0Q3JzOU0iLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjB9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InJlc2VhcmNoZXIiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtM2NkOC03OWYxLTg4YTEtY2U2Y2VlMjk0ZTc5LTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoidHJhbnNmZXJfdG9fc2tlcHRpYyIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsInRvdGFsX3Rva2VucyI6NzQxMSwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fSx7ImNvbnRlbnQiOiJTdWNjZXNzZnVsbHkgdHJhbnNmZXJyZWQgdG8gc2tlcHRpYyIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ0cmFuc2Zlcl90b19za2VwdGljIiwiaWQiOiIzODkwNThkMS0wODNmLTQ2MzItYmJjNS0wOWJmZDNhMGZmZDIiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMVhqajNhUmRXeWhCSmZQbVRKQnhRYTkiLCJzdGF0dXMiOiJzdWNjZXNzIn1dLCJhY3RpdmVfYWdlbnQiOiJza2VwdGljIn0=" + } + } + ], + "events": [ + { + "timeUnixNano": "1790968030909441000", + "name": "exception", + "attributes": [ + { + "key": "exception.type", + "value": { + "stringValue": "Exception" + } + }, + { + "key": "exception.message", + "value": { + "stringValue": "ParentCommand(Command(graph='skeptic:3dfc4328-0d19-e1ea-bcda-3d3d36d7cda8', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', id='389058d1-083f-4632-bbc5-09bfd3a0ffd2', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9'), AIMessage(content=[{'signature': 'CAQS1QgKEAgSGAI4AUIIdGhpbmtpbmcSDN6TfU6NDM971lEbzxoMvRUA1J9ox3fJQYT9IjArCQQGHBP4pRHPavXzRlbMbaYHC57Yj+Gbxgr0helvd2OIL/o6Rbt4PFA3FaIFUlgq8gc7KQI9FtGnvSAkLBRzIsUp/2d5Rd2zgEsfkFVeXsn8WYYZkinAQVVy6A7B3LaEJW0xb/x88Di67fedwx0Q+FlZtwcmT3Dcaa11JveyHRr3dm+zGSZP6CXeXNJ8nrTZ7W1YRTysD6w7o7Lp8h1Rwq4f4o8j+QUfY0vje+zJZsxHPmQNt3ykeKh99hgn6dbiz6/EHqmbHjSAu1M4SqEDfqIKThHYenamAZmcQ4XAOo6mdQ6+77VgpV1lC1/k52IiUoJjkGlQrnLCvutiI7KN+sgK8c2+2KCJrPRCvVV+BkStvP5jWWqzytpKQ+nHzhL448OrIA9gdJC39C8eDrXeRnI7t4FqlkXGoMlZAV9UMcrQHRxOMi8tOyTzi9oyBFDTSsQ+LJqer0tz1Ft9gek6vzg50SpVAFQdLERv8R1YqMaeI+xsvTAEW9MRgvRMkzLuzRT2guw3jD+Fjip37xgK5QI0R+CsOAmQPoTZizzcIyAUAuCJFCgq/gD/CibLxoymiVUI1an9Snt3KpNRoWdvWIne33V0pv8PHg9yW2JuLOqQGilLCsK/Q3oQr3upEMXxXGjCL7WUizvRZGFEEV+VplYt7v4tKZMj5pk3vdeM0MJ5XZg7PvUsRzycxcnrT3Knoc3qM+ttqZfTsb8d6U/pQifq7GuahFOHrvroqgJLj+/81Yy1pZFkjfJgY0XklzAnA6CFq/YT0gxbV+TH5IS+WaYOUzK36S8s5qqMwhngv4IzE3ctXXqnhIZXNznyVBqbYa/Gx2bCjcEj2n5RyGYRS/+MculgLBznQ9MkG2D7QP3Mq/nwHXKMIaLS8VK8hG3d0rAjlvnauXShNrJxtkj4qkwnxD4LrEjk1ue+PLcbLqYWwAr8n0s+41jrsIPVyyK9vFaEZrA9KaBxkJyiqx50GZNn7ckx+mkI18gH52iyVPtU8FKMXjugCl6mPLiHYiDQd+G63LudmOKys9itVnBDBNMP2b/I7+bIVRlheGaZWr6MATKl86B3y+0kuOrLRMyWcdibc2iQkZm+8f7dTUwbKbBJsKOFY73/8nlL/JiYpjz6nUIk+IqSjaFyK6XFlCcukWpWHpDJnSvbEzey7qHyCqJ+I+on8iP8GOtempQaS0/4kZGSwlrE6xOj6EwGpfjbDZKHkP/2RAdNYPQq3z7bdVJMJNnFMz6e37mzMpCBihwPdSFTRE3SCY66wvTgJzGCUyhHbSi5i1azzOXd8uHCSpT/6L1o6VU0pjoR8Kaa4wCWHGf7P/WEd+avyp02Jnu1cmpW0+Gq+vK9h0tdb2IClM/eNfdouQP+ep8MJUjraDRICmAJQY5obS71vniOo+reaFy0zRgB', 'thinking': '', 'type': 'thinking'}, {'signature': 'CAQS/REKEQgSGAI4AUIJbmFycmF0aW9uEgwFh3AVJZP3s8y1Hq0aDG76uhHONaScXAGShiIwGgjLXHUx1yZrkLPljjzTOQycpLJiKVwXcxm/EzKJm1s8M5WxuREWEldXcoaN0/msKpkRMT+eLb5ih9rAHULTomUNsUwJeu0jrJe4GLazy7sP5KtjEOEBKQsXUfoQ7fDkZOQUHZi9zzQN4hFNmYJUf3OVqf9TQNdC1+m//HAZPfWmAy6HtDvm7kgBJkCEoiFnm7tn/PX1hXGPRgWc6BTXUMr2WA8aKyqggRUbMH2+JYv5iWZddCc1up1Nbr3sF7WPcot9ZBhyHQXr692zeJk6SJgygIVrASbMly2bBxTWJB623gVgzpCmVSLZFpkKXgPcp8hGyDKOvF6SiQSHqGRxQofr7Vs1aPhfZixROJGD+MlrkggVAnGMyYIDUZpavtCeiU1JmBSz0084bXPtT/tnpsKOnSPYZQo3B0BXKbqA/WsKng8/V3Z8Uuajo6IVZ2hgA1WDduqQOQkExGU6pFuLhebIjuWxSNI/DffvR//LNqCfvdnf0hvoPtDxfx/SawIx4/spq9xbwFzHtLgoHg1F2wFw7clPDlg1PyD/v2yJG3V9T8GL6jGPE6rhfil/u4HNvf7BW0dQzCTTXcqZy4nByy0+p+S7ZxfMl4Kpxa2gOhhSNjfpfuX0QpL4kME8J1nliFN1d8KqEPuDI18BQHUhD/UfY29oA0x510FjM/BsLZ4n5sppkoIi7CFoQq2aAKVNfC1KilhIpRD+1dyDtZQklN8gZpOESHw6ML1TtU0yfAcbmqoxJ6hhjzwH6CMsTQEEy8MbRBM+QKKkIRdRCPHGFW2LkuR50+J0pF/tr6pqVvJeozfiMnrEilB27TZP4nKTJG8rd//vCWen0y7CA/8dBJ9gdp5tx7NVA45k1gdcxER1gYouNX3n1vG3/B1F4+93wXW9Mqo6Us59TQmrb9Nqa7pSQNSrH06w1GxyBAlkFfqAlafk16aMpq416TJYpGy43WCUiCcLWlhiWgz2sgOLAXP9a5weWAKRC8J6jkcUkDeXnW+HFSiPM6+9Bo6kVhLUhN5rc4mHZOZpF7ptPpsQN4XCqLPsqpRH/8o5XpyC6ZneTeF2ExtjhwZVjXDwMbIzVYIbw1y2PPGw8dC7pc4sIVSnI4oQAH0v650ghfhmMevWW4h4Pylbw/DG4fdeT3+8z0f8MOSTtfipx05/QttHlAvODRxa5yDCvOz6n3f1BO0N/4MbGyOM27qW4x8mBVoryUZWfsmhfi0/GfNvB8+ylTGbYNX27OcEF5WZtNqbWEy7V5GbmZUSxspcg7FBZ3j3/3L7muvZXoZi1f5erq4y3lSH+oF7qPFnvieyLOCwubcQy+oyowj4cD0idRecnp+s+52D7+mzF1gpW+f+cJasoxigbxkOloS4yeiKvgM4nWSpGEj12RPx5o80EmziXN9t4zJzW1htA5hXJJeFympMYtyCUCevRWVmrRYktQ7UnCa6tL/AVPi1ni5XCXHfLOuqfK5KLYxjwVtdhhfmCLvpaKVNCEOCf4nNGHqD98wUb0ejWNwDYBnJGkBRw9RVspfwNdoWoVcn2qv8ywGUp7yc/sk2LpI6VziRyvWqbGNG1ZJqVVutRc46+sOE4VSeS2/CTNKNa6NMCf/hxxaUVsYfKdP+S1Lc4ianKp0sUugPpmtoydC0CP2RwjnHLcnzxCnvqxlLQWXMNPTsu2yBcHlWJLtcmdg/McLagy7l5kG6sN7ZxJLD/kAx8E6uqohP+K/jMSrYuQJnhr4tf4biWFY3Jzf/KpAYyVSq+pbfFKXMbL9nHEoZQT15Wyabd6DMAq3QZOthM5pUzdNqUXkcrI1PwjASAg4dVU/2o+98mTXnyhP7VlEkTnddpwyJF+YCCc01pqa6ZX99pnR+YOpIrb5DtPcKq0t5cFCDyQxN1lUuqZcYujvX/GC7+6IpRTz/NkEY/5cntOfZJyd6SkUx/JR5AJIRPCNeib0rcWnnFn0XK8s22x40wLjEtufXDO9+yraf4kHxlBI3SB0meMjepX50CJhAx7NekkRFnwoecLfqIyrjXrgAZsTMPiX94snjdrzisAXtBKanulilSxOfW2HSmfnk0L2GXHJJ3S+vCbgREml9dekfmHhmwEYtv3kfhth7ERIwVT8j69OtIBCmeTIJuA7o73PXc1LJ2whuZK7FZe6NTOs61MP7JVlIc6tbhtOBk3NXU8pBPOCY6Q55d63FtpUU/KCbQ0K2WQ9o5jNGwNNJ6Cw5FDCt+x5YJeP1d2QzDSiq+cSZqwqCZVcNoAftGGzgrRZ4vkpbP0xQZHCtWsUwHVTjrY4NfRDjHdVPRn8J9Xtooc/hIWkqzFaczYTlapu2RCJqe8aEo3jzWZCEJ7AiBzBYraYDgOfeiQyeJUsbbXHdGKiiw22Bdp4M+Yz671MZ0ie9EWRks0Q2DHGUTne9S4tAAJPGbUf4xtCMoJ40Q4QW05QLSZy3wNFAUFYmXE4ECDFB8+IEeNFFF/tgAnfh8GvFh/1xY+LE+ozMminjSRwhq09zQAEkNFyu0IKgvDkyOI+iccF7doWLTUasPxLH1xGPBSKkeWCnSoX/4C2c9du66BbRXamHqae+VCiS6BN953iYKOu8rMkneW+TFjV0CzAf8PNG9LBayUgY5Uw9x0Sap6zI4PpaZKH4d9ktkFU+91yBbN5SRtdd8/EQatJXmBNfw7kRrll+LjwZweWLEb0jT1RpGuDqHLdLTMMgowSczrp2zCsJdQH/seaYrpwkCFR8dRjM7JRUkVa1A0SvdZAX75vuBvvLBzN2c+Rbt5oqy93lqIBIjKb7w2UkRTb4pBWcNKNPUgt3az/PxQxEasaltdmuyvsXYbur3VPNn8JzN/6J5N+QCnz571TccapCmkPvFE+gayXU+UF1VKYyFcUvlev7lsQlmjqzK2QA8X7Q3TzFapqhvJKlt7dzBHraqZBgPgb9N4oKNayi8FZT71+kaYhIhFrESKYRHfHkj5n3kfhgLy+x+jLDbKxMggkgHnJz9RuEHZXBWtkYAQ==', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_verifier', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAMRdS21FeJfzQhM3s', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7396, 'output_tokens': 770, 'output_tokens_details': {'thinking_tokens': 739}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='skeptic', id='lc_run--01a0fe03-4ae9-76c0-89a6-4f0b114e3030-0', tool_calls=[{'name': 'transfer_to_verifier', 'args': {}, 'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7396, 'output_tokens': 770, 'total_tokens': 8166, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 739}}), ToolMessage(content='Successfully transferred to verifier', name='transfer_to_verifier', tool_call_id='toolu_01H6fV8JvRJNvXy47eRnd3jH')], 'active_agent': 'verifier'}, goto='verifier'))Traceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 2967, in stream\n for _ in runner.tick(\n ~~~~~~~~~~~^\n [t for t in loop.tasks.values() if not t.writes],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<2 lines>...\n schedule_task=loop.accept_push,\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ):\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_runner.py\", line 207, in tick\n run_with_retry(\n ~~~~~~~~~~~~~~^\n t,\n ^^\n ...<10 lines>...\n },\n ^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_retry.py\", line 617, in run_with_retry\n return task.proc.invoke(task.input, config)\n ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 709, in invoke\n input = step.invoke(input, config)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 447, in invoke\n ret = self.func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 84, in _write\n self.do_write(\n ~~~~~~~~~~~~~^\n config,\n ^^^^^^^\n writes,\n ^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 126, in do_write\n write(_assemble_writes(writes))\n ~~~~~~~~~~~~~~~~^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 181, in _assemble_writes\n if ww := w.mapper(w.value):\n ~~~~~~~~^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/graph/state.py\", line 1762, in _control_branch\n raise ParentCommand(command)\n\n\nlanggraph.errors.ParentCommand: Command(graph='skeptic:3dfc4328-0d19-e1ea-bcda-3d3d36d7cda8', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', id='389058d1-083f-4632-bbc5-09bfd3a0ffd2', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9'), AIMessage(content=[{'signature': 'CAQS1QgKEAgSGAI4AUIIdGhpbmtpbmcSDN6TfU6NDM971lEbzxoMvRUA1J9ox3fJQYT9IjArCQQGHBP4pRHPavXzRlbMbaYHC57Yj+Gbxgr0helvd2OIL/o6Rbt4PFA3FaIFUlgq8gc7KQI9FtGnvSAkLBRzIsUp/2d5Rd2zgEsfkFVeXsn8WYYZkinAQVVy6A7B3LaEJW0xb/x88Di67fedwx0Q+FlZtwcmT3Dcaa11JveyHRr3dm+zGSZP6CXeXNJ8nrTZ7W1YRTysD6w7o7Lp8h1Rwq4f4o8j+QUfY0vje+zJZsxHPmQNt3ykeKh99hgn6dbiz6/EHqmbHjSAu1M4SqEDfqIKThHYenamAZmcQ4XAOo6mdQ6+77VgpV1lC1/k52IiUoJjkGlQrnLCvutiI7KN+sgK8c2+2KCJrPRCvVV+BkStvP5jWWqzytpKQ+nHzhL448OrIA9gdJC39C8eDrXeRnI7t4FqlkXGoMlZAV9UMcrQHRxOMi8tOyTzi9oyBFDTSsQ+LJqer0tz1Ft9gek6vzg50SpVAFQdLERv8R1YqMaeI+xsvTAEW9MRgvRMkzLuzRT2guw3jD+Fjip37xgK5QI0R+CsOAmQPoTZizzcIyAUAuCJFCgq/gD/CibLxoymiVUI1an9Snt3KpNRoWdvWIne33V0pv8PHg9yW2JuLOqQGilLCsK/Q3oQr3upEMXxXGjCL7WUizvRZGFEEV+VplYt7v4tKZMj5pk3vdeM0MJ5XZg7PvUsRzycxcnrT3Knoc3qM+ttqZfTsb8d6U/pQifq7GuahFOHrvroqgJLj+/81Yy1pZFkjfJgY0XklzAnA6CFq/YT0gxbV+TH5IS+WaYOUzK36S8s5qqMwhngv4IzE3ctXXqnhIZXNznyVBqbYa/Gx2bCjcEj2n5RyGYRS/+MculgLBznQ9MkG2D7QP3Mq/nwHXKMIaLS8VK8hG3d0rAjlvnauXShNrJxtkj4qkwnxD4LrEjk1ue+PLcbLqYWwAr8n0s+41jrsIPVyyK9vFaEZrA9KaBxkJyiqx50GZNn7ckx+mkI18gH52iyVPtU8FKMXjugCl6mPLiHYiDQd+G63LudmOKys9itVnBDBNMP2b/I7+bIVRlheGaZWr6MATKl86B3y+0kuOrLRMyWcdibc2iQkZm+8f7dTUwbKbBJsKOFY73/8nlL/JiYpjz6nUIk+IqSjaFyK6XFlCcukWpWHpDJnSvbEzey7qHyCqJ+I+on8iP8GOtempQaS0/4kZGSwlrE6xOj6EwGpfjbDZKHkP/2RAdNYPQq3z7bdVJMJNnFMz6e37mzMpCBihwPdSFTRE3SCY66wvTgJzGCUyhHbSi5i1azzOXd8uHCSpT/6L1o6VU0pjoR8Kaa4wCWHGf7P/WEd+avyp02Jnu1cmpW0+Gq+vK9h0tdb2IClM/eNfdouQP+ep8MJUjraDRICmAJQY5obS71vniOo+reaFy0zRgB', 'thinking': '', 'type': 'thinking'}, {'signature': 'CAQS/REKEQgSGAI4AUIJbmFycmF0aW9uEgwFh3AVJZP3s8y1Hq0aDG76uhHONaScXAGShiIwGgjLXHUx1yZrkLPljjzTOQycpLJiKVwXcxm/EzKJm1s8M5WxuREWEldXcoaN0/msKpkRMT+eLb5ih9rAHULTomUNsUwJeu0jrJe4GLazy7sP5KtjEOEBKQsXUfoQ7fDkZOQUHZi9zzQN4hFNmYJUf3OVqf9TQNdC1+m//HAZPfWmAy6HtDvm7kgBJkCEoiFnm7tn/PX1hXGPRgWc6BTXUMr2WA8aKyqggRUbMH2+JYv5iWZddCc1up1Nbr3sF7WPcot9ZBhyHQXr692zeJk6SJgygIVrASbMly2bBxTWJB623gVgzpCmVSLZFpkKXgPcp8hGyDKOvF6SiQSHqGRxQofr7Vs1aPhfZixROJGD+MlrkggVAnGMyYIDUZpavtCeiU1JmBSz0084bXPtT/tnpsKOnSPYZQo3B0BXKbqA/WsKng8/V3Z8Uuajo6IVZ2hgA1WDduqQOQkExGU6pFuLhebIjuWxSNI/DffvR//LNqCfvdnf0hvoPtDxfx/SawIx4/spq9xbwFzHtLgoHg1F2wFw7clPDlg1PyD/v2yJG3V9T8GL6jGPE6rhfil/u4HNvf7BW0dQzCTTXcqZy4nByy0+p+S7ZxfMl4Kpxa2gOhhSNjfpfuX0QpL4kME8J1nliFN1d8KqEPuDI18BQHUhD/UfY29oA0x510FjM/BsLZ4n5sppkoIi7CFoQq2aAKVNfC1KilhIpRD+1dyDtZQklN8gZpOESHw6ML1TtU0yfAcbmqoxJ6hhjzwH6CMsTQEEy8MbRBM+QKKkIRdRCPHGFW2LkuR50+J0pF/tr6pqVvJeozfiMnrEilB27TZP4nKTJG8rd//vCWen0y7CA/8dBJ9gdp5tx7NVA45k1gdcxER1gYouNX3n1vG3/B1F4+93wXW9Mqo6Us59TQmrb9Nqa7pSQNSrH06w1GxyBAlkFfqAlafk16aMpq416TJYpGy43WCUiCcLWlhiWgz2sgOLAXP9a5weWAKRC8J6jkcUkDeXnW+HFSiPM6+9Bo6kVhLUhN5rc4mHZOZpF7ptPpsQN4XCqLPsqpRH/8o5XpyC6ZneTeF2ExtjhwZVjXDwMbIzVYIbw1y2PPGw8dC7pc4sIVSnI4oQAH0v650ghfhmMevWW4h4Pylbw/DG4fdeT3+8z0f8MOSTtfipx05/QttHlAvODRxa5yDCvOz6n3f1BO0N/4MbGyOM27qW4x8mBVoryUZWfsmhfi0/GfNvB8+ylTGbYNX27OcEF5WZtNqbWEy7V5GbmZUSxspcg7FBZ3j3/3L7muvZXoZi1f5erq4y3lSH+oF7qPFnvieyLOCwubcQy+oyowj4cD0idRecnp+s+52D7+mzF1gpW+f+cJasoxigbxkOloS4yeiKvgM4nWSpGEj12RPx5o80EmziXN9t4zJzW1htA5hXJJeFympMYtyCUCevRWVmrRYktQ7UnCa6tL/AVPi1ni5XCXHfLOuqfK5KLYxjwVtdhhfmCLvpaKVNCEOCf4nNGHqD98wUb0ejWNwDYBnJGkBRw9RVspfwNdoWoVcn2qv8ywGUp7yc/sk2LpI6VziRyvWqbGNG1ZJqVVutRc46+sOE4VSeS2/CTNKNa6NMCf/hxxaUVsYfKdP+S1Lc4ianKp0sUugPpmtoydC0CP2RwjnHLcnzxCnvqxlLQWXMNPTsu2yBcHlWJLtcmdg/McLagy7l5kG6sN7ZxJLD/kAx8E6uqohP+K/jMSrYuQJnhr4tf4biWFY3Jzf/KpAYyVSq+pbfFKXMbL9nHEoZQT15Wyabd6DMAq3QZOthM5pUzdNqUXkcrI1PwjASAg4dVU/2o+98mTXnyhP7VlEkTnddpwyJF+YCCc01pqa6ZX99pnR+YOpIrb5DtPcKq0t5cFCDyQxN1lUuqZcYujvX/GC7+6IpRTz/NkEY/5cntOfZJyd6SkUx/JR5AJIRPCNeib0rcWnnFn0XK8s22x40wLjEtufXDO9+yraf4kHxlBI3SB0meMjepX50CJhAx7NekkRFnwoecLfqIyrjXrgAZsTMPiX94snjdrzisAXtBKanulilSxOfW2HSmfnk0L2GXHJJ3S+vCbgREml9dekfmHhmwEYtv3kfhth7ERIwVT8j69OtIBCmeTIJuA7o73PXc1LJ2whuZK7FZe6NTOs61MP7JVlIc6tbhtOBk3NXU8pBPOCY6Q55d63FtpUU/KCbQ0K2WQ9o5jNGwNNJ6Cw5FDCt+x5YJeP1d2QzDSiq+cSZqwqCZVcNoAftGGzgrRZ4vkpbP0xQZHCtWsUwHVTjrY4NfRDjHdVPRn8J9Xtooc/hIWkqzFaczYTlapu2RCJqe8aEo3jzWZCEJ7AiBzBYraYDgOfeiQyeJUsbbXHdGKiiw22Bdp4M+Yz671MZ0ie9EWRks0Q2DHGUTne9S4tAAJPGbUf4xtCMoJ40Q4QW05QLSZy3wNFAUFYmXE4ECDFB8+IEeNFFF/tgAnfh8GvFh/1xY+LE+ozMminjSRwhq09zQAEkNFyu0IKgvDkyOI+iccF7doWLTUasPxLH1xGPBSKkeWCnSoX/4C2c9du66BbRXamHqae+VCiS6BN953iYKOu8rMkneW+TFjV0CzAf8PNG9LBayUgY5Uw9x0Sap6zI4PpaZKH4d9ktkFU+91yBbN5SRtdd8/EQatJXmBNfw7kRrll+LjwZweWLEb0jT1RpGuDqHLdLTMMgowSczrp2zCsJdQH/seaYrpwkCFR8dRjM7JRUkVa1A0SvdZAX75vuBvvLBzN2c+Rbt5oqy93lqIBIjKb7w2UkRTb4pBWcNKNPUgt3az/PxQxEasaltdmuyvsXYbur3VPNn8JzN/6J5N+QCnz571TccapCmkPvFE+gayXU+UF1VKYyFcUvlev7lsQlmjqzK2QA8X7Q3TzFapqhvJKlt7dzBHraqZBgPgb9N4oKNayi8FZT71+kaYhIhFrESKYRHfHkj5n3kfhgLy+x+jLDbKxMggkgHnJz9RuEHZXBWtkYAQ==', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_verifier', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAMRdS21FeJfzQhM3s', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7396, 'output_tokens': 770, 'output_tokens_details': {'thinking_tokens': 739}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='skeptic', id='lc_run--01a0fe03-4ae9-76c0-89a6-4f0b114e3030-0', tool_calls=[{'name': 'transfer_to_verifier', 'args': {}, 'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7396, 'output_tokens': 770, 'total_tokens': 8166, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 739}}), ToolMessage(content='Successfully transferred to verifier', name='transfer_to_verifier', tool_call_id='toolu_01H6fV8JvRJNvXy47eRnd3jH')], 'active_agent': 'verifier'}, goto='verifier')" + } + }, + { + "key": "exception.stacktrace", + "value": { + "stringValue": "Exception: ParentCommand(Command(graph='skeptic:3dfc4328-0d19-e1ea-bcda-3d3d36d7cda8', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', id='389058d1-083f-4632-bbc5-09bfd3a0ffd2', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9'), AIMessage(content=[{'signature': 'CAQS1QgKEAgSGAI4AUIIdGhpbmtpbmcSDN6TfU6NDM971lEbzxoMvRUA1J9ox3fJQYT9IjArCQQGHBP4pRHPavXzRlbMbaYHC57Yj+Gbxgr0helvd2OIL/o6Rbt4PFA3FaIFUlgq8gc7KQI9FtGnvSAkLBRzIsUp/2d5Rd2zgEsfkFVeXsn8WYYZkinAQVVy6A7B3LaEJW0xb/x88Di67fedwx0Q+FlZtwcmT3Dcaa11JveyHRr3dm+zGSZP6CXeXNJ8nrTZ7W1YRTysD6w7o7Lp8h1Rwq4f4o8j+QUfY0vje+zJZsxHPmQNt3ykeKh99hgn6dbiz6/EHqmbHjSAu1M4SqEDfqIKThHYenamAZmcQ4XAOo6mdQ6+77VgpV1lC1/k52IiUoJjkGlQrnLCvutiI7KN+sgK8c2+2KCJrPRCvVV+BkStvP5jWWqzytpKQ+nHzhL448OrIA9gdJC39C8eDrXeRnI7t4FqlkXGoMlZAV9UMcrQHRxOMi8tOyTzi9oyBFDTSsQ+LJqer0tz1Ft9gek6vzg50SpVAFQdLERv8R1YqMaeI+xsvTAEW9MRgvRMkzLuzRT2guw3jD+Fjip37xgK5QI0R+CsOAmQPoTZizzcIyAUAuCJFCgq/gD/CibLxoymiVUI1an9Snt3KpNRoWdvWIne33V0pv8PHg9yW2JuLOqQGilLCsK/Q3oQr3upEMXxXGjCL7WUizvRZGFEEV+VplYt7v4tKZMj5pk3vdeM0MJ5XZg7PvUsRzycxcnrT3Knoc3qM+ttqZfTsb8d6U/pQifq7GuahFOHrvroqgJLj+/81Yy1pZFkjfJgY0XklzAnA6CFq/YT0gxbV+TH5IS+WaYOUzK36S8s5qqMwhngv4IzE3ctXXqnhIZXNznyVBqbYa/Gx2bCjcEj2n5RyGYRS/+MculgLBznQ9MkG2D7QP3Mq/nwHXKMIaLS8VK8hG3d0rAjlvnauXShNrJxtkj4qkwnxD4LrEjk1ue+PLcbLqYWwAr8n0s+41jrsIPVyyK9vFaEZrA9KaBxkJyiqx50GZNn7ckx+mkI18gH52iyVPtU8FKMXjugCl6mPLiHYiDQd+G63LudmOKys9itVnBDBNMP2b/I7+bIVRlheGaZWr6MATKl86B3y+0kuOrLRMyWcdibc2iQkZm+8f7dTUwbKbBJsKOFY73/8nlL/JiYpjz6nUIk+IqSjaFyK6XFlCcukWpWHpDJnSvbEzey7qHyCqJ+I+on8iP8GOtempQaS0/4kZGSwlrE6xOj6EwGpfjbDZKHkP/2RAdNYPQq3z7bdVJMJNnFMz6e37mzMpCBihwPdSFTRE3SCY66wvTgJzGCUyhHbSi5i1azzOXd8uHCSpT/6L1o6VU0pjoR8Kaa4wCWHGf7P/WEd+avyp02Jnu1cmpW0+Gq+vK9h0tdb2IClM/eNfdouQP+ep8MJUjraDRICmAJQY5obS71vniOo+reaFy0zRgB', 'thinking': '', 'type': 'thinking'}, {'signature': 'CAQS/REKEQgSGAI4AUIJbmFycmF0aW9uEgwFh3AVJZP3s8y1Hq0aDG76uhHONaScXAGShiIwGgjLXHUx1yZrkLPljjzTOQycpLJiKVwXcxm/EzKJm1s8M5WxuREWEldXcoaN0/msKpkRMT+eLb5ih9rAHULTomUNsUwJeu0jrJe4GLazy7sP5KtjEOEBKQsXUfoQ7fDkZOQUHZi9zzQN4hFNmYJUf3OVqf9TQNdC1+m//HAZPfWmAy6HtDvm7kgBJkCEoiFnm7tn/PX1hXGPRgWc6BTXUMr2WA8aKyqggRUbMH2+JYv5iWZddCc1up1Nbr3sF7WPcot9ZBhyHQXr692zeJk6SJgygIVrASbMly2bBxTWJB623gVgzpCmVSLZFpkKXgPcp8hGyDKOvF6SiQSHqGRxQofr7Vs1aPhfZixROJGD+MlrkggVAnGMyYIDUZpavtCeiU1JmBSz0084bXPtT/tnpsKOnSPYZQo3B0BXKbqA/WsKng8/V3Z8Uuajo6IVZ2hgA1WDduqQOQkExGU6pFuLhebIjuWxSNI/DffvR//LNqCfvdnf0hvoPtDxfx/SawIx4/spq9xbwFzHtLgoHg1F2wFw7clPDlg1PyD/v2yJG3V9T8GL6jGPE6rhfil/u4HNvf7BW0dQzCTTXcqZy4nByy0+p+S7ZxfMl4Kpxa2gOhhSNjfpfuX0QpL4kME8J1nliFN1d8KqEPuDI18BQHUhD/UfY29oA0x510FjM/BsLZ4n5sppkoIi7CFoQq2aAKVNfC1KilhIpRD+1dyDtZQklN8gZpOESHw6ML1TtU0yfAcbmqoxJ6hhjzwH6CMsTQEEy8MbRBM+QKKkIRdRCPHGFW2LkuR50+J0pF/tr6pqVvJeozfiMnrEilB27TZP4nKTJG8rd//vCWen0y7CA/8dBJ9gdp5tx7NVA45k1gdcxER1gYouNX3n1vG3/B1F4+93wXW9Mqo6Us59TQmrb9Nqa7pSQNSrH06w1GxyBAlkFfqAlafk16aMpq416TJYpGy43WCUiCcLWlhiWgz2sgOLAXP9a5weWAKRC8J6jkcUkDeXnW+HFSiPM6+9Bo6kVhLUhN5rc4mHZOZpF7ptPpsQN4XCqLPsqpRH/8o5XpyC6ZneTeF2ExtjhwZVjXDwMbIzVYIbw1y2PPGw8dC7pc4sIVSnI4oQAH0v650ghfhmMevWW4h4Pylbw/DG4fdeT3+8z0f8MOSTtfipx05/QttHlAvODRxa5yDCvOz6n3f1BO0N/4MbGyOM27qW4x8mBVoryUZWfsmhfi0/GfNvB8+ylTGbYNX27OcEF5WZtNqbWEy7V5GbmZUSxspcg7FBZ3j3/3L7muvZXoZi1f5erq4y3lSH+oF7qPFnvieyLOCwubcQy+oyowj4cD0idRecnp+s+52D7+mzF1gpW+f+cJasoxigbxkOloS4yeiKvgM4nWSpGEj12RPx5o80EmziXN9t4zJzW1htA5hXJJeFympMYtyCUCevRWVmrRYktQ7UnCa6tL/AVPi1ni5XCXHfLOuqfK5KLYxjwVtdhhfmCLvpaKVNCEOCf4nNGHqD98wUb0ejWNwDYBnJGkBRw9RVspfwNdoWoVcn2qv8ywGUp7yc/sk2LpI6VziRyvWqbGNG1ZJqVVutRc46+sOE4VSeS2/CTNKNa6NMCf/hxxaUVsYfKdP+S1Lc4ianKp0sUugPpmtoydC0CP2RwjnHLcnzxCnvqxlLQWXMNPTsu2yBcHlWJLtcmdg/McLagy7l5kG6sN7ZxJLD/kAx8E6uqohP+K/jMSrYuQJnhr4tf4biWFY3Jzf/KpAYyVSq+pbfFKXMbL9nHEoZQT15Wyabd6DMAq3QZOthM5pUzdNqUXkcrI1PwjASAg4dVU/2o+98mTXnyhP7VlEkTnddpwyJF+YCCc01pqa6ZX99pnR+YOpIrb5DtPcKq0t5cFCDyQxN1lUuqZcYujvX/GC7+6IpRTz/NkEY/5cntOfZJyd6SkUx/JR5AJIRPCNeib0rcWnnFn0XK8s22x40wLjEtufXDO9+yraf4kHxlBI3SB0meMjepX50CJhAx7NekkRFnwoecLfqIyrjXrgAZsTMPiX94snjdrzisAXtBKanulilSxOfW2HSmfnk0L2GXHJJ3S+vCbgREml9dekfmHhmwEYtv3kfhth7ERIwVT8j69OtIBCmeTIJuA7o73PXc1LJ2whuZK7FZe6NTOs61MP7JVlIc6tbhtOBk3NXU8pBPOCY6Q55d63FtpUU/KCbQ0K2WQ9o5jNGwNNJ6Cw5FDCt+x5YJeP1d2QzDSiq+cSZqwqCZVcNoAftGGzgrRZ4vkpbP0xQZHCtWsUwHVTjrY4NfRDjHdVPRn8J9Xtooc/hIWkqzFaczYTlapu2RCJqe8aEo3jzWZCEJ7AiBzBYraYDgOfeiQyeJUsbbXHdGKiiw22Bdp4M+Yz671MZ0ie9EWRks0Q2DHGUTne9S4tAAJPGbUf4xtCMoJ40Q4QW05QLSZy3wNFAUFYmXE4ECDFB8+IEeNFFF/tgAnfh8GvFh/1xY+LE+ozMminjSRwhq09zQAEkNFyu0IKgvDkyOI+iccF7doWLTUasPxLH1xGPBSKkeWCnSoX/4C2c9du66BbRXamHqae+VCiS6BN953iYKOu8rMkneW+TFjV0CzAf8PNG9LBayUgY5Uw9x0Sap6zI4PpaZKH4d9ktkFU+91yBbN5SRtdd8/EQatJXmBNfw7kRrll+LjwZweWLEb0jT1RpGuDqHLdLTMMgowSczrp2zCsJdQH/seaYrpwkCFR8dRjM7JRUkVa1A0SvdZAX75vuBvvLBzN2c+Rbt5oqy93lqIBIjKb7w2UkRTb4pBWcNKNPUgt3az/PxQxEasaltdmuyvsXYbur3VPNn8JzN/6J5N+QCnz571TccapCmkPvFE+gayXU+UF1VKYyFcUvlev7lsQlmjqzK2QA8X7Q3TzFapqhvJKlt7dzBHraqZBgPgb9N4oKNayi8FZT71+kaYhIhFrESKYRHfHkj5n3kfhgLy+x+jLDbKxMggkgHnJz9RuEHZXBWtkYAQ==', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_verifier', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAMRdS21FeJfzQhM3s', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7396, 'output_tokens': 770, 'output_tokens_details': {'thinking_tokens': 739}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='skeptic', id='lc_run--01a0fe03-4ae9-76c0-89a6-4f0b114e3030-0', tool_calls=[{'name': 'transfer_to_verifier', 'args': {}, 'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7396, 'output_tokens': 770, 'total_tokens': 8166, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 739}}), ToolMessage(content='Successfully transferred to verifier', name='transfer_to_verifier', tool_call_id='toolu_01H6fV8JvRJNvXy47eRnd3jH')], 'active_agent': 'verifier'}, goto='verifier'))Traceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 2967, in stream\n for _ in runner.tick(\n ~~~~~~~~~~~^\n [t for t in loop.tasks.values() if not t.writes],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<2 lines>...\n schedule_task=loop.accept_push,\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ):\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_runner.py\", line 207, in tick\n run_with_retry(\n ~~~~~~~~~~~~~~^\n t,\n ^^\n ...<10 lines>...\n },\n ^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_retry.py\", line 617, in run_with_retry\n return task.proc.invoke(task.input, config)\n ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 709, in invoke\n input = step.invoke(input, config)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 447, in invoke\n ret = self.func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 84, in _write\n self.do_write(\n ~~~~~~~~~~~~~^\n config,\n ^^^^^^^\n writes,\n ^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 126, in do_write\n write(_assemble_writes(writes))\n ~~~~~~~~~~~~~~~~^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 181, in _assemble_writes\n if ww := w.mapper(w.value):\n ~~~~~~~~^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/graph/state.py\", line 1762, in _control_branch\n raise ParentCommand(command)\n\n\nlanggraph.errors.ParentCommand: Command(graph='skeptic:3dfc4328-0d19-e1ea-bcda-3d3d36d7cda8', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', id='389058d1-083f-4632-bbc5-09bfd3a0ffd2', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9'), AIMessage(content=[{'signature': 'CAQS1QgKEAgSGAI4AUIIdGhpbmtpbmcSDN6TfU6NDM971lEbzxoMvRUA1J9ox3fJQYT9IjArCQQGHBP4pRHPavXzRlbMbaYHC57Yj+Gbxgr0helvd2OIL/o6Rbt4PFA3FaIFUlgq8gc7KQI9FtGnvSAkLBRzIsUp/2d5Rd2zgEsfkFVeXsn8WYYZkinAQVVy6A7B3LaEJW0xb/x88Di67fedwx0Q+FlZtwcmT3Dcaa11JveyHRr3dm+zGSZP6CXeXNJ8nrTZ7W1YRTysD6w7o7Lp8h1Rwq4f4o8j+QUfY0vje+zJZsxHPmQNt3ykeKh99hgn6dbiz6/EHqmbHjSAu1M4SqEDfqIKThHYenamAZmcQ4XAOo6mdQ6+77VgpV1lC1/k52IiUoJjkGlQrnLCvutiI7KN+sgK8c2+2KCJrPRCvVV+BkStvP5jWWqzytpKQ+nHzhL448OrIA9gdJC39C8eDrXeRnI7t4FqlkXGoMlZAV9UMcrQHRxOMi8tOyTzi9oyBFDTSsQ+LJqer0tz1Ft9gek6vzg50SpVAFQdLERv8R1YqMaeI+xsvTAEW9MRgvRMkzLuzRT2guw3jD+Fjip37xgK5QI0R+CsOAmQPoTZizzcIyAUAuCJFCgq/gD/CibLxoymiVUI1an9Snt3KpNRoWdvWIne33V0pv8PHg9yW2JuLOqQGilLCsK/Q3oQr3upEMXxXGjCL7WUizvRZGFEEV+VplYt7v4tKZMj5pk3vdeM0MJ5XZg7PvUsRzycxcnrT3Knoc3qM+ttqZfTsb8d6U/pQifq7GuahFOHrvroqgJLj+/81Yy1pZFkjfJgY0XklzAnA6CFq/YT0gxbV+TH5IS+WaYOUzK36S8s5qqMwhngv4IzE3ctXXqnhIZXNznyVBqbYa/Gx2bCjcEj2n5RyGYRS/+MculgLBznQ9MkG2D7QP3Mq/nwHXKMIaLS8VK8hG3d0rAjlvnauXShNrJxtkj4qkwnxD4LrEjk1ue+PLcbLqYWwAr8n0s+41jrsIPVyyK9vFaEZrA9KaBxkJyiqx50GZNn7ckx+mkI18gH52iyVPtU8FKMXjugCl6mPLiHYiDQd+G63LudmOKys9itVnBDBNMP2b/I7+bIVRlheGaZWr6MATKl86B3y+0kuOrLRMyWcdibc2iQkZm+8f7dTUwbKbBJsKOFY73/8nlL/JiYpjz6nUIk+IqSjaFyK6XFlCcukWpWHpDJnSvbEzey7qHyCqJ+I+on8iP8GOtempQaS0/4kZGSwlrE6xOj6EwGpfjbDZKHkP/2RAdNYPQq3z7bdVJMJNnFMz6e37mzMpCBihwPdSFTRE3SCY66wvTgJzGCUyhHbSi5i1azzOXd8uHCSpT/6L1o6VU0pjoR8Kaa4wCWHGf7P/WEd+avyp02Jnu1cmpW0+Gq+vK9h0tdb2IClM/eNfdouQP+ep8MJUjraDRICmAJQY5obS71vniOo+reaFy0zRgB', 'thinking': '', 'type': 'thinking'}, {'signature': 'CAQS/REKEQgSGAI4AUIJbmFycmF0aW9uEgwFh3AVJZP3s8y1Hq0aDG76uhHONaScXAGShiIwGgjLXHUx1yZrkLPljjzTOQycpLJiKVwXcxm/EzKJm1s8M5WxuREWEldXcoaN0/msKpkRMT+eLb5ih9rAHULTomUNsUwJeu0jrJe4GLazy7sP5KtjEOEBKQsXUfoQ7fDkZOQUHZi9zzQN4hFNmYJUf3OVqf9TQNdC1+m//HAZPfWmAy6HtDvm7kgBJkCEoiFnm7tn/PX1hXGPRgWc6BTXUMr2WA8aKyqggRUbMH2+JYv5iWZddCc1up1Nbr3sF7WPcot9ZBhyHQXr692zeJk6SJgygIVrASbMly2bBxTWJB623gVgzpCmVSLZFpkKXgPcp8hGyDKOvF6SiQSHqGRxQofr7Vs1aPhfZixROJGD+MlrkggVAnGMyYIDUZpavtCeiU1JmBSz0084bXPtT/tnpsKOnSPYZQo3B0BXKbqA/WsKng8/V3Z8Uuajo6IVZ2hgA1WDduqQOQkExGU6pFuLhebIjuWxSNI/DffvR//LNqCfvdnf0hvoPtDxfx/SawIx4/spq9xbwFzHtLgoHg1F2wFw7clPDlg1PyD/v2yJG3V9T8GL6jGPE6rhfil/u4HNvf7BW0dQzCTTXcqZy4nByy0+p+S7ZxfMl4Kpxa2gOhhSNjfpfuX0QpL4kME8J1nliFN1d8KqEPuDI18BQHUhD/UfY29oA0x510FjM/BsLZ4n5sppkoIi7CFoQq2aAKVNfC1KilhIpRD+1dyDtZQklN8gZpOESHw6ML1TtU0yfAcbmqoxJ6hhjzwH6CMsTQEEy8MbRBM+QKKkIRdRCPHGFW2LkuR50+J0pF/tr6pqVvJeozfiMnrEilB27TZP4nKTJG8rd//vCWen0y7CA/8dBJ9gdp5tx7NVA45k1gdcxER1gYouNX3n1vG3/B1F4+93wXW9Mqo6Us59TQmrb9Nqa7pSQNSrH06w1GxyBAlkFfqAlafk16aMpq416TJYpGy43WCUiCcLWlhiWgz2sgOLAXP9a5weWAKRC8J6jkcUkDeXnW+HFSiPM6+9Bo6kVhLUhN5rc4mHZOZpF7ptPpsQN4XCqLPsqpRH/8o5XpyC6ZneTeF2ExtjhwZVjXDwMbIzVYIbw1y2PPGw8dC7pc4sIVSnI4oQAH0v650ghfhmMevWW4h4Pylbw/DG4fdeT3+8z0f8MOSTtfipx05/QttHlAvODRxa5yDCvOz6n3f1BO0N/4MbGyOM27qW4x8mBVoryUZWfsmhfi0/GfNvB8+ylTGbYNX27OcEF5WZtNqbWEy7V5GbmZUSxspcg7FBZ3j3/3L7muvZXoZi1f5erq4y3lSH+oF7qPFnvieyLOCwubcQy+oyowj4cD0idRecnp+s+52D7+mzF1gpW+f+cJasoxigbxkOloS4yeiKvgM4nWSpGEj12RPx5o80EmziXN9t4zJzW1htA5hXJJeFympMYtyCUCevRWVmrRYktQ7UnCa6tL/AVPi1ni5XCXHfLOuqfK5KLYxjwVtdhhfmCLvpaKVNCEOCf4nNGHqD98wUb0ejWNwDYBnJGkBRw9RVspfwNdoWoVcn2qv8ywGUp7yc/sk2LpI6VziRyvWqbGNG1ZJqVVutRc46+sOE4VSeS2/CTNKNa6NMCf/hxxaUVsYfKdP+S1Lc4ianKp0sUugPpmtoydC0CP2RwjnHLcnzxCnvqxlLQWXMNPTsu2yBcHlWJLtcmdg/McLagy7l5kG6sN7ZxJLD/kAx8E6uqohP+K/jMSrYuQJnhr4tf4biWFY3Jzf/KpAYyVSq+pbfFKXMbL9nHEoZQT15Wyabd6DMAq3QZOthM5pUzdNqUXkcrI1PwjASAg4dVU/2o+98mTXnyhP7VlEkTnddpwyJF+YCCc01pqa6ZX99pnR+YOpIrb5DtPcKq0t5cFCDyQxN1lUuqZcYujvX/GC7+6IpRTz/NkEY/5cntOfZJyd6SkUx/JR5AJIRPCNeib0rcWnnFn0XK8s22x40wLjEtufXDO9+yraf4kHxlBI3SB0meMjepX50CJhAx7NekkRFnwoecLfqIyrjXrgAZsTMPiX94snjdrzisAXtBKanulilSxOfW2HSmfnk0L2GXHJJ3S+vCbgREml9dekfmHhmwEYtv3kfhth7ERIwVT8j69OtIBCmeTIJuA7o73PXc1LJ2whuZK7FZe6NTOs61MP7JVlIc6tbhtOBk3NXU8pBPOCY6Q55d63FtpUU/KCbQ0K2WQ9o5jNGwNNJ6Cw5FDCt+x5YJeP1d2QzDSiq+cSZqwqCZVcNoAftGGzgrRZ4vkpbP0xQZHCtWsUwHVTjrY4NfRDjHdVPRn8J9Xtooc/hIWkqzFaczYTlapu2RCJqe8aEo3jzWZCEJ7AiBzBYraYDgOfeiQyeJUsbbXHdGKiiw22Bdp4M+Yz671MZ0ie9EWRks0Q2DHGUTne9S4tAAJPGbUf4xtCMoJ40Q4QW05QLSZy3wNFAUFYmXE4ECDFB8+IEeNFFF/tgAnfh8GvFh/1xY+LE+ozMminjSRwhq09zQAEkNFyu0IKgvDkyOI+iccF7doWLTUasPxLH1xGPBSKkeWCnSoX/4C2c9du66BbRXamHqae+VCiS6BN953iYKOu8rMkneW+TFjV0CzAf8PNG9LBayUgY5Uw9x0Sap6zI4PpaZKH4d9ktkFU+91yBbN5SRtdd8/EQatJXmBNfw7kRrll+LjwZweWLEb0jT1RpGuDqHLdLTMMgowSczrp2zCsJdQH/seaYrpwkCFR8dRjM7JRUkVa1A0SvdZAX75vuBvvLBzN2c+Rbt5oqy93lqIBIjKb7w2UkRTb4pBWcNKNPUgt3az/PxQxEasaltdmuyvsXYbur3VPNn8JzN/6J5N+QCnz571TccapCmkPvFE+gayXU+UF1VKYyFcUvlev7lsQlmjqzK2QA8X7Q3TzFapqhvJKlt7dzBHraqZBgPgb9N4oKNayi8FZT71+kaYhIhFrESKYRHfHkj5n3kfhgLy+x+jLDbKxMggkgHnJz9RuEHZXBWtkYAQ==', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_verifier', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAMRdS21FeJfzQhM3s', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7396, 'output_tokens': 770, 'output_tokens_details': {'thinking_tokens': 739}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='skeptic', id='lc_run--01a0fe03-4ae9-76c0-89a6-4f0b114e3030-0', tool_calls=[{'name': 'transfer_to_verifier', 'args': {}, 'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7396, 'output_tokens': 770, 'total_tokens': 8166, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 739}}), ToolMessage(content='Successfully transferred to verifier', name='transfer_to_verifier', tool_call_id='toolu_01H6fV8JvRJNvXy47eRnd3jH')], 'active_agent': 'verifier'}, goto='verifier')\n" + } + }, + { + "key": "exception.escaped", + "value": { + "stringValue": "False" + } + } + ] + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "0ff4846461350af9", + "parentSpanId": "04ffc0db9ce0f358", + "name": "skeptic", + "kind": 1, + "startTimeUnixNano": "1790968023779922944", + "endTimeUnixNano": "1790968030845179136", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "e30=" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "skeptic" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langgraph" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "2" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "skeptic" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:skeptic\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"skeptic\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "skeptic:3dfc4328-0d19-e1ea-bcda-3d3d36d7cda8" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "graph:step:2" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W3siY29udGVudCI6IkZvciBhIHN5bnRoZXRpYyB0cmFjaW5nIGZpeHR1cmUsIGV4cGxhaW4gUHl0aG9uIHR1cGxlIHZlcnN1cyBsaXN0IGltbXV0YWJpbGl0eSBpbiBhdCBtb3N0IDEyMCB3b3Jkcy4gVXNlIG9ubHkgb2ZmaWNpYWwgUHl0aG9uIGRvY3VtZW50YXRpb24gaWYgeW91IHNlYXJjaC4gSGF2ZSB0aGUgc2tlcHRpYyBhbmQgdmVyaWZpZXIgY2hlY2sgaXQsIHRoZW4gaGF2ZSB0aGUgZWRpdG9yIHdyaXRlIC9hbnN3ZXIubWQuIEtlZXAgdGhlIHJ1biBzaG9ydCBhbmQgZG8gbm90IGFzayBmb2xsb3ctdXAgcXVlc3Rpb25zLiIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoiaHVtYW4iLCJpZCI6IjQ2MjA0MzA1LWY4ZWUtNDM4NC05Y2U4LWZjZDgwYmZjZGY3NSJ9LHsiY29udGVudCI6W3siaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9LCJuYW1lIjoid2ViX3NlYXJjaCIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHc5YVc5YnJlblBxZ3liZk53UCIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjoyODQ1LCJvdXRwdXRfdG9rZW5zIjo3Mywib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6MH0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoicmVzZWFyY2hlciIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy0yMWEzLTcwMDAtYTM4MS0yODRhNmEwYzAzZjctMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ3ZWJfc2VhcmNoIiwiYXJncyI6eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9LCJpZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6Mjg0NSwib3V0cHV0X3Rva2VucyI6NzMsInRvdGFsX3Rva2VucyI6MjkxOCwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fSx7ImNvbnRlbnQiOiJbe1widGl0bGVcIjogXCI1LiBEYXRhIFN0cnVjdHVyZXMgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3R1dG9yaWFsL2RhdGFzdHJ1Y3R1cmVzLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcIllvdSBtaWdodCBoYXZlIG5vdGljZWQgdGhhdCBtZXRob2RzIGxpa2VgaW5zZXJ0YCxgcmVtb3ZlYCBvcmBzb3J0YCB0aGF0IG9ubHkgbW9kaWZ5IHRoZSBsaXN0IGhhdmUgbm8gcmV0dXJuIHZhbHVlIHByaW50ZWQgXFx1MjAxMyB0aGV5IHJldHVybiB0aGUgZGVmYXVsdGBOb25lYC4gWzFdIFRoaXMgaXMgYSBkZXNpZ24gcHJpbmNpcGxlIGZvciBhbGwgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgaW4gUHl0aG9uLlxcbi4uLlxcbiMjIDUuMy4gVHVwbGVzIGFuZCBTZXF1ZW5jZXNcXHUwMGI2XFxuXFxuV2Ugc2F3IHRoYXQgbGlzdHMgYW5kIHN0cmluZ3MgaGF2ZSBtYW55IGNvbW1vbiBwcm9wZXJ0aWVzLCBzdWNoIGFzIGluZGV4aW5nIGFuZCBzbGljaW5nIG9wZXJhdGlvbnMuIFRoZXkgYXJlIHR3byBleGFtcGxlcyBvZiBzZXF1ZW5jZSBkYXRhIHR5cGVzIChzZWUgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBsaXN0LCB0dXBsZSwgcmFuZ2UpLiBTaW5jZSBQeXRob24gaXMgYW4gZXZvbHZpbmcgbGFuZ3VhZ2UsIG90aGVyIHNlcXVlbmNlIGRhdGEgdHlwZXMgbWF5IGJlIGFkZGVkLiBUaGVyZSBpcyBhbHNvIGFub3RoZXIgc3RhbmRhcmQgc2VxdWVuY2UgZGF0YSB0eXBlOiB0aGUgdHVwbGUuXFxuXFxuQSB0dXBsZSBjb25zaXN0cyBvZiBhIG51bWJlciBvZiB2YWx1ZXMgc2VwYXJhdGVkIGJ5IGNvbW1hcywgZm9yIGluc3RhbmNlOlxcbi4uLlxcbj4+PiAjIFR1cGxlcyBhcmUgaW1tdXRhYmxlOlxcbj4+PiB0WzBdID0gODg4ODhcXG5UcmFjZWJhY2sgKG1vc3QgcmVjZW50IGNhbGwgbGFzdCk6XFxuICBGaWxlIFxcXCI8c3RkaW4+XFxcIiwgbGluZSAxLCBpbiA8bW9kdWxlPlxcblR5cGVFcnJvcjogJ3R1cGxlJyBvYmplY3QgZG9lcyBub3Qgc3VwcG9ydCBpdGVtIGFzc2lnbm1lbnRcXG4+Pj4gIyBidXQgdGhleSBjYW4gY29udGFpbiBtdXRhYmxlIG9iamVjdHM6XFxuPj4+IHYgPSAoWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuPj4+IHZcXG4oWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuLi4uXFxuQXMgeW91IHNlZSwgb24gb3V0cHV0IHR1cGxlcyBhcmUgYWx3YXlzIGVuY2xvc2VkIGluIHBhcmVudGhlc2VzLCBzbyB0aGF0IG5lc3RlZCB0dXBsZXMgYXJlIGludGVycHJldGVkIGNvcnJlY3RseTsgdGhleSBtYXkgYmUgaW5wdXQgd2l0aCBvciB3aXRob3V0IHN1cnJvdW5kaW5nIHBhcmVudGhlc2VzLCBhbHRob3VnaCBvZnRlbiBwYXJlbnRoZXNlcyBhcmUgbmVjZXNzYXJ5IGFueXdheSAoaWYgdGhlIHR1cGxlIGlzIHBhcnQgb2YgYSBsYXJnZXIgZXhwcmVzc2lvbikuIEl0IGlzIG5vdCBwb3NzaWJsZSB0byBhc3NpZ24gdG8gdGhlIGluZGl2aWR1YWwgaXRlbXMgb2YgYSB0dXBsZSwgaG93ZXZlciBpdCBpcyBwb3NzaWJsZSB0byBjcmVhdGUgdHVwbGVzIHdoaWNoIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzLCBzdWNoIGFzIGxpc3RzLlxcblxcblRob3VnaCB0dXBsZXMgbWF5IHNlZW0gc2ltaWxhciB0byBsaXN0cywgdGhleSBhcmUgb2Z0ZW4gdXNlZCBpbiBkaWZmZXJlbnQgc2l0dWF0aW9ucyBhbmQgZm9yIGRpZmZlcmVudCBwdXJwb3Nlcy4gVHVwbGVzIGFyZSBpbW11dGFibGUsIGFuZCB1c3VhbGx5IGNvbnRhaW4gYSBoZXRlcm9nZW5lb3VzIHNlcXVlbmNlIG9mIGVsZW1lbnRzIHRoYXQgYXJlIGFjY2Vzc2VkIHZpYSB1bnBhY2tpbmcgKHNlZSBsYXRlciBpbiB0aGlzIHNlY3Rpb24pIG9yIGluZGV4aW5nIChvciBldmVuIGJ5IGF0dHJpYnV0ZSBpbiB0aGUgY2FzZSBvZiBuYW1lZHR1cGxlcykuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG5Bbm90aGVyIHVzZWZ1bCBkYXRhIHR5cGUgYnVpbHQgaW50byBQeXRob24gaXMgdGhlIGRpY3Rpb25hcnkgKHNlZSBNYXBwaW5nIFR5cGVzIFxcdTIwMTQgZGljdCkuIERpY3Rpb25hcmllcyBhcmUgc29tZXRpbWVzIGZvdW5kIGluIG90aGVyIGxhbmd1YWdlcyBhcyBcXHUyMDFjYXNzb2NpYXRpdmUgbWVtb3JpZXNcXHUyMDFkIG9yIFxcdTIwMWNhc3NvY2lhdGl2ZSBhcnJheXNcXHUyMDFkLiBVbmxpa2Ugc2VxdWVuY2VzLCB3aGljaCBhcmUgaW5kZXhlZCBieSBhIHJhbmdlIG9mIG51bWJlcnMsIGRpY3Rpb25hcmllcyBhcmUgaW5kZXhlZCBieSBrZXlzLCB3aGljaCBjYW4gYmUgYW55IGltbXV0YWJsZSB0eXBlOyBzdHJpbmdzIGFuZCBudW1iZXJzIGNhbiBhbHdheXMgYmUga2V5cy4gVHVwbGVzIGNhbiBiZSB1c2VkIGFzIGtleXMgaWYgdGhleSBjb250YWluIG9ubHkgc3RyaW5ncywgbnVtYmVycywgb3IgdHVwbGVzOyBpZiBhIHR1cGxlIGNvbnRhaW5zIGFueSBtdXRhYmxlIG9iamVjdCBlaXRoZXIgZGlyZWN0bHkgb3IgaW5kaXJlY3RseSwgaXQgY2Fubm90IGJlIHVzZWQgYXMgYSBrZXkuIFlvdSBjYW5cXHUyMDE5dCB1c2UgbGlzdHMgYXMga2V5cywgc2luY2UgbGlzdHMgY2FuIGJlIG1vZGlmaWVkIGluIHBsYWNlIHVzaW5nIGluZGV4IGFzc2lnbm1lbnRzLCBzbGljZSBhc3NpZ25tZW50cywgb3IgbWV0aG9kcyBsaWtlIGFwcGVuZCgpIGFuZCBleHRlbmQoKS5cIl19LCB7XCJ0aXRsZVwiOiBcIjMuIERhdGEgbW9kZWwgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3JlZmVyZW5jZS9kYXRhbW9kZWwuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiVGhlIHZhbHVlIG9mIHNvbWUgb2JqZWN0cyBjYW4gY2hhbmdlLiBPYmplY3RzIHdob3NlIHZhbHVlIGNhbiBjaGFuZ2UgYXJlIHNhaWQgdG8gYmUgbXV0YWJsZTsgb2JqZWN0cyB3aG9zZSB2YWx1ZSBpcyB1bmNoYW5nZWFibGUgb25jZSB0aGV5IGFyZSBjcmVhdGVkIGFyZSBjYWxsZWQgaW1tdXRhYmxlLiAoVGhlIHZhbHVlIG9mIGFuIGltbXV0YWJsZSBjb250YWluZXIgb2JqZWN0IHRoYXQgY29udGFpbnMgYSByZWZlcmVuY2UgdG8gYSBtdXRhYmxlIG9iamVjdCBjYW4gY2hhbmdlIHdoZW4gdGhlIGxhdHRlclxcdTIwMTlzIHZhbHVlIGlzIGNoYW5nZWQ7IGhvd2V2ZXIgdGhlIGNvbnRhaW5lciBpcyBzdGlsbCBjb25zaWRlcmVkIGltbXV0YWJsZSwgYmVjYXVzZSB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGl0IGNvbnRhaW5zIGNhbm5vdCBiZSBjaGFuZ2VkLiBTbywgaW1tdXRhYmlsaXR5IGlzIG5vdCBzdHJpY3RseSB0aGUgc2FtZSBhcyBoYXZpbmcgYW4gdW5jaGFuZ2VhYmxlIHZhbHVlLCBpdCBpcyBtb3JlIHN1YnRsZS4pIEFuIG9iamVjdFxcdTIwMTlzIG11dGFiaWxpdHkgaXMgZGV0ZXJtaW5lZCBieSBpdHMgdHlwZTsgZm9yIGluc3RhbmNlLCBudW1iZXJzLCBzdHJpbmdzIGFuZCB0dXBsZXMgYXJlIGltbXV0YWJsZSwgd2hpbGUgZGljdGlvbmFyaWVzIGFuZCBsaXN0cyBhcmUgbXV0YWJsZS5cXG4uLi5cXG5Tb21lIG9iamVjdHMgY29udGFpbiByZWZlcmVuY2VzIHRvIG90aGVyIG9iamVjdHM7IHRoZXNlIGFyZSBjYWxsZWQgY29udGFpbmVycy4gRXhhbXBsZXMgb2YgY29udGFpbmVycyBhcmUgdHVwbGVzLCBsaXN0cyBhbmQgZGljdGlvbmFyaWVzLiBUaGUgcmVmZXJlbmNlcyBhcmUgcGFydCBvZiBhIGNvbnRhaW5lclxcdTIwMTlzIHZhbHVlLiBJbiBtb3N0IGNhc2VzLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIHZhbHVlIG9mIGEgY29udGFpbmVyLCB3ZSBpbXBseSB0aGUgdmFsdWVzLCBub3QgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGNvbnRhaW5lZCBvYmplY3RzOyBob3dldmVyLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIG11dGFiaWxpdHkgb2YgYSBjb250YWluZXIsIG9ubHkgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGltbWVkaWF0ZWx5IGNvbnRhaW5lZCBvYmplY3RzIGFyZSBpbXBsaWVkLiBTbywgaWYgYW4gaW1tdXRhYmxlIGNvbnRhaW5lciAobGlrZSBhIHR1cGxlKSBjb250YWlucyBhIHJlZmVyZW5jZSB0byBhIG11dGFibGUgb2JqZWN0LCBpdHMgdmFsdWUgY2hhbmdlcyBpZiB0aGF0IG11dGFibGUgb2JqZWN0IGlzIGNoYW5nZWQuXFxuLi4uXFxuIyMjIDMuMi41LiBTZXF1ZW5jZXNcXHUwMGI2XFxuLi4uXFxuIyMjIyAzLjIuNS4xLiBJbW11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbkFuIG9iamVjdCBvZiBhbiBpbW11dGFibGUgc2VxdWVuY2UgdHlwZSBjYW5ub3QgY2hhbmdlIG9uY2UgaXQgaXMgY3JlYXRlZC4gKElmIHRoZSBvYmplY3QgY29udGFpbnMgcmVmZXJlbmNlcyB0byBvdGhlciBvYmplY3RzLCB0aGVzZSBvdGhlciBvYmplY3RzIG1heSBiZSBtdXRhYmxlIGFuZCBtYXkgYmUgY2hhbmdlZDsgaG93ZXZlciwgdGhlIGNvbGxlY3Rpb24gb2Ygb2JqZWN0cyBkaXJlY3RseSByZWZlcmVuY2VkIGJ5IGFuIGltbXV0YWJsZSBvYmplY3QgY2Fubm90IGNoYW5nZS4pXFxuXFxuVGhlIGZvbGxvd2luZyB0eXBlcyBhcmUgaW1tdXRhYmxlIHNlcXVlbmNlczpcXG4uLi5cXG5UdXBsZXNcXG46IFRoZSBpdGVtcyBvZiBhIGB0dXBsZWAgYXJlIGFyYml0cmFyeSBQeXRob24gb2JqZWN0cy4gVHVwbGVzIG9mIHR3byBvciBtb3JlIGl0ZW1zIGFyZSBmb3JtZWQgYnkgY29tbWEtc2VwYXJhdGVkIGxpc3RzIG9mIGV4cHJlc3Npb25zLiBBIHR1cGxlIG9mIG9uZSBpdGVtIChhIFxcdTIwMThzaW5nbGV0b25cXHUyMDE5KSBjYW4gYmUgZm9ybWVkIGJ5IGFmZml4aW5nIGEgY29tbWEgdG8gYW4gZXhwcmVzc2lvbiAoYW4gZXhwcmVzc2lvbiBieSBpdHNlbGYgZG9lcyBub3QgY3JlYXRlIGEgdHVwbGUsIHNpbmNlIHBhcmVudGhlc2VzIG11c3QgYmUgdXNhYmxlIGZvciBncm91cGluZyBvZiBleHByZXNzaW9ucykuIEFuIGVtcHR5IHR1cGxlIGNhbiBiZSBmb3JtZWQgYnkgYW4gZW1wdHkgcGFpciBvZiBwYXJlbnRoZXNlcy5cXG4uLi5cXG4jIyMjIDMuMi41LjIuIE11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbk11dGFibGUgc2VxdWVuY2VzIGNhbiBiZSBjaGFuZ2VkIGFmdGVyIHRoZXkgYXJlIGNyZWF0ZWQuIFRoZSBzdWJzY3JpcHRpb24gYW5kIHNsaWNpbmcgbm90YXRpb25zIGNhbiBiZSB1c2VkIGFzIHRoZSB0YXJnZXQgb2YgYXNzaWdubWVudCBhbmQgYGRlbGAgKGRlbGV0ZSkgc3RhdGVtZW50cy5cXG4uLi5cXG5UaGVyZSBhcmUgY3VycmVudGx5IHR3byBpbnRyaW5zaWMgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlczpcXG5cXG5MaXN0c1xcbjogVGhlIGl0ZW1zIG9mIGEgbGlzdCBhcmUgYXJiaXRyYXJ5IFB5dGhvbiBvYmplY3RzLiBMaXN0cyBhcmUgZm9ybWVkIGJ5IHBsYWNpbmcgYSBjb21tYS1zZXBhcmF0ZWQgbGlzdCBvZiBleHByZXNzaW9ucyBpbiBzcXVhcmUgYnJhY2tldHMuIChOb3RlIHRoYXQgdGhlcmUgYXJlIG5vIHNwZWNpYWwgY2FzZXMgbmVlZGVkIHRvIGZvcm0gbGlzdHMgb2YgbGVuZ3RoIDAgb3IgMS4pXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjcgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvYnVpbHRpbnMvc3RkdHlwZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiY29sbGVjdGlvbiBjbGFzc2VzIGFyZSBtdXRhYmxlLiBUaGUgbWV0aG9kcyB0aGF0IGFkZCwgc3VidHJhY3QsIG9yIHJlYXJyYW5nZSB0aGVpciAuLi4gaW4gcGxhY2UsIGFuZCBkb25cXHUyMDE5dCByZXR1cm4gYSAuLi4gLCBuZXZlciByZXR1cm4gdGhlIGNvbGxlY3Rpb24gaW5zdGFuY2UgaXRzZWxmIGJ1dCBgTm9uZWAuXFxuLi4uXFxuIyMgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBgbGlzdGAsIGB0dXBsZWAsIGByYW5nZWBcXHUwMGI2XFxuXFxuVGhlcmUgYXJlIHRocmVlIGJhc2ljIHNlcXVlbmNlIHR5cGVzOiBsaXN0cywgdHVwbGVzLCBhbmQgcmFuZ2Ugb2JqZWN0cy4gQWRkaXRpb25hbCBzZXF1ZW5jZSB0eXBlcyB0YWlsb3JlZCBmb3IgcHJvY2Vzc2luZyBvZiBiaW5hcnkgZGF0YSBhbmQgdGV4dCBzdHJpbmdzIGFyZSBkZXNjcmliZWQgaW4gZGVkaWNhdGVkIHNlY3Rpb25zLlxcbi4uLlxcblRoZSBvcGVyYXRpb25zIGluIHRoZSBmb2xsb3dpbmcgdGFibGUgLi4uIG11dGFibGUgYW5kIGltbXV0YWJsZS4gVGhlIGBjb2xsZWN0aW9ucy4gLi4uIGlzIHByb3ZpZGVkIHRvIG1ha2UgLi4uIGVhc2llciB0byBjb3JyZWN0bHkgaW1wbGVtZW50IHRoZXNlIG9wZXJhdGlvbnMgb24gY3VzdG9tIHNlcXVlbmNlIHR5cGVzXFxuLi4uXFxuIyMjIEltbXV0YWJsZSBTZXF1ZW5jZSBUeXBlc1xcdTAwYjZcXG4uLi5cXG5tdXRhYmxlIHNlcXVlbmNlIHR5cGVzIGlzXFxuLi4uXFxuc3VwcG9ydCBhbGxvd3MgaW1tdXRhYmxlIHNlcXVlbmNlcywgLi4uICwgdG8gYmUgdXNlZCBhcyBgZGljdGAga2V5cyBhbmQgc3RvcmVkIGluIC4uLiBlbnNldGAgaW5zdGFuY2VzXFxuLi4uXFxuIyMjIE11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxuIyMjIExpc3RzXFx1MDBiNlxcblxcbkxpc3RzIGFyZSBtdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaG9tb2dlbmVvdXMgaXRlbXMgKHdoZXJlIHRoZSBwcmVjaXNlIGRlZ3JlZSBvZiBzaW1pbGFyaXR5IHdpbGwgdmFyeSBieSBhcHBsaWNhdGlvbikuXFxuLi4uXFxuIyMjIFR1cGxlc1xcdTAwYjZcXG5cXG5UdXBsZXMgYXJlIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhldGVyb2dlbmVvdXMgZGF0YSAoc3VjaCBhcyB0aGUgMi10dXBsZXMgcHJvZHVjZWQgYnkgdGhlIGBlbnVtZXJhdGUoKWAgYnVpbHQtaW4pLiBUdXBsZXMgYXJlIGFsc28gdXNlZCBmb3IgY2FzZXMgd2hlcmUgYW4gaW1tdXRhYmxlIHNlcXVlbmNlIG9mIGhvbW9nZW5lb3VzIGRhdGEgaXMgbmVlZGVkIChzdWNoIGFzIGFsbG93aW5nIHN0b3JhZ2UgaW4gYSBgc2V0YCBvciBgZGljdGAgaW5zdGFuY2UpLlxcbi4uLlxcbnR1cGxlKGl0ZXJhYmxlPVxcbi4uLlxcblRoZSBjb25zdHJ1Y3RvciBidWlsZHMgYSB0dXBsZSB3aG9zZSBpdGVtcyBhcmUgdGhlIHNhbWUgYW5kIGluIHRoZSBzYW1lIG9yZGVyIGFzIGl0ZXJhYmxlXFx1MjAxOXMgaXRlbXMuIGl0ZXJhYmxlIG1heSBiZSBlaXRoZXIgYSBzZXF1ZW5jZSwgYSBjb250YWluZXIgdGhhdCBzdXBwb3J0cyBpdGVyYXRpb24sIG9yIGFuIGl0ZXJhdG9yIG9iamVjdC4gSWYgaXRlcmFibGUgaXMgYWxyZWFkeSBhIHR1cGxlLCBpdCBpcyByZXR1cm5lZCB1bmNoYW5nZWQuIEZvciBleGFtcGxlLCBgdHVwbGUoJ2FiYycpYCByZXR1cm5zIGAoJ2EnLCAnYicsICdjJylgIGFuZCBgdHVwbGUoIFsxLCAyLCAzXSApYCByZXR1cm5zIGAoMSwgMiwgMylgLiBJZiBubyBhcmd1bWVudCBpcyBnaXZlbiwgdGhlIGNvbnN0cnVjdG9yIGNyZWF0ZXMgYSBuZXcgZW1wdHkgdHVwbGUsIGAoKWAuXFxuLi4uXFxuVHVwbGVzIGltcGxlbWVudCBhbGwgb2YgdGhlIGNvbW1vbiBzZXF1ZW5jZSBvcGVyYXRpb25zLlxcbi4uLlxcbkZvciBoZXRlcm9nZW5lb3VzIGNvbGxlY3Rpb25zIG9mIGRhdGEgd2hlcmUgYWNjZXNzIGJ5IG5hbWUgaXMgY2xlYXJlciB0aGFuIGFjY2VzcyBieSBpbmRleCwgYGNvbGxlY3Rpb25zLm5hbWVkdHVwbGUoKWAgbWF5IGJlIGEgbW9yZSBhcHByb3ByaWF0ZSBjaG9pY2UgdGhhbiBhIHNpbXBsZSB0dXBsZSBvYmplY3QuXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvbGlicmFyeS9zdGR0eXBlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJTb21lIGNvbGxlY3Rpb24gY2xhc3NlcyBhcmUgbXV0YWJsZS4gVGhlIG1ldGhvZHMgdGhhdCBhZGQsIHN1YnRyYWN0LCBvciByZWFycmFuZ2UgdGhlaXIgLi4uIGluIHBsYWNlLCBhbmQgZG9uXFx1MjAxOXQgcmV0dXJuIGEgLi4uICwgbmV2ZXIgcmV0dXJuIHRoZSBjb2xsZWN0aW9uIGluc3RhbmNlIGl0c2VsZiBidXQgYE5vbmVgLlxcbi4uLlxcbiMjIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgYGxpc3RgLCBgdHVwbGVgLCBgcmFuZ2VgXFx1MDBiNlxcblxcblRoZXJlIGFyZSB0aHJlZSBiYXNpYyBzZXF1ZW5jZSB0eXBlczogbGlzdHMsIHR1cGxlcywgYW5kIC4uLiBvYmplY3RzLiBBZGRpdGlvbmFsIHNlcXVlbmNlIHR5cGVzIHRhaWxvcmVkIGZvciBwcm9jZXNzaW5nIG9mIGJpbmFyeSBkYXRhIGFuZCB0ZXh0IHN0cmluZ3MgYXJlIGRlc2NyaWJlZCBpbiBkZWRpY2F0ZWQgc2VjdGlvbnMuXFxuLi4uXFxuVGhlIG9wZXJhdGlvbnMgaW4gdGhlIGZvbGxvd2luZyB0YWJsZSAuLi4gaW1tdXRhYmxlLiBUaGUgYCAuLi4gaXMgcHJvdmlkZWQgdG8gbWFrZSAuLi4gZWFzaWVyIHRvIGNvcnJlY3RseSBpbXBsZW1lbnQgdGhlc2Ugb3BlcmF0aW9ucyBvbiBjdXN0b20gc2VxdWVuY2UgdHlwZXNcXG4uLi5cXG4jIyMgSW1tdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbm9wZXJhdGlvbiB0aGF0IGltbXV0YWJsZSBzZXF1ZW5jZSAuLi4gYnkgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlcyBpcyAuLi4gYGhhc2goKWBcXG4uLi5cXG5UaGlzIHN1cHBvcnQgYWxsb3dzIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHN1Y2ggYXMgYHR1cGxlYCBpbnN0YW5jZXMsIHRvIGJlIHVzZWQgYXMgYGRpY3RgIGtleXMgYW5kIHN0b3JlZCBpbiBgc2V0YCBhbmQgYGZyb3plbnNldGAgaW5zdGFuY2VzLlxcbi4uLlxcbiMjIyBNdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbiMjIyBMaXN0c1xcdTAwYjZcXG5cXG5MaXN0cyBhcmUgbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhvbW9nZW5lb3VzIGl0ZW1zICh3aGVyZSB0aGUgcHJlY2lzZSBkZWdyZWUgb2Ygc2ltaWxhcml0eSB3aWxsIHZhcnkgYnkgYXBwbGljYXRpb24pLlxcbi4uLlxcbiMjIyBUdXBsZXNcXHUwMGI2XFxuXFxuVHVwbGVzIGFyZSBpbW11dGFibGUgc2VxdWVuY2VzLCB0eXBpY2FsbHkgdXNlZCB0byBzdG9yZSBjb2xsZWN0aW9ucyBvZiBoZXRlcm9nZW5lb3VzIGRhdGEgKHN1Y2ggYXMgdGhlIDItdHVwbGVzIHByb2R1Y2VkIGJ5IHRoZSBgZW51bWVyYXRlKClgIGJ1aWx0LWluKS4gVHVwbGVzIGFyZSBhbHNvIHVzZWQgZm9yIGNhc2VzIHdoZXJlIGFuIGltbXV0YWJsZSBzZXF1ZW5jZSBvZiBob21vZ2VuZW91cyBkYXRhIGlzIG5lZWRlZCAoc3VjaCBhcyBhbGxvd2luZyBzdG9yYWdlIGluIGEgYHNldGAgb3IgYGRpY3RgIGluc3RhbmNlKS5cXG4uLi5cXG5UaGUgY29uc3RydWN0b3IgYnVpbGRzIGEgdHVwbGUgd2hvc2UgaXRlbXMgYXJlIHRoZSBzYW1lIGFuZCBpbiB0aGUgc2FtZSBvcmRlciBhcyBpdGVyYWJsZVxcdTIwMTlzIGl0ZW1zLiBpdGVyYWJsZSBtYXkgYmUgZWl0aGVyIGEgc2VxdWVuY2UsIGEgY29udGFpbmVyIHRoYXQgc3VwcG9ydHMgaXRlcmF0aW9uLCBvciBhbiBpdGVyYXRvciBvYmplY3QuIElmIGl0ZXJhYmxlIGlzIGFscmVhZHkgYSB0dXBsZSwgaXQgaXMgcmV0dXJuZWQgdW5jaGFuZ2VkLiBGb3IgZXhhbXBsZSwgYHR1cGxlKCdhYmMnKWAgcmV0dXJucyBgKCdhJywgJ2InLCAnYycpYCBhbmQgYHR1cGxlKCBbMSwgMiwgM10gKWAgcmV0dXJucyBgKDEsIDIsIDMpYC4gSWYgbm8gYXJndW1lbnQgaXMgZ2l2ZW4sIHRoZSBjb25zdHJ1Y3RvciBjcmVhdGVzIGEgbmV3IGVtcHR5IHR1cGxlLCBgKClgLlxcbi4uLlxcblR1cGxlcyBpbXBsZW1lbnQgYWxsIG9mIHRoZSBjb21tb24gc2VxdWVuY2Ugb3BlcmF0aW9ucy5cXG4uLi5cXG5Gb3IgaGV0ZXJvZ2VuZW91cyBjb2xsZWN0aW9ucyBvZiBkYXRhIHdoZXJlIGFjY2VzcyBieSBuYW1lIGlzIGNsZWFyZXIgdGhhbiBhY2Nlc3MgYnkgaW5kZXgsIGBjb2xsZWN0aW9ucy5uYW1lZHR1cGxlKClgIG1heSBiZSBhIG1vcmUgYXBwcm9wcmlhdGUgY2hvaWNlIHRoYW4gYSBzaW1wbGUgdHVwbGUgb2JqZWN0LlwiXX0sIHtcInRpdGxlXCI6IFwiNS4gRGF0YSBTdHJ1Y3R1cmVzIFxcdTIwMTQgUHl0aG9uIDMuMTAuMjAgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMuMTAvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiWW91IG1pZ2h0IGhhdmUgbm90aWNlZCB0aGF0IG1ldGhvZHMgbGlrZSBgaW5zZXJ0YCwgYHJlbW92ZWAgb3IgYHNvcnRgIHRoYXQgb25seSBtb2RpZnkgdGhlIGxpc3QgaGF2ZSBubyByZXR1cm4gdmFsdWUgcHJpbnRlZCBcXHUyMDEzIHRoZXkgcmV0dXJuIHRoZSBkZWZhdWx0IGBOb25lYC4gMSBUaGlzIGlzIGEgZGVzaWduIHByaW5jaXBsZSBmb3IgYWxsIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIGluIFB5dGhvbi5cXG4uLi5cXG4jIyA1LjMuIFR1cGxlcyBhbmQgU2VxdWVuY2VzXFx1MDBiNlxcblxcbldlIHNhdyB0aGF0IGxpc3RzIGFuZCBzdHJpbmdzIGhhdmUgbWFueSBjb21tb24gcHJvcGVydGllcywgc3VjaCBhcyBpbmRleGluZyBhbmQgc2xpY2luZyBvcGVyYXRpb25zLiBUaGV5IGFyZSB0d28gZXhhbXBsZXMgb2Ygc2VxdWVuY2UgZGF0YSB0eXBlcyAoc2VlIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgbGlzdCwgdHVwbGUsIHJhbmdlKS4gU2luY2UgUHl0aG9uIGlzIGFuIGV2b2x2aW5nIGxhbmd1YWdlLCBvdGhlciBzZXF1ZW5jZSBkYXRhIHR5cGVzIG1heSBiZSBhZGRlZC4gVGhlcmUgaXMgYWxzbyBhbm90aGVyIHN0YW5kYXJkIHNlcXVlbmNlIGRhdGEgdHlwZTogdGhlIHR1cGxlLlxcblxcbkEgdHVwbGUgY29uc2lzdHMgb2YgYSBudW1iZXIgb2YgdmFsdWVzIHNlcGFyYXRlZCBieSBjb21tYXMsIGZvciBpbnN0YW5jZTpcXG4uLi5cXG4+Pj4gIyBUdXBsZXMgYXJlIGltbXV0YWJsZTpcXG4uLi4gdFswXSA9IDg4ODg4XFxuVHJhY2ViYWNrIChtb3N0IHJlY2VudCBjYWxsIGxhc3QpOlxcbiAgRmlsZSBcXFwiPHN0ZGluPlxcXCIsIGxpbmUgMSwgaW4gPG1vZHVsZT5cXG5UeXBlRXJyb3I6ICd0dXBsZScgb2JqZWN0IGRvZXMgbm90IHN1cHBvcnQgaXRlbSBhc3NpZ25tZW50XFxuPj4+ICMgYnV0IHRoZXkgY2FuIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzOlxcbi4uLiB2ID0gKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbj4+PiB2XFxuKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbi4uLlxcbkFzIHlvdSBzZWUsIG9uIG91dHB1dCB0dXBsZXMgYXJlIGFsd2F5cyBlbmNsb3NlZCBpbiBwYXJlbnRoZXNlcywgc28gdGhhdCBuZXN0ZWQgdHVwbGVzIGFyZSBpbnRlcnByZXRlZCBjb3JyZWN0bHk7IHRoZXkgbWF5IGJlIGlucHV0IHdpdGggb3Igd2l0aG91dCBzdXJyb3VuZGluZyBwYXJlbnRoZXNlcywgYWx0aG91Z2ggb2Z0ZW4gcGFyZW50aGVzZXMgYXJlIG5lY2Vzc2FyeSBhbnl3YXkgKGlmIHRoZSB0dXBsZSBpcyBwYXJ0IG9mIGEgbGFyZ2VyIGV4cHJlc3Npb24pLiBJdCBpcyBub3QgcG9zc2libGUgdG8gYXNzaWduIHRvIHRoZSBpbmRpdmlkdWFsIGl0ZW1zIG9mIGEgdHVwbGUsIGhvd2V2ZXIgaXQgaXMgcG9zc2libGUgdG8gY3JlYXRlIHR1cGxlcyB3aGljaCBjb250YWluIG11dGFibGUgb2JqZWN0cywgc3VjaCBhcyBsaXN0cy5cXG5cXG5UaG91Z2ggdHVwbGVzIG1heSBzZWVtIHNpbWlsYXIgdG8gbGlzdHMsIHRoZXkgYXJlIG9mdGVuIHVzZWQgaW4gZGlmZmVyZW50IHNpdHVhdGlvbnMgYW5kIGZvciBkaWZmZXJlbnQgcHVycG9zZXMuIFR1cGxlcyBhcmUgaW1tdXRhYmxlLCBhbmQgdXN1YWxseSBjb250YWluIGEgaGV0ZXJvZ2VuZW91cyBzZXF1ZW5jZSBvZiBlbGVtZW50cyB0aGF0IGFyZSBhY2Nlc3NlZCB2aWEgdW5wYWNraW5nIChzZWUgbGF0ZXIgaW4gdGhpcyBzZWN0aW9uKSBvciBpbmRleGluZyAob3IgZXZlbiBieSBhdHRyaWJ1dGUgaW4gdGhlIGNhc2Ugb2YgYG5hbWVkdHVwbGVzYCkuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG4jIyA1LjQuXFxuLi4uXFxuLiBTZXQgb2JqZWN0c1xcbi4uLlxcbiMjIDUuNS4gRGljdGlvbmFyaWVzXFx1MDBiNlxcblxcbkFub3RoZXIgdXNlZnVsIGRhdGEgdHlwZSBidWlsdCBpbnRvIFB5dGhvbiBpcyB0aGUgZGljdGlvbmFyeSAoc2VlIE1hcHBpbmcgVHlwZXMgXFx1MjAxNCBkaWN0KS4gRGljdGlvbmFyaWVzIGFyZSBzb21ldGltZXMgZm91bmQgaW4gb3RoZXIgbGFuZ3VhZ2VzIGFzIFxcdTIwMWNhc3NvY2lhdGl2ZSBtZW1vcmllc1xcdTIwMWQgb3IgXFx1MjAxY2Fzc29jaWF0aXZlIGFycmF5c1xcdTIwMWQuIFVubGlrZSBzZXF1ZW5jZXMsIHdoaWNoIGFyZSBpbmRleGVkIGJ5IGEgcmFuZ2Ugb2YgbnVtYmVycywgZGljdGlvbmFyaWVzIGFyZSBpbmRleGVkIGJ5IGtleXMsIHdoaWNoIGNhbiBiZSBhbnkgaW1tdXRhYmxlIHR5cGU7IHN0cmluZ3MgYW5kIG51bWJlcnMgY2FuIGFsd2F5cyBiZSBrZXlzLiBUdXBsZXMgY2FuIGJlIHVzZWQgYXMga2V5cyBpZiB0aGV5IGNvbnRhaW4gb25seSBzdHJpbmdzLCBudW1iZXJzLCBvciB0dXBsZXM7IGlmIGEgdHVwbGUgY29udGFpbnMgYW55IG11dGFibGUgb2JqZWN0IGVpdGhlciBkaXJlY3RseSBvciBpbmRpcmVjdGx5LCBpdCBjYW5ub3QgYmUgdXNlZCBhcyBhIGtleS4gWW91IGNhblxcdTIwMTl0IHVzZSBsaXN0cyBhcyBrZXlzLCBzaW5jZSBsaXN0cyBjYW4gYmUgbW9kaWZpZWQgaW4gcGxhY2UgdXNpbmcgaW5kZXggYXNzaWdubWVudHMsIHNsaWNlIGFzc2lnbm1lbnRzLCBvciBtZXRob2RzIGxpa2UgYGFwcGVuZCgpYCBhbmQgYGV4dGVuZCgpYC5cIl19XSIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ3ZWJfc2VhcmNoIiwiaWQiOiI2ZTUxMjVhYy0zMTY0LTRhNzYtOWM1Mi1hYzM0NjBjZTM2ZTQiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJzdGF0dXMiOiJzdWNjZXNzIn0seyJjb250ZW50IjpbeyJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3NrZXB0aWMiLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QTZGNGNDTjV5aEQ0Q3JzOU0iLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjB9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InJlc2VhcmNoZXIiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtM2NkOC03OWYxLTg4YTEtY2U2Y2VlMjk0ZTc5LTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoidHJhbnNmZXJfdG9fc2tlcHRpYyIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsInRvdGFsX3Rva2VucyI6NzQxMSwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fSx7ImNvbnRlbnQiOiJTdWNjZXNzZnVsbHkgdHJhbnNmZXJyZWQgdG8gc2tlcHRpYyIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ0cmFuc2Zlcl90b19za2VwdGljIiwiaWQiOiIzODkwNThkMS0wODNmLTQ2MzItYmJjNS0wOWJmZDNhMGZmZDIiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMVhqajNhUmRXeWhCSmZQbVRKQnhRYTkiLCJzdGF0dXMiOiJzdWNjZXNzIn1dLCJhY3RpdmVfYWdlbnQiOiJza2VwdGljIn0=" + } + } + ], + "events": [ + { + "timeUnixNano": "1790968030909679000", + "name": "exception", + "attributes": [ + { + "key": "exception.type", + "value": { + "stringValue": "Exception" + } + }, + { + "key": "exception.message", + "value": { + "stringValue": "ParentCommand(Command(graph='skeptic:3dfc4328-0d19-e1ea-bcda-3d3d36d7cda8', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', id='389058d1-083f-4632-bbc5-09bfd3a0ffd2', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9'), AIMessage(content=[{'signature': 'CAQS1QgKEAgSGAI4AUIIdGhpbmtpbmcSDN6TfU6NDM971lEbzxoMvRUA1J9ox3fJQYT9IjArCQQGHBP4pRHPavXzRlbMbaYHC57Yj+Gbxgr0helvd2OIL/o6Rbt4PFA3FaIFUlgq8gc7KQI9FtGnvSAkLBRzIsUp/2d5Rd2zgEsfkFVeXsn8WYYZkinAQVVy6A7B3LaEJW0xb/x88Di67fedwx0Q+FlZtwcmT3Dcaa11JveyHRr3dm+zGSZP6CXeXNJ8nrTZ7W1YRTysD6w7o7Lp8h1Rwq4f4o8j+QUfY0vje+zJZsxHPmQNt3ykeKh99hgn6dbiz6/EHqmbHjSAu1M4SqEDfqIKThHYenamAZmcQ4XAOo6mdQ6+77VgpV1lC1/k52IiUoJjkGlQrnLCvutiI7KN+sgK8c2+2KCJrPRCvVV+BkStvP5jWWqzytpKQ+nHzhL448OrIA9gdJC39C8eDrXeRnI7t4FqlkXGoMlZAV9UMcrQHRxOMi8tOyTzi9oyBFDTSsQ+LJqer0tz1Ft9gek6vzg50SpVAFQdLERv8R1YqMaeI+xsvTAEW9MRgvRMkzLuzRT2guw3jD+Fjip37xgK5QI0R+CsOAmQPoTZizzcIyAUAuCJFCgq/gD/CibLxoymiVUI1an9Snt3KpNRoWdvWIne33V0pv8PHg9yW2JuLOqQGilLCsK/Q3oQr3upEMXxXGjCL7WUizvRZGFEEV+VplYt7v4tKZMj5pk3vdeM0MJ5XZg7PvUsRzycxcnrT3Knoc3qM+ttqZfTsb8d6U/pQifq7GuahFOHrvroqgJLj+/81Yy1pZFkjfJgY0XklzAnA6CFq/YT0gxbV+TH5IS+WaYOUzK36S8s5qqMwhngv4IzE3ctXXqnhIZXNznyVBqbYa/Gx2bCjcEj2n5RyGYRS/+MculgLBznQ9MkG2D7QP3Mq/nwHXKMIaLS8VK8hG3d0rAjlvnauXShNrJxtkj4qkwnxD4LrEjk1ue+PLcbLqYWwAr8n0s+41jrsIPVyyK9vFaEZrA9KaBxkJyiqx50GZNn7ckx+mkI18gH52iyVPtU8FKMXjugCl6mPLiHYiDQd+G63LudmOKys9itVnBDBNMP2b/I7+bIVRlheGaZWr6MATKl86B3y+0kuOrLRMyWcdibc2iQkZm+8f7dTUwbKbBJsKOFY73/8nlL/JiYpjz6nUIk+IqSjaFyK6XFlCcukWpWHpDJnSvbEzey7qHyCqJ+I+on8iP8GOtempQaS0/4kZGSwlrE6xOj6EwGpfjbDZKHkP/2RAdNYPQq3z7bdVJMJNnFMz6e37mzMpCBihwPdSFTRE3SCY66wvTgJzGCUyhHbSi5i1azzOXd8uHCSpT/6L1o6VU0pjoR8Kaa4wCWHGf7P/WEd+avyp02Jnu1cmpW0+Gq+vK9h0tdb2IClM/eNfdouQP+ep8MJUjraDRICmAJQY5obS71vniOo+reaFy0zRgB', 'thinking': '', 'type': 'thinking'}, {'signature': 'CAQS/REKEQgSGAI4AUIJbmFycmF0aW9uEgwFh3AVJZP3s8y1Hq0aDG76uhHONaScXAGShiIwGgjLXHUx1yZrkLPljjzTOQycpLJiKVwXcxm/EzKJm1s8M5WxuREWEldXcoaN0/msKpkRMT+eLb5ih9rAHULTomUNsUwJeu0jrJe4GLazy7sP5KtjEOEBKQsXUfoQ7fDkZOQUHZi9zzQN4hFNmYJUf3OVqf9TQNdC1+m//HAZPfWmAy6HtDvm7kgBJkCEoiFnm7tn/PX1hXGPRgWc6BTXUMr2WA8aKyqggRUbMH2+JYv5iWZddCc1up1Nbr3sF7WPcot9ZBhyHQXr692zeJk6SJgygIVrASbMly2bBxTWJB623gVgzpCmVSLZFpkKXgPcp8hGyDKOvF6SiQSHqGRxQofr7Vs1aPhfZixROJGD+MlrkggVAnGMyYIDUZpavtCeiU1JmBSz0084bXPtT/tnpsKOnSPYZQo3B0BXKbqA/WsKng8/V3Z8Uuajo6IVZ2hgA1WDduqQOQkExGU6pFuLhebIjuWxSNI/DffvR//LNqCfvdnf0hvoPtDxfx/SawIx4/spq9xbwFzHtLgoHg1F2wFw7clPDlg1PyD/v2yJG3V9T8GL6jGPE6rhfil/u4HNvf7BW0dQzCTTXcqZy4nByy0+p+S7ZxfMl4Kpxa2gOhhSNjfpfuX0QpL4kME8J1nliFN1d8KqEPuDI18BQHUhD/UfY29oA0x510FjM/BsLZ4n5sppkoIi7CFoQq2aAKVNfC1KilhIpRD+1dyDtZQklN8gZpOESHw6ML1TtU0yfAcbmqoxJ6hhjzwH6CMsTQEEy8MbRBM+QKKkIRdRCPHGFW2LkuR50+J0pF/tr6pqVvJeozfiMnrEilB27TZP4nKTJG8rd//vCWen0y7CA/8dBJ9gdp5tx7NVA45k1gdcxER1gYouNX3n1vG3/B1F4+93wXW9Mqo6Us59TQmrb9Nqa7pSQNSrH06w1GxyBAlkFfqAlafk16aMpq416TJYpGy43WCUiCcLWlhiWgz2sgOLAXP9a5weWAKRC8J6jkcUkDeXnW+HFSiPM6+9Bo6kVhLUhN5rc4mHZOZpF7ptPpsQN4XCqLPsqpRH/8o5XpyC6ZneTeF2ExtjhwZVjXDwMbIzVYIbw1y2PPGw8dC7pc4sIVSnI4oQAH0v650ghfhmMevWW4h4Pylbw/DG4fdeT3+8z0f8MOSTtfipx05/QttHlAvODRxa5yDCvOz6n3f1BO0N/4MbGyOM27qW4x8mBVoryUZWfsmhfi0/GfNvB8+ylTGbYNX27OcEF5WZtNqbWEy7V5GbmZUSxspcg7FBZ3j3/3L7muvZXoZi1f5erq4y3lSH+oF7qPFnvieyLOCwubcQy+oyowj4cD0idRecnp+s+52D7+mzF1gpW+f+cJasoxigbxkOloS4yeiKvgM4nWSpGEj12RPx5o80EmziXN9t4zJzW1htA5hXJJeFympMYtyCUCevRWVmrRYktQ7UnCa6tL/AVPi1ni5XCXHfLOuqfK5KLYxjwVtdhhfmCLvpaKVNCEOCf4nNGHqD98wUb0ejWNwDYBnJGkBRw9RVspfwNdoWoVcn2qv8ywGUp7yc/sk2LpI6VziRyvWqbGNG1ZJqVVutRc46+sOE4VSeS2/CTNKNa6NMCf/hxxaUVsYfKdP+S1Lc4ianKp0sUugPpmtoydC0CP2RwjnHLcnzxCnvqxlLQWXMNPTsu2yBcHlWJLtcmdg/McLagy7l5kG6sN7ZxJLD/kAx8E6uqohP+K/jMSrYuQJnhr4tf4biWFY3Jzf/KpAYyVSq+pbfFKXMbL9nHEoZQT15Wyabd6DMAq3QZOthM5pUzdNqUXkcrI1PwjASAg4dVU/2o+98mTXnyhP7VlEkTnddpwyJF+YCCc01pqa6ZX99pnR+YOpIrb5DtPcKq0t5cFCDyQxN1lUuqZcYujvX/GC7+6IpRTz/NkEY/5cntOfZJyd6SkUx/JR5AJIRPCNeib0rcWnnFn0XK8s22x40wLjEtufXDO9+yraf4kHxlBI3SB0meMjepX50CJhAx7NekkRFnwoecLfqIyrjXrgAZsTMPiX94snjdrzisAXtBKanulilSxOfW2HSmfnk0L2GXHJJ3S+vCbgREml9dekfmHhmwEYtv3kfhth7ERIwVT8j69OtIBCmeTIJuA7o73PXc1LJ2whuZK7FZe6NTOs61MP7JVlIc6tbhtOBk3NXU8pBPOCY6Q55d63FtpUU/KCbQ0K2WQ9o5jNGwNNJ6Cw5FDCt+x5YJeP1d2QzDSiq+cSZqwqCZVcNoAftGGzgrRZ4vkpbP0xQZHCtWsUwHVTjrY4NfRDjHdVPRn8J9Xtooc/hIWkqzFaczYTlapu2RCJqe8aEo3jzWZCEJ7AiBzBYraYDgOfeiQyeJUsbbXHdGKiiw22Bdp4M+Yz671MZ0ie9EWRks0Q2DHGUTne9S4tAAJPGbUf4xtCMoJ40Q4QW05QLSZy3wNFAUFYmXE4ECDFB8+IEeNFFF/tgAnfh8GvFh/1xY+LE+ozMminjSRwhq09zQAEkNFyu0IKgvDkyOI+iccF7doWLTUasPxLH1xGPBSKkeWCnSoX/4C2c9du66BbRXamHqae+VCiS6BN953iYKOu8rMkneW+TFjV0CzAf8PNG9LBayUgY5Uw9x0Sap6zI4PpaZKH4d9ktkFU+91yBbN5SRtdd8/EQatJXmBNfw7kRrll+LjwZweWLEb0jT1RpGuDqHLdLTMMgowSczrp2zCsJdQH/seaYrpwkCFR8dRjM7JRUkVa1A0SvdZAX75vuBvvLBzN2c+Rbt5oqy93lqIBIjKb7w2UkRTb4pBWcNKNPUgt3az/PxQxEasaltdmuyvsXYbur3VPNn8JzN/6J5N+QCnz571TccapCmkPvFE+gayXU+UF1VKYyFcUvlev7lsQlmjqzK2QA8X7Q3TzFapqhvJKlt7dzBHraqZBgPgb9N4oKNayi8FZT71+kaYhIhFrESKYRHfHkj5n3kfhgLy+x+jLDbKxMggkgHnJz9RuEHZXBWtkYAQ==', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_verifier', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAMRdS21FeJfzQhM3s', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7396, 'output_tokens': 770, 'output_tokens_details': {'thinking_tokens': 739}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='skeptic', id='lc_run--01a0fe03-4ae9-76c0-89a6-4f0b114e3030-0', tool_calls=[{'name': 'transfer_to_verifier', 'args': {}, 'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7396, 'output_tokens': 770, 'total_tokens': 8166, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 739}}), ToolMessage(content='Successfully transferred to verifier', name='transfer_to_verifier', tool_call_id='toolu_01H6fV8JvRJNvXy47eRnd3jH')], 'active_agent': 'verifier'}, goto='verifier'))Traceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 707, in invoke\n input = context.run(step.invoke, input, config, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 3913, in invoke\n for chunk in self.stream(\n ~~~~~~~~~~~^\n input,\n ^^^^^^\n ...<11 lines>...\n **kwargs,\n ^^^^^^^^^\n ):\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 2967, in stream\n for _ in runner.tick(\n ~~~~~~~~~~~^\n [t for t in loop.tasks.values() if not t.writes],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<2 lines>...\n schedule_task=loop.accept_push,\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ):\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_runner.py\", line 207, in tick\n run_with_retry(\n ~~~~~~~~~~~~~~^\n t,\n ^^\n ...<10 lines>...\n },\n ^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_retry.py\", line 617, in run_with_retry\n return task.proc.invoke(task.input, config)\n ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 709, in invoke\n input = step.invoke(input, config)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 447, in invoke\n ret = self.func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 84, in _write\n self.do_write(\n ~~~~~~~~~~~~~^\n config,\n ^^^^^^^\n writes,\n ^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 126, in do_write\n write(_assemble_writes(writes))\n ~~~~~~~~~~~~~~~~^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 181, in _assemble_writes\n if ww := w.mapper(w.value):\n ~~~~~~~~^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/graph/state.py\", line 1762, in _control_branch\n raise ParentCommand(command)\n\n\nlanggraph.errors.ParentCommand: Command(graph='skeptic:3dfc4328-0d19-e1ea-bcda-3d3d36d7cda8', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', id='389058d1-083f-4632-bbc5-09bfd3a0ffd2', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9'), AIMessage(content=[{'signature': 'CAQS1QgKEAgSGAI4AUIIdGhpbmtpbmcSDN6TfU6NDM971lEbzxoMvRUA1J9ox3fJQYT9IjArCQQGHBP4pRHPavXzRlbMbaYHC57Yj+Gbxgr0helvd2OIL/o6Rbt4PFA3FaIFUlgq8gc7KQI9FtGnvSAkLBRzIsUp/2d5Rd2zgEsfkFVeXsn8WYYZkinAQVVy6A7B3LaEJW0xb/x88Di67fedwx0Q+FlZtwcmT3Dcaa11JveyHRr3dm+zGSZP6CXeXNJ8nrTZ7W1YRTysD6w7o7Lp8h1Rwq4f4o8j+QUfY0vje+zJZsxHPmQNt3ykeKh99hgn6dbiz6/EHqmbHjSAu1M4SqEDfqIKThHYenamAZmcQ4XAOo6mdQ6+77VgpV1lC1/k52IiUoJjkGlQrnLCvutiI7KN+sgK8c2+2KCJrPRCvVV+BkStvP5jWWqzytpKQ+nHzhL448OrIA9gdJC39C8eDrXeRnI7t4FqlkXGoMlZAV9UMcrQHRxOMi8tOyTzi9oyBFDTSsQ+LJqer0tz1Ft9gek6vzg50SpVAFQdLERv8R1YqMaeI+xsvTAEW9MRgvRMkzLuzRT2guw3jD+Fjip37xgK5QI0R+CsOAmQPoTZizzcIyAUAuCJFCgq/gD/CibLxoymiVUI1an9Snt3KpNRoWdvWIne33V0pv8PHg9yW2JuLOqQGilLCsK/Q3oQr3upEMXxXGjCL7WUizvRZGFEEV+VplYt7v4tKZMj5pk3vdeM0MJ5XZg7PvUsRzycxcnrT3Knoc3qM+ttqZfTsb8d6U/pQifq7GuahFOHrvroqgJLj+/81Yy1pZFkjfJgY0XklzAnA6CFq/YT0gxbV+TH5IS+WaYOUzK36S8s5qqMwhngv4IzE3ctXXqnhIZXNznyVBqbYa/Gx2bCjcEj2n5RyGYRS/+MculgLBznQ9MkG2D7QP3Mq/nwHXKMIaLS8VK8hG3d0rAjlvnauXShNrJxtkj4qkwnxD4LrEjk1ue+PLcbLqYWwAr8n0s+41jrsIPVyyK9vFaEZrA9KaBxkJyiqx50GZNn7ckx+mkI18gH52iyVPtU8FKMXjugCl6mPLiHYiDQd+G63LudmOKys9itVnBDBNMP2b/I7+bIVRlheGaZWr6MATKl86B3y+0kuOrLRMyWcdibc2iQkZm+8f7dTUwbKbBJsKOFY73/8nlL/JiYpjz6nUIk+IqSjaFyK6XFlCcukWpWHpDJnSvbEzey7qHyCqJ+I+on8iP8GOtempQaS0/4kZGSwlrE6xOj6EwGpfjbDZKHkP/2RAdNYPQq3z7bdVJMJNnFMz6e37mzMpCBihwPdSFTRE3SCY66wvTgJzGCUyhHbSi5i1azzOXd8uHCSpT/6L1o6VU0pjoR8Kaa4wCWHGf7P/WEd+avyp02Jnu1cmpW0+Gq+vK9h0tdb2IClM/eNfdouQP+ep8MJUjraDRICmAJQY5obS71vniOo+reaFy0zRgB', 'thinking': '', 'type': 'thinking'}, {'signature': 'CAQS/REKEQgSGAI4AUIJbmFycmF0aW9uEgwFh3AVJZP3s8y1Hq0aDG76uhHONaScXAGShiIwGgjLXHUx1yZrkLPljjzTOQycpLJiKVwXcxm/EzKJm1s8M5WxuREWEldXcoaN0/msKpkRMT+eLb5ih9rAHULTomUNsUwJeu0jrJe4GLazy7sP5KtjEOEBKQsXUfoQ7fDkZOQUHZi9zzQN4hFNmYJUf3OVqf9TQNdC1+m//HAZPfWmAy6HtDvm7kgBJkCEoiFnm7tn/PX1hXGPRgWc6BTXUMr2WA8aKyqggRUbMH2+JYv5iWZddCc1up1Nbr3sF7WPcot9ZBhyHQXr692zeJk6SJgygIVrASbMly2bBxTWJB623gVgzpCmVSLZFpkKXgPcp8hGyDKOvF6SiQSHqGRxQofr7Vs1aPhfZixROJGD+MlrkggVAnGMyYIDUZpavtCeiU1JmBSz0084bXPtT/tnpsKOnSPYZQo3B0BXKbqA/WsKng8/V3Z8Uuajo6IVZ2hgA1WDduqQOQkExGU6pFuLhebIjuWxSNI/DffvR//LNqCfvdnf0hvoPtDxfx/SawIx4/spq9xbwFzHtLgoHg1F2wFw7clPDlg1PyD/v2yJG3V9T8GL6jGPE6rhfil/u4HNvf7BW0dQzCTTXcqZy4nByy0+p+S7ZxfMl4Kpxa2gOhhSNjfpfuX0QpL4kME8J1nliFN1d8KqEPuDI18BQHUhD/UfY29oA0x510FjM/BsLZ4n5sppkoIi7CFoQq2aAKVNfC1KilhIpRD+1dyDtZQklN8gZpOESHw6ML1TtU0yfAcbmqoxJ6hhjzwH6CMsTQEEy8MbRBM+QKKkIRdRCPHGFW2LkuR50+J0pF/tr6pqVvJeozfiMnrEilB27TZP4nKTJG8rd//vCWen0y7CA/8dBJ9gdp5tx7NVA45k1gdcxER1gYouNX3n1vG3/B1F4+93wXW9Mqo6Us59TQmrb9Nqa7pSQNSrH06w1GxyBAlkFfqAlafk16aMpq416TJYpGy43WCUiCcLWlhiWgz2sgOLAXP9a5weWAKRC8J6jkcUkDeXnW+HFSiPM6+9Bo6kVhLUhN5rc4mHZOZpF7ptPpsQN4XCqLPsqpRH/8o5XpyC6ZneTeF2ExtjhwZVjXDwMbIzVYIbw1y2PPGw8dC7pc4sIVSnI4oQAH0v650ghfhmMevWW4h4Pylbw/DG4fdeT3+8z0f8MOSTtfipx05/QttHlAvODRxa5yDCvOz6n3f1BO0N/4MbGyOM27qW4x8mBVoryUZWfsmhfi0/GfNvB8+ylTGbYNX27OcEF5WZtNqbWEy7V5GbmZUSxspcg7FBZ3j3/3L7muvZXoZi1f5erq4y3lSH+oF7qPFnvieyLOCwubcQy+oyowj4cD0idRecnp+s+52D7+mzF1gpW+f+cJasoxigbxkOloS4yeiKvgM4nWSpGEj12RPx5o80EmziXN9t4zJzW1htA5hXJJeFympMYtyCUCevRWVmrRYktQ7UnCa6tL/AVPi1ni5XCXHfLOuqfK5KLYxjwVtdhhfmCLvpaKVNCEOCf4nNGHqD98wUb0ejWNwDYBnJGkBRw9RVspfwNdoWoVcn2qv8ywGUp7yc/sk2LpI6VziRyvWqbGNG1ZJqVVutRc46+sOE4VSeS2/CTNKNa6NMCf/hxxaUVsYfKdP+S1Lc4ianKp0sUugPpmtoydC0CP2RwjnHLcnzxCnvqxlLQWXMNPTsu2yBcHlWJLtcmdg/McLagy7l5kG6sN7ZxJLD/kAx8E6uqohP+K/jMSrYuQJnhr4tf4biWFY3Jzf/KpAYyVSq+pbfFKXMbL9nHEoZQT15Wyabd6DMAq3QZOthM5pUzdNqUXkcrI1PwjASAg4dVU/2o+98mTXnyhP7VlEkTnddpwyJF+YCCc01pqa6ZX99pnR+YOpIrb5DtPcKq0t5cFCDyQxN1lUuqZcYujvX/GC7+6IpRTz/NkEY/5cntOfZJyd6SkUx/JR5AJIRPCNeib0rcWnnFn0XK8s22x40wLjEtufXDO9+yraf4kHxlBI3SB0meMjepX50CJhAx7NekkRFnwoecLfqIyrjXrgAZsTMPiX94snjdrzisAXtBKanulilSxOfW2HSmfnk0L2GXHJJ3S+vCbgREml9dekfmHhmwEYtv3kfhth7ERIwVT8j69OtIBCmeTIJuA7o73PXc1LJ2whuZK7FZe6NTOs61MP7JVlIc6tbhtOBk3NXU8pBPOCY6Q55d63FtpUU/KCbQ0K2WQ9o5jNGwNNJ6Cw5FDCt+x5YJeP1d2QzDSiq+cSZqwqCZVcNoAftGGzgrRZ4vkpbP0xQZHCtWsUwHVTjrY4NfRDjHdVPRn8J9Xtooc/hIWkqzFaczYTlapu2RCJqe8aEo3jzWZCEJ7AiBzBYraYDgOfeiQyeJUsbbXHdGKiiw22Bdp4M+Yz671MZ0ie9EWRks0Q2DHGUTne9S4tAAJPGbUf4xtCMoJ40Q4QW05QLSZy3wNFAUFYmXE4ECDFB8+IEeNFFF/tgAnfh8GvFh/1xY+LE+ozMminjSRwhq09zQAEkNFyu0IKgvDkyOI+iccF7doWLTUasPxLH1xGPBSKkeWCnSoX/4C2c9du66BbRXamHqae+VCiS6BN953iYKOu8rMkneW+TFjV0CzAf8PNG9LBayUgY5Uw9x0Sap6zI4PpaZKH4d9ktkFU+91yBbN5SRtdd8/EQatJXmBNfw7kRrll+LjwZweWLEb0jT1RpGuDqHLdLTMMgowSczrp2zCsJdQH/seaYrpwkCFR8dRjM7JRUkVa1A0SvdZAX75vuBvvLBzN2c+Rbt5oqy93lqIBIjKb7w2UkRTb4pBWcNKNPUgt3az/PxQxEasaltdmuyvsXYbur3VPNn8JzN/6J5N+QCnz571TccapCmkPvFE+gayXU+UF1VKYyFcUvlev7lsQlmjqzK2QA8X7Q3TzFapqhvJKlt7dzBHraqZBgPgb9N4oKNayi8FZT71+kaYhIhFrESKYRHfHkj5n3kfhgLy+x+jLDbKxMggkgHnJz9RuEHZXBWtkYAQ==', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_verifier', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAMRdS21FeJfzQhM3s', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7396, 'output_tokens': 770, 'output_tokens_details': {'thinking_tokens': 739}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='skeptic', id='lc_run--01a0fe03-4ae9-76c0-89a6-4f0b114e3030-0', tool_calls=[{'name': 'transfer_to_verifier', 'args': {}, 'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7396, 'output_tokens': 770, 'total_tokens': 8166, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 739}}), ToolMessage(content='Successfully transferred to verifier', name='transfer_to_verifier', tool_call_id='toolu_01H6fV8JvRJNvXy47eRnd3jH')], 'active_agent': 'verifier'}, goto='verifier')" + } + }, + { + "key": "exception.stacktrace", + "value": { + "stringValue": "Exception: ParentCommand(Command(graph='skeptic:3dfc4328-0d19-e1ea-bcda-3d3d36d7cda8', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', id='389058d1-083f-4632-bbc5-09bfd3a0ffd2', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9'), AIMessage(content=[{'signature': 'CAQS1QgKEAgSGAI4AUIIdGhpbmtpbmcSDN6TfU6NDM971lEbzxoMvRUA1J9ox3fJQYT9IjArCQQGHBP4pRHPavXzRlbMbaYHC57Yj+Gbxgr0helvd2OIL/o6Rbt4PFA3FaIFUlgq8gc7KQI9FtGnvSAkLBRzIsUp/2d5Rd2zgEsfkFVeXsn8WYYZkinAQVVy6A7B3LaEJW0xb/x88Di67fedwx0Q+FlZtwcmT3Dcaa11JveyHRr3dm+zGSZP6CXeXNJ8nrTZ7W1YRTysD6w7o7Lp8h1Rwq4f4o8j+QUfY0vje+zJZsxHPmQNt3ykeKh99hgn6dbiz6/EHqmbHjSAu1M4SqEDfqIKThHYenamAZmcQ4XAOo6mdQ6+77VgpV1lC1/k52IiUoJjkGlQrnLCvutiI7KN+sgK8c2+2KCJrPRCvVV+BkStvP5jWWqzytpKQ+nHzhL448OrIA9gdJC39C8eDrXeRnI7t4FqlkXGoMlZAV9UMcrQHRxOMi8tOyTzi9oyBFDTSsQ+LJqer0tz1Ft9gek6vzg50SpVAFQdLERv8R1YqMaeI+xsvTAEW9MRgvRMkzLuzRT2guw3jD+Fjip37xgK5QI0R+CsOAmQPoTZizzcIyAUAuCJFCgq/gD/CibLxoymiVUI1an9Snt3KpNRoWdvWIne33V0pv8PHg9yW2JuLOqQGilLCsK/Q3oQr3upEMXxXGjCL7WUizvRZGFEEV+VplYt7v4tKZMj5pk3vdeM0MJ5XZg7PvUsRzycxcnrT3Knoc3qM+ttqZfTsb8d6U/pQifq7GuahFOHrvroqgJLj+/81Yy1pZFkjfJgY0XklzAnA6CFq/YT0gxbV+TH5IS+WaYOUzK36S8s5qqMwhngv4IzE3ctXXqnhIZXNznyVBqbYa/Gx2bCjcEj2n5RyGYRS/+MculgLBznQ9MkG2D7QP3Mq/nwHXKMIaLS8VK8hG3d0rAjlvnauXShNrJxtkj4qkwnxD4LrEjk1ue+PLcbLqYWwAr8n0s+41jrsIPVyyK9vFaEZrA9KaBxkJyiqx50GZNn7ckx+mkI18gH52iyVPtU8FKMXjugCl6mPLiHYiDQd+G63LudmOKys9itVnBDBNMP2b/I7+bIVRlheGaZWr6MATKl86B3y+0kuOrLRMyWcdibc2iQkZm+8f7dTUwbKbBJsKOFY73/8nlL/JiYpjz6nUIk+IqSjaFyK6XFlCcukWpWHpDJnSvbEzey7qHyCqJ+I+on8iP8GOtempQaS0/4kZGSwlrE6xOj6EwGpfjbDZKHkP/2RAdNYPQq3z7bdVJMJNnFMz6e37mzMpCBihwPdSFTRE3SCY66wvTgJzGCUyhHbSi5i1azzOXd8uHCSpT/6L1o6VU0pjoR8Kaa4wCWHGf7P/WEd+avyp02Jnu1cmpW0+Gq+vK9h0tdb2IClM/eNfdouQP+ep8MJUjraDRICmAJQY5obS71vniOo+reaFy0zRgB', 'thinking': '', 'type': 'thinking'}, {'signature': 'CAQS/REKEQgSGAI4AUIJbmFycmF0aW9uEgwFh3AVJZP3s8y1Hq0aDG76uhHONaScXAGShiIwGgjLXHUx1yZrkLPljjzTOQycpLJiKVwXcxm/EzKJm1s8M5WxuREWEldXcoaN0/msKpkRMT+eLb5ih9rAHULTomUNsUwJeu0jrJe4GLazy7sP5KtjEOEBKQsXUfoQ7fDkZOQUHZi9zzQN4hFNmYJUf3OVqf9TQNdC1+m//HAZPfWmAy6HtDvm7kgBJkCEoiFnm7tn/PX1hXGPRgWc6BTXUMr2WA8aKyqggRUbMH2+JYv5iWZddCc1up1Nbr3sF7WPcot9ZBhyHQXr692zeJk6SJgygIVrASbMly2bBxTWJB623gVgzpCmVSLZFpkKXgPcp8hGyDKOvF6SiQSHqGRxQofr7Vs1aPhfZixROJGD+MlrkggVAnGMyYIDUZpavtCeiU1JmBSz0084bXPtT/tnpsKOnSPYZQo3B0BXKbqA/WsKng8/V3Z8Uuajo6IVZ2hgA1WDduqQOQkExGU6pFuLhebIjuWxSNI/DffvR//LNqCfvdnf0hvoPtDxfx/SawIx4/spq9xbwFzHtLgoHg1F2wFw7clPDlg1PyD/v2yJG3V9T8GL6jGPE6rhfil/u4HNvf7BW0dQzCTTXcqZy4nByy0+p+S7ZxfMl4Kpxa2gOhhSNjfpfuX0QpL4kME8J1nliFN1d8KqEPuDI18BQHUhD/UfY29oA0x510FjM/BsLZ4n5sppkoIi7CFoQq2aAKVNfC1KilhIpRD+1dyDtZQklN8gZpOESHw6ML1TtU0yfAcbmqoxJ6hhjzwH6CMsTQEEy8MbRBM+QKKkIRdRCPHGFW2LkuR50+J0pF/tr6pqVvJeozfiMnrEilB27TZP4nKTJG8rd//vCWen0y7CA/8dBJ9gdp5tx7NVA45k1gdcxER1gYouNX3n1vG3/B1F4+93wXW9Mqo6Us59TQmrb9Nqa7pSQNSrH06w1GxyBAlkFfqAlafk16aMpq416TJYpGy43WCUiCcLWlhiWgz2sgOLAXP9a5weWAKRC8J6jkcUkDeXnW+HFSiPM6+9Bo6kVhLUhN5rc4mHZOZpF7ptPpsQN4XCqLPsqpRH/8o5XpyC6ZneTeF2ExtjhwZVjXDwMbIzVYIbw1y2PPGw8dC7pc4sIVSnI4oQAH0v650ghfhmMevWW4h4Pylbw/DG4fdeT3+8z0f8MOSTtfipx05/QttHlAvODRxa5yDCvOz6n3f1BO0N/4MbGyOM27qW4x8mBVoryUZWfsmhfi0/GfNvB8+ylTGbYNX27OcEF5WZtNqbWEy7V5GbmZUSxspcg7FBZ3j3/3L7muvZXoZi1f5erq4y3lSH+oF7qPFnvieyLOCwubcQy+oyowj4cD0idRecnp+s+52D7+mzF1gpW+f+cJasoxigbxkOloS4yeiKvgM4nWSpGEj12RPx5o80EmziXN9t4zJzW1htA5hXJJeFympMYtyCUCevRWVmrRYktQ7UnCa6tL/AVPi1ni5XCXHfLOuqfK5KLYxjwVtdhhfmCLvpaKVNCEOCf4nNGHqD98wUb0ejWNwDYBnJGkBRw9RVspfwNdoWoVcn2qv8ywGUp7yc/sk2LpI6VziRyvWqbGNG1ZJqVVutRc46+sOE4VSeS2/CTNKNa6NMCf/hxxaUVsYfKdP+S1Lc4ianKp0sUugPpmtoydC0CP2RwjnHLcnzxCnvqxlLQWXMNPTsu2yBcHlWJLtcmdg/McLagy7l5kG6sN7ZxJLD/kAx8E6uqohP+K/jMSrYuQJnhr4tf4biWFY3Jzf/KpAYyVSq+pbfFKXMbL9nHEoZQT15Wyabd6DMAq3QZOthM5pUzdNqUXkcrI1PwjASAg4dVU/2o+98mTXnyhP7VlEkTnddpwyJF+YCCc01pqa6ZX99pnR+YOpIrb5DtPcKq0t5cFCDyQxN1lUuqZcYujvX/GC7+6IpRTz/NkEY/5cntOfZJyd6SkUx/JR5AJIRPCNeib0rcWnnFn0XK8s22x40wLjEtufXDO9+yraf4kHxlBI3SB0meMjepX50CJhAx7NekkRFnwoecLfqIyrjXrgAZsTMPiX94snjdrzisAXtBKanulilSxOfW2HSmfnk0L2GXHJJ3S+vCbgREml9dekfmHhmwEYtv3kfhth7ERIwVT8j69OtIBCmeTIJuA7o73PXc1LJ2whuZK7FZe6NTOs61MP7JVlIc6tbhtOBk3NXU8pBPOCY6Q55d63FtpUU/KCbQ0K2WQ9o5jNGwNNJ6Cw5FDCt+x5YJeP1d2QzDSiq+cSZqwqCZVcNoAftGGzgrRZ4vkpbP0xQZHCtWsUwHVTjrY4NfRDjHdVPRn8J9Xtooc/hIWkqzFaczYTlapu2RCJqe8aEo3jzWZCEJ7AiBzBYraYDgOfeiQyeJUsbbXHdGKiiw22Bdp4M+Yz671MZ0ie9EWRks0Q2DHGUTne9S4tAAJPGbUf4xtCMoJ40Q4QW05QLSZy3wNFAUFYmXE4ECDFB8+IEeNFFF/tgAnfh8GvFh/1xY+LE+ozMminjSRwhq09zQAEkNFyu0IKgvDkyOI+iccF7doWLTUasPxLH1xGPBSKkeWCnSoX/4C2c9du66BbRXamHqae+VCiS6BN953iYKOu8rMkneW+TFjV0CzAf8PNG9LBayUgY5Uw9x0Sap6zI4PpaZKH4d9ktkFU+91yBbN5SRtdd8/EQatJXmBNfw7kRrll+LjwZweWLEb0jT1RpGuDqHLdLTMMgowSczrp2zCsJdQH/seaYrpwkCFR8dRjM7JRUkVa1A0SvdZAX75vuBvvLBzN2c+Rbt5oqy93lqIBIjKb7w2UkRTb4pBWcNKNPUgt3az/PxQxEasaltdmuyvsXYbur3VPNn8JzN/6J5N+QCnz571TccapCmkPvFE+gayXU+UF1VKYyFcUvlev7lsQlmjqzK2QA8X7Q3TzFapqhvJKlt7dzBHraqZBgPgb9N4oKNayi8FZT71+kaYhIhFrESKYRHfHkj5n3kfhgLy+x+jLDbKxMggkgHnJz9RuEHZXBWtkYAQ==', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_verifier', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAMRdS21FeJfzQhM3s', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7396, 'output_tokens': 770, 'output_tokens_details': {'thinking_tokens': 739}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='skeptic', id='lc_run--01a0fe03-4ae9-76c0-89a6-4f0b114e3030-0', tool_calls=[{'name': 'transfer_to_verifier', 'args': {}, 'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7396, 'output_tokens': 770, 'total_tokens': 8166, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 739}}), ToolMessage(content='Successfully transferred to verifier', name='transfer_to_verifier', tool_call_id='toolu_01H6fV8JvRJNvXy47eRnd3jH')], 'active_agent': 'verifier'}, goto='verifier'))Traceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 707, in invoke\n input = context.run(step.invoke, input, config, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 3913, in invoke\n for chunk in self.stream(\n ~~~~~~~~~~~^\n input,\n ^^^^^^\n ...<11 lines>...\n **kwargs,\n ^^^^^^^^^\n ):\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 2967, in stream\n for _ in runner.tick(\n ~~~~~~~~~~~^\n [t for t in loop.tasks.values() if not t.writes],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<2 lines>...\n schedule_task=loop.accept_push,\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ):\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_runner.py\", line 207, in tick\n run_with_retry(\n ~~~~~~~~~~~~~~^\n t,\n ^^\n ...<10 lines>...\n },\n ^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_retry.py\", line 617, in run_with_retry\n return task.proc.invoke(task.input, config)\n ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 709, in invoke\n input = step.invoke(input, config)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 447, in invoke\n ret = self.func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 84, in _write\n self.do_write(\n ~~~~~~~~~~~~~^\n config,\n ^^^^^^^\n writes,\n ^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 126, in do_write\n write(_assemble_writes(writes))\n ~~~~~~~~~~~~~~~~^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 181, in _assemble_writes\n if ww := w.mapper(w.value):\n ~~~~~~~~^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/graph/state.py\", line 1762, in _control_branch\n raise ParentCommand(command)\n\n\nlanggraph.errors.ParentCommand: Command(graph='skeptic:3dfc4328-0d19-e1ea-bcda-3d3d36d7cda8', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', id='389058d1-083f-4632-bbc5-09bfd3a0ffd2', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9'), AIMessage(content=[{'signature': 'CAQS1QgKEAgSGAI4AUIIdGhpbmtpbmcSDN6TfU6NDM971lEbzxoMvRUA1J9ox3fJQYT9IjArCQQGHBP4pRHPavXzRlbMbaYHC57Yj+Gbxgr0helvd2OIL/o6Rbt4PFA3FaIFUlgq8gc7KQI9FtGnvSAkLBRzIsUp/2d5Rd2zgEsfkFVeXsn8WYYZkinAQVVy6A7B3LaEJW0xb/x88Di67fedwx0Q+FlZtwcmT3Dcaa11JveyHRr3dm+zGSZP6CXeXNJ8nrTZ7W1YRTysD6w7o7Lp8h1Rwq4f4o8j+QUfY0vje+zJZsxHPmQNt3ykeKh99hgn6dbiz6/EHqmbHjSAu1M4SqEDfqIKThHYenamAZmcQ4XAOo6mdQ6+77VgpV1lC1/k52IiUoJjkGlQrnLCvutiI7KN+sgK8c2+2KCJrPRCvVV+BkStvP5jWWqzytpKQ+nHzhL448OrIA9gdJC39C8eDrXeRnI7t4FqlkXGoMlZAV9UMcrQHRxOMi8tOyTzi9oyBFDTSsQ+LJqer0tz1Ft9gek6vzg50SpVAFQdLERv8R1YqMaeI+xsvTAEW9MRgvRMkzLuzRT2guw3jD+Fjip37xgK5QI0R+CsOAmQPoTZizzcIyAUAuCJFCgq/gD/CibLxoymiVUI1an9Snt3KpNRoWdvWIne33V0pv8PHg9yW2JuLOqQGilLCsK/Q3oQr3upEMXxXGjCL7WUizvRZGFEEV+VplYt7v4tKZMj5pk3vdeM0MJ5XZg7PvUsRzycxcnrT3Knoc3qM+ttqZfTsb8d6U/pQifq7GuahFOHrvroqgJLj+/81Yy1pZFkjfJgY0XklzAnA6CFq/YT0gxbV+TH5IS+WaYOUzK36S8s5qqMwhngv4IzE3ctXXqnhIZXNznyVBqbYa/Gx2bCjcEj2n5RyGYRS/+MculgLBznQ9MkG2D7QP3Mq/nwHXKMIaLS8VK8hG3d0rAjlvnauXShNrJxtkj4qkwnxD4LrEjk1ue+PLcbLqYWwAr8n0s+41jrsIPVyyK9vFaEZrA9KaBxkJyiqx50GZNn7ckx+mkI18gH52iyVPtU8FKMXjugCl6mPLiHYiDQd+G63LudmOKys9itVnBDBNMP2b/I7+bIVRlheGaZWr6MATKl86B3y+0kuOrLRMyWcdibc2iQkZm+8f7dTUwbKbBJsKOFY73/8nlL/JiYpjz6nUIk+IqSjaFyK6XFlCcukWpWHpDJnSvbEzey7qHyCqJ+I+on8iP8GOtempQaS0/4kZGSwlrE6xOj6EwGpfjbDZKHkP/2RAdNYPQq3z7bdVJMJNnFMz6e37mzMpCBihwPdSFTRE3SCY66wvTgJzGCUyhHbSi5i1azzOXd8uHCSpT/6L1o6VU0pjoR8Kaa4wCWHGf7P/WEd+avyp02Jnu1cmpW0+Gq+vK9h0tdb2IClM/eNfdouQP+ep8MJUjraDRICmAJQY5obS71vniOo+reaFy0zRgB', 'thinking': '', 'type': 'thinking'}, {'signature': 'CAQS/REKEQgSGAI4AUIJbmFycmF0aW9uEgwFh3AVJZP3s8y1Hq0aDG76uhHONaScXAGShiIwGgjLXHUx1yZrkLPljjzTOQycpLJiKVwXcxm/EzKJm1s8M5WxuREWEldXcoaN0/msKpkRMT+eLb5ih9rAHULTomUNsUwJeu0jrJe4GLazy7sP5KtjEOEBKQsXUfoQ7fDkZOQUHZi9zzQN4hFNmYJUf3OVqf9TQNdC1+m//HAZPfWmAy6HtDvm7kgBJkCEoiFnm7tn/PX1hXGPRgWc6BTXUMr2WA8aKyqggRUbMH2+JYv5iWZddCc1up1Nbr3sF7WPcot9ZBhyHQXr692zeJk6SJgygIVrASbMly2bBxTWJB623gVgzpCmVSLZFpkKXgPcp8hGyDKOvF6SiQSHqGRxQofr7Vs1aPhfZixROJGD+MlrkggVAnGMyYIDUZpavtCeiU1JmBSz0084bXPtT/tnpsKOnSPYZQo3B0BXKbqA/WsKng8/V3Z8Uuajo6IVZ2hgA1WDduqQOQkExGU6pFuLhebIjuWxSNI/DffvR//LNqCfvdnf0hvoPtDxfx/SawIx4/spq9xbwFzHtLgoHg1F2wFw7clPDlg1PyD/v2yJG3V9T8GL6jGPE6rhfil/u4HNvf7BW0dQzCTTXcqZy4nByy0+p+S7ZxfMl4Kpxa2gOhhSNjfpfuX0QpL4kME8J1nliFN1d8KqEPuDI18BQHUhD/UfY29oA0x510FjM/BsLZ4n5sppkoIi7CFoQq2aAKVNfC1KilhIpRD+1dyDtZQklN8gZpOESHw6ML1TtU0yfAcbmqoxJ6hhjzwH6CMsTQEEy8MbRBM+QKKkIRdRCPHGFW2LkuR50+J0pF/tr6pqVvJeozfiMnrEilB27TZP4nKTJG8rd//vCWen0y7CA/8dBJ9gdp5tx7NVA45k1gdcxER1gYouNX3n1vG3/B1F4+93wXW9Mqo6Us59TQmrb9Nqa7pSQNSrH06w1GxyBAlkFfqAlafk16aMpq416TJYpGy43WCUiCcLWlhiWgz2sgOLAXP9a5weWAKRC8J6jkcUkDeXnW+HFSiPM6+9Bo6kVhLUhN5rc4mHZOZpF7ptPpsQN4XCqLPsqpRH/8o5XpyC6ZneTeF2ExtjhwZVjXDwMbIzVYIbw1y2PPGw8dC7pc4sIVSnI4oQAH0v650ghfhmMevWW4h4Pylbw/DG4fdeT3+8z0f8MOSTtfipx05/QttHlAvODRxa5yDCvOz6n3f1BO0N/4MbGyOM27qW4x8mBVoryUZWfsmhfi0/GfNvB8+ylTGbYNX27OcEF5WZtNqbWEy7V5GbmZUSxspcg7FBZ3j3/3L7muvZXoZi1f5erq4y3lSH+oF7qPFnvieyLOCwubcQy+oyowj4cD0idRecnp+s+52D7+mzF1gpW+f+cJasoxigbxkOloS4yeiKvgM4nWSpGEj12RPx5o80EmziXN9t4zJzW1htA5hXJJeFympMYtyCUCevRWVmrRYktQ7UnCa6tL/AVPi1ni5XCXHfLOuqfK5KLYxjwVtdhhfmCLvpaKVNCEOCf4nNGHqD98wUb0ejWNwDYBnJGkBRw9RVspfwNdoWoVcn2qv8ywGUp7yc/sk2LpI6VziRyvWqbGNG1ZJqVVutRc46+sOE4VSeS2/CTNKNa6NMCf/hxxaUVsYfKdP+S1Lc4ianKp0sUugPpmtoydC0CP2RwjnHLcnzxCnvqxlLQWXMNPTsu2yBcHlWJLtcmdg/McLagy7l5kG6sN7ZxJLD/kAx8E6uqohP+K/jMSrYuQJnhr4tf4biWFY3Jzf/KpAYyVSq+pbfFKXMbL9nHEoZQT15Wyabd6DMAq3QZOthM5pUzdNqUXkcrI1PwjASAg4dVU/2o+98mTXnyhP7VlEkTnddpwyJF+YCCc01pqa6ZX99pnR+YOpIrb5DtPcKq0t5cFCDyQxN1lUuqZcYujvX/GC7+6IpRTz/NkEY/5cntOfZJyd6SkUx/JR5AJIRPCNeib0rcWnnFn0XK8s22x40wLjEtufXDO9+yraf4kHxlBI3SB0meMjepX50CJhAx7NekkRFnwoecLfqIyrjXrgAZsTMPiX94snjdrzisAXtBKanulilSxOfW2HSmfnk0L2GXHJJ3S+vCbgREml9dekfmHhmwEYtv3kfhth7ERIwVT8j69OtIBCmeTIJuA7o73PXc1LJ2whuZK7FZe6NTOs61MP7JVlIc6tbhtOBk3NXU8pBPOCY6Q55d63FtpUU/KCbQ0K2WQ9o5jNGwNNJ6Cw5FDCt+x5YJeP1d2QzDSiq+cSZqwqCZVcNoAftGGzgrRZ4vkpbP0xQZHCtWsUwHVTjrY4NfRDjHdVPRn8J9Xtooc/hIWkqzFaczYTlapu2RCJqe8aEo3jzWZCEJ7AiBzBYraYDgOfeiQyeJUsbbXHdGKiiw22Bdp4M+Yz671MZ0ie9EWRks0Q2DHGUTne9S4tAAJPGbUf4xtCMoJ40Q4QW05QLSZy3wNFAUFYmXE4ECDFB8+IEeNFFF/tgAnfh8GvFh/1xY+LE+ozMminjSRwhq09zQAEkNFyu0IKgvDkyOI+iccF7doWLTUasPxLH1xGPBSKkeWCnSoX/4C2c9du66BbRXamHqae+VCiS6BN953iYKOu8rMkneW+TFjV0CzAf8PNG9LBayUgY5Uw9x0Sap6zI4PpaZKH4d9ktkFU+91yBbN5SRtdd8/EQatJXmBNfw7kRrll+LjwZweWLEb0jT1RpGuDqHLdLTMMgowSczrp2zCsJdQH/seaYrpwkCFR8dRjM7JRUkVa1A0SvdZAX75vuBvvLBzN2c+Rbt5oqy93lqIBIjKb7w2UkRTb4pBWcNKNPUgt3az/PxQxEasaltdmuyvsXYbur3VPNn8JzN/6J5N+QCnz571TccapCmkPvFE+gayXU+UF1VKYyFcUvlev7lsQlmjqzK2QA8X7Q3TzFapqhvJKlt7dzBHraqZBgPgb9N4oKNayi8FZT71+kaYhIhFrESKYRHfHkj5n3kfhgLy+x+jLDbKxMggkgHnJz9RuEHZXBWtkYAQ==', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_verifier', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAMRdS21FeJfzQhM3s', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7396, 'output_tokens': 770, 'output_tokens_details': {'thinking_tokens': 739}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='skeptic', id='lc_run--01a0fe03-4ae9-76c0-89a6-4f0b114e3030-0', tool_calls=[{'name': 'transfer_to_verifier', 'args': {}, 'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7396, 'output_tokens': 770, 'total_tokens': 8166, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 739}}), ToolMessage(content='Successfully transferred to verifier', name='transfer_to_verifier', tool_call_id='toolu_01H6fV8JvRJNvXy47eRnd3jH')], 'active_agent': 'verifier'}, goto='verifier')\n" + } + }, + { + "key": "exception.escaped", + "value": { + "stringValue": "False" + } + } + ] + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "8e84b2f5d3c9d733", + "parentSpanId": "4862a8dc9e239ae2", + "name": "tools", + "kind": 1, + "startTimeUnixNano": "1790968032471715072", + "endTimeUnixNano": "1790968032476058880", + "attributes": [ + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "tools" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "2" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "tools" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"__pregel_push\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_push\",0,false]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "verifier:544dea40-e16b-458c-1464-128e1b9b10a9|tools:74348550-0598-498b-1732-6675e778549b" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "verifier" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "verifier:544dea40-e16b-458c-1464-128e1b9b10a9" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "graph:step:2" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJpbnB1dCI6W3sibmFtZSI6InRyYW5zZmVyX3RvX3JlZF90ZWFtIiwiYXJncyI6e30sImlkIjoidG9vbHVfMDFYRUF1aFFIQ0JkeUN3dHltWndGZkRpIiwidHlwZSI6InRvb2xfY2FsbCJ9XX0=" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "e30=" + } + } + ], + "events": [ + { + "timeUnixNano": "1790968032542335000", + "name": "exception", + "attributes": [ + { + "key": "exception.type", + "value": { + "stringValue": "Exception" + } + }, + { + "key": "exception.message", + "value": { + "stringValue": "ParentCommand(Command(graph='__parent__', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', id='389058d1-083f-4632-bbc5-09bfd3a0ffd2', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9'), AIMessage(content=[{'signature': 'CAQS1QgKEAgSGAI4AUIIdGhpbmtpbmcSDN6TfU6NDM971lEbzxoMvRUA1J9ox3fJQYT9IjArCQQGHBP4pRHPavXzRlbMbaYHC57Yj+Gbxgr0helvd2OIL/o6Rbt4PFA3FaIFUlgq8gc7KQI9FtGnvSAkLBRzIsUp/2d5Rd2zgEsfkFVeXsn8WYYZkinAQVVy6A7B3LaEJW0xb/x88Di67fedwx0Q+FlZtwcmT3Dcaa11JveyHRr3dm+zGSZP6CXeXNJ8nrTZ7W1YRTysD6w7o7Lp8h1Rwq4f4o8j+QUfY0vje+zJZsxHPmQNt3ykeKh99hgn6dbiz6/EHqmbHjSAu1M4SqEDfqIKThHYenamAZmcQ4XAOo6mdQ6+77VgpV1lC1/k52IiUoJjkGlQrnLCvutiI7KN+sgK8c2+2KCJrPRCvVV+BkStvP5jWWqzytpKQ+nHzhL448OrIA9gdJC39C8eDrXeRnI7t4FqlkXGoMlZAV9UMcrQHRxOMi8tOyTzi9oyBFDTSsQ+LJqer0tz1Ft9gek6vzg50SpVAFQdLERv8R1YqMaeI+xsvTAEW9MRgvRMkzLuzRT2guw3jD+Fjip37xgK5QI0R+CsOAmQPoTZizzcIyAUAuCJFCgq/gD/CibLxoymiVUI1an9Snt3KpNRoWdvWIne33V0pv8PHg9yW2JuLOqQGilLCsK/Q3oQr3upEMXxXGjCL7WUizvRZGFEEV+VplYt7v4tKZMj5pk3vdeM0MJ5XZg7PvUsRzycxcnrT3Knoc3qM+ttqZfTsb8d6U/pQifq7GuahFOHrvroqgJLj+/81Yy1pZFkjfJgY0XklzAnA6CFq/YT0gxbV+TH5IS+WaYOUzK36S8s5qqMwhngv4IzE3ctXXqnhIZXNznyVBqbYa/Gx2bCjcEj2n5RyGYRS/+MculgLBznQ9MkG2D7QP3Mq/nwHXKMIaLS8VK8hG3d0rAjlvnauXShNrJxtkj4qkwnxD4LrEjk1ue+PLcbLqYWwAr8n0s+41jrsIPVyyK9vFaEZrA9KaBxkJyiqx50GZNn7ckx+mkI18gH52iyVPtU8FKMXjugCl6mPLiHYiDQd+G63LudmOKys9itVnBDBNMP2b/I7+bIVRlheGaZWr6MATKl86B3y+0kuOrLRMyWcdibc2iQkZm+8f7dTUwbKbBJsKOFY73/8nlL/JiYpjz6nUIk+IqSjaFyK6XFlCcukWpWHpDJnSvbEzey7qHyCqJ+I+on8iP8GOtempQaS0/4kZGSwlrE6xOj6EwGpfjbDZKHkP/2RAdNYPQq3z7bdVJMJNnFMz6e37mzMpCBihwPdSFTRE3SCY66wvTgJzGCUyhHbSi5i1azzOXd8uHCSpT/6L1o6VU0pjoR8Kaa4wCWHGf7P/WEd+avyp02Jnu1cmpW0+Gq+vK9h0tdb2IClM/eNfdouQP+ep8MJUjraDRICmAJQY5obS71vniOo+reaFy0zRgB', 'thinking': '', 'type': 'thinking'}, {'signature': 'CAQS/REKEQgSGAI4AUIJbmFycmF0aW9uEgwFh3AVJZP3s8y1Hq0aDG76uhHONaScXAGShiIwGgjLXHUx1yZrkLPljjzTOQycpLJiKVwXcxm/EzKJm1s8M5WxuREWEldXcoaN0/msKpkRMT+eLb5ih9rAHULTomUNsUwJeu0jrJe4GLazy7sP5KtjEOEBKQsXUfoQ7fDkZOQUHZi9zzQN4hFNmYJUf3OVqf9TQNdC1+m//HAZPfWmAy6HtDvm7kgBJkCEoiFnm7tn/PX1hXGPRgWc6BTXUMr2WA8aKyqggRUbMH2+JYv5iWZddCc1up1Nbr3sF7WPcot9ZBhyHQXr692zeJk6SJgygIVrASbMly2bBxTWJB623gVgzpCmVSLZFpkKXgPcp8hGyDKOvF6SiQSHqGRxQofr7Vs1aPhfZixROJGD+MlrkggVAnGMyYIDUZpavtCeiU1JmBSz0084bXPtT/tnpsKOnSPYZQo3B0BXKbqA/WsKng8/V3Z8Uuajo6IVZ2hgA1WDduqQOQkExGU6pFuLhebIjuWxSNI/DffvR//LNqCfvdnf0hvoPtDxfx/SawIx4/spq9xbwFzHtLgoHg1F2wFw7clPDlg1PyD/v2yJG3V9T8GL6jGPE6rhfil/u4HNvf7BW0dQzCTTXcqZy4nByy0+p+S7ZxfMl4Kpxa2gOhhSNjfpfuX0QpL4kME8J1nliFN1d8KqEPuDI18BQHUhD/UfY29oA0x510FjM/BsLZ4n5sppkoIi7CFoQq2aAKVNfC1KilhIpRD+1dyDtZQklN8gZpOESHw6ML1TtU0yfAcbmqoxJ6hhjzwH6CMsTQEEy8MbRBM+QKKkIRdRCPHGFW2LkuR50+J0pF/tr6pqVvJeozfiMnrEilB27TZP4nKTJG8rd//vCWen0y7CA/8dBJ9gdp5tx7NVA45k1gdcxER1gYouNX3n1vG3/B1F4+93wXW9Mqo6Us59TQmrb9Nqa7pSQNSrH06w1GxyBAlkFfqAlafk16aMpq416TJYpGy43WCUiCcLWlhiWgz2sgOLAXP9a5weWAKRC8J6jkcUkDeXnW+HFSiPM6+9Bo6kVhLUhN5rc4mHZOZpF7ptPpsQN4XCqLPsqpRH/8o5XpyC6ZneTeF2ExtjhwZVjXDwMbIzVYIbw1y2PPGw8dC7pc4sIVSnI4oQAH0v650ghfhmMevWW4h4Pylbw/DG4fdeT3+8z0f8MOSTtfipx05/QttHlAvODRxa5yDCvOz6n3f1BO0N/4MbGyOM27qW4x8mBVoryUZWfsmhfi0/GfNvB8+ylTGbYNX27OcEF5WZtNqbWEy7V5GbmZUSxspcg7FBZ3j3/3L7muvZXoZi1f5erq4y3lSH+oF7qPFnvieyLOCwubcQy+oyowj4cD0idRecnp+s+52D7+mzF1gpW+f+cJasoxigbxkOloS4yeiKvgM4nWSpGEj12RPx5o80EmziXN9t4zJzW1htA5hXJJeFympMYtyCUCevRWVmrRYktQ7UnCa6tL/AVPi1ni5XCXHfLOuqfK5KLYxjwVtdhhfmCLvpaKVNCEOCf4nNGHqD98wUb0ejWNwDYBnJGkBRw9RVspfwNdoWoVcn2qv8ywGUp7yc/sk2LpI6VziRyvWqbGNG1ZJqVVutRc46+sOE4VSeS2/CTNKNa6NMCf/hxxaUVsYfKdP+S1Lc4ianKp0sUugPpmtoydC0CP2RwjnHLcnzxCnvqxlLQWXMNPTsu2yBcHlWJLtcmdg/McLagy7l5kG6sN7ZxJLD/kAx8E6uqohP+K/jMSrYuQJnhr4tf4biWFY3Jzf/KpAYyVSq+pbfFKXMbL9nHEoZQT15Wyabd6DMAq3QZOthM5pUzdNqUXkcrI1PwjASAg4dVU/2o+98mTXnyhP7VlEkTnddpwyJF+YCCc01pqa6ZX99pnR+YOpIrb5DtPcKq0t5cFCDyQxN1lUuqZcYujvX/GC7+6IpRTz/NkEY/5cntOfZJyd6SkUx/JR5AJIRPCNeib0rcWnnFn0XK8s22x40wLjEtufXDO9+yraf4kHxlBI3SB0meMjepX50CJhAx7NekkRFnwoecLfqIyrjXrgAZsTMPiX94snjdrzisAXtBKanulilSxOfW2HSmfnk0L2GXHJJ3S+vCbgREml9dekfmHhmwEYtv3kfhth7ERIwVT8j69OtIBCmeTIJuA7o73PXc1LJ2whuZK7FZe6NTOs61MP7JVlIc6tbhtOBk3NXU8pBPOCY6Q55d63FtpUU/KCbQ0K2WQ9o5jNGwNNJ6Cw5FDCt+x5YJeP1d2QzDSiq+cSZqwqCZVcNoAftGGzgrRZ4vkpbP0xQZHCtWsUwHVTjrY4NfRDjHdVPRn8J9Xtooc/hIWkqzFaczYTlapu2RCJqe8aEo3jzWZCEJ7AiBzBYraYDgOfeiQyeJUsbbXHdGKiiw22Bdp4M+Yz671MZ0ie9EWRks0Q2DHGUTne9S4tAAJPGbUf4xtCMoJ40Q4QW05QLSZy3wNFAUFYmXE4ECDFB8+IEeNFFF/tgAnfh8GvFh/1xY+LE+ozMminjSRwhq09zQAEkNFyu0IKgvDkyOI+iccF7doWLTUasPxLH1xGPBSKkeWCnSoX/4C2c9du66BbRXamHqae+VCiS6BN953iYKOu8rMkneW+TFjV0CzAf8PNG9LBayUgY5Uw9x0Sap6zI4PpaZKH4d9ktkFU+91yBbN5SRtdd8/EQatJXmBNfw7kRrll+LjwZweWLEb0jT1RpGuDqHLdLTMMgowSczrp2zCsJdQH/seaYrpwkCFR8dRjM7JRUkVa1A0SvdZAX75vuBvvLBzN2c+Rbt5oqy93lqIBIjKb7w2UkRTb4pBWcNKNPUgt3az/PxQxEasaltdmuyvsXYbur3VPNn8JzN/6J5N+QCnz571TccapCmkPvFE+gayXU+UF1VKYyFcUvlev7lsQlmjqzK2QA8X7Q3TzFapqhvJKlt7dzBHraqZBgPgb9N4oKNayi8FZT71+kaYhIhFrESKYRHfHkj5n3kfhgLy+x+jLDbKxMggkgHnJz9RuEHZXBWtkYAQ==', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_verifier', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAMRdS21FeJfzQhM3s', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7396, 'output_tokens': 770, 'output_tokens_details': {'thinking_tokens': 739}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='skeptic', id='lc_run--01a0fe03-4ae9-76c0-89a6-4f0b114e3030-0', tool_calls=[{'name': 'transfer_to_verifier', 'args': {}, 'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7396, 'output_tokens': 770, 'total_tokens': 8166, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 739}}), ToolMessage(content='Successfully transferred to verifier', name='transfer_to_verifier', id='c064563a-974f-4f0f-ab4a-080e63c01cf5', tool_call_id='toolu_01H6fV8JvRJNvXy47eRnd3jH'), AIMessage(content=[{'signature': 'CAQS9gUKEAgSGAI4AUIIdGhpbmtpbmcSDGFyJ1MXoY+Ai6NkbxoMRd2TrUfUU7WrPpA2IjDgy8dUplcZKc7DnJwNLTBfH5UNR+nGnakSPQia8UVIzrjkOnJ2fLrWUsA2oEO8yRsqkwXkkDCsyhB97dwj57RMt91XzgWhL9luNEGO92yW0rNFGoMSUVOxPIrFc5RjyrhKEc3+1RnpxCkylq+LNlQ5NFxLn6v2cF5DzqpvBHNR7XAig+urJIU3gXydCkaGZZDetJtQOUOXgZi9z9100NpdAEkXDo2ICHCGTGpVc0J/pbXxWRBxb6B1/Gl0Yjn68V6HlesH7IoC7GR/3HwRu9NSDBG4ttfQjEWcg+PeAea4Rn7jzx+Tm9H7L1joTKVgDbQg5iKMLfjvPqlnksCLtXOt8jSIS5OYLN7a/7TX6/w5d34vnw8vPDmU1UYi0Q72qUWhUa2vw8OXNpcKL7Bp0lNfBS8rvHY+vBY8/haAoTL3wvLFU1fT+4z/vA3BcsigZDYlfJeDYcxXmRuqxzxEGpXCRe9s1HWtID+41pHu6lpZHkXaghpc6TZHLxNwcVhTb6fMQ7Qo+2L6Pvklh1m32peodxeg2YiPhdpl3w/xkOEu5/JEF7ZBxpdUL+u3wkO5pUlsqfgX7uhzv7HC2WpSWVimrwiQwQOUx0+JPrLtw+lYfq7I3jruo0GW0guWvnuf7JM3YLj84q+JgHomnSSZnnZmrL0UaRvrVyLo877F39ccLS7XoZnnui8J4P58ZTmiLkEJeGcx1Iuag2e33339O7Yew/59tLbQtIGh9VWMwHBnFx39S8/4LEFrK5HldPQhOrloyTtkRs66iQRKtxMKS0VI6QTKZDAbs7a7H1yBv+mJi9Xg4uKmyU6qnn7zEKjjY2bIlCwezx4LLGP5HD3Zr6fZsmdG1UHzDsBvYG2oCI0oqEKM+valsT6hU8MRjbQBD/JJ8JJN/iBGlhBAUHo0UmhhtR2euTVpZWlMnhmpXAdKDXSfPdaPohgB', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_red_team', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAsabDquegHi6YUpCn', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7509, 'output_tokens': 88, 'output_tokens_details': {'thinking_tokens': 55}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='verifier', id='lc_run--01a0fe03-6682-7d21-8c15-ffbce9422c9f-0', tool_calls=[{'name': 'transfer_to_red_team', 'args': {}, 'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7509, 'output_tokens': 88, 'total_tokens': 7597, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 55}}), ToolMessage(content='Successfully transferred to red_team', name='transfer_to_red_team', tool_call_id='toolu_01XEAuhQHCBdyCwtymZwFfDi')], 'active_agent': 'red_team'}, goto='red_team'))Traceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 709, in invoke\n input = step.invoke(input, config)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 447, in invoke\n ret = self.func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 84, in _write\n self.do_write(\n ~~~~~~~~~~~~~^\n config,\n ^^^^^^^\n writes,\n ^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 126, in do_write\n write(_assemble_writes(writes))\n ~~~~~~~~~~~~~~~~^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 181, in _assemble_writes\n if ww := w.mapper(w.value):\n ~~~~~~~~^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/graph/state.py\", line 1762, in _control_branch\n raise ParentCommand(command)\n\n\nlanggraph.errors.ParentCommand: Command(graph='__parent__', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', id='389058d1-083f-4632-bbc5-09bfd3a0ffd2', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9'), AIMessage(content=[{'signature': 'CAQS1QgKEAgSGAI4AUIIdGhpbmtpbmcSDN6TfU6NDM971lEbzxoMvRUA1J9ox3fJQYT9IjArCQQGHBP4pRHPavXzRlbMbaYHC57Yj+Gbxgr0helvd2OIL/o6Rbt4PFA3FaIFUlgq8gc7KQI9FtGnvSAkLBRzIsUp/2d5Rd2zgEsfkFVeXsn8WYYZkinAQVVy6A7B3LaEJW0xb/x88Di67fedwx0Q+FlZtwcmT3Dcaa11JveyHRr3dm+zGSZP6CXeXNJ8nrTZ7W1YRTysD6w7o7Lp8h1Rwq4f4o8j+QUfY0vje+zJZsxHPmQNt3ykeKh99hgn6dbiz6/EHqmbHjSAu1M4SqEDfqIKThHYenamAZmcQ4XAOo6mdQ6+77VgpV1lC1/k52IiUoJjkGlQrnLCvutiI7KN+sgK8c2+2KCJrPRCvVV+BkStvP5jWWqzytpKQ+nHzhL448OrIA9gdJC39C8eDrXeRnI7t4FqlkXGoMlZAV9UMcrQHRxOMi8tOyTzi9oyBFDTSsQ+LJqer0tz1Ft9gek6vzg50SpVAFQdLERv8R1YqMaeI+xsvTAEW9MRgvRMkzLuzRT2guw3jD+Fjip37xgK5QI0R+CsOAmQPoTZizzcIyAUAuCJFCgq/gD/CibLxoymiVUI1an9Snt3KpNRoWdvWIne33V0pv8PHg9yW2JuLOqQGilLCsK/Q3oQr3upEMXxXGjCL7WUizvRZGFEEV+VplYt7v4tKZMj5pk3vdeM0MJ5XZg7PvUsRzycxcnrT3Knoc3qM+ttqZfTsb8d6U/pQifq7GuahFOHrvroqgJLj+/81Yy1pZFkjfJgY0XklzAnA6CFq/YT0gxbV+TH5IS+WaYOUzK36S8s5qqMwhngv4IzE3ctXXqnhIZXNznyVBqbYa/Gx2bCjcEj2n5RyGYRS/+MculgLBznQ9MkG2D7QP3Mq/nwHXKMIaLS8VK8hG3d0rAjlvnauXShNrJxtkj4qkwnxD4LrEjk1ue+PLcbLqYWwAr8n0s+41jrsIPVyyK9vFaEZrA9KaBxkJyiqx50GZNn7ckx+mkI18gH52iyVPtU8FKMXjugCl6mPLiHYiDQd+G63LudmOKys9itVnBDBNMP2b/I7+bIVRlheGaZWr6MATKl86B3y+0kuOrLRMyWcdibc2iQkZm+8f7dTUwbKbBJsKOFY73/8nlL/JiYpjz6nUIk+IqSjaFyK6XFlCcukWpWHpDJnSvbEzey7qHyCqJ+I+on8iP8GOtempQaS0/4kZGSwlrE6xOj6EwGpfjbDZKHkP/2RAdNYPQq3z7bdVJMJNnFMz6e37mzMpCBihwPdSFTRE3SCY66wvTgJzGCUyhHbSi5i1azzOXd8uHCSpT/6L1o6VU0pjoR8Kaa4wCWHGf7P/WEd+avyp02Jnu1cmpW0+Gq+vK9h0tdb2IClM/eNfdouQP+ep8MJUjraDRICmAJQY5obS71vniOo+reaFy0zRgB', 'thinking': '', 'type': 'thinking'}, {'signature': 'CAQS/REKEQgSGAI4AUIJbmFycmF0aW9uEgwFh3AVJZP3s8y1Hq0aDG76uhHONaScXAGShiIwGgjLXHUx1yZrkLPljjzTOQycpLJiKVwXcxm/EzKJm1s8M5WxuREWEldXcoaN0/msKpkRMT+eLb5ih9rAHULTomUNsUwJeu0jrJe4GLazy7sP5KtjEOEBKQsXUfoQ7fDkZOQUHZi9zzQN4hFNmYJUf3OVqf9TQNdC1+m//HAZPfWmAy6HtDvm7kgBJkCEoiFnm7tn/PX1hXGPRgWc6BTXUMr2WA8aKyqggRUbMH2+JYv5iWZddCc1up1Nbr3sF7WPcot9ZBhyHQXr692zeJk6SJgygIVrASbMly2bBxTWJB623gVgzpCmVSLZFpkKXgPcp8hGyDKOvF6SiQSHqGRxQofr7Vs1aPhfZixROJGD+MlrkggVAnGMyYIDUZpavtCeiU1JmBSz0084bXPtT/tnpsKOnSPYZQo3B0BXKbqA/WsKng8/V3Z8Uuajo6IVZ2hgA1WDduqQOQkExGU6pFuLhebIjuWxSNI/DffvR//LNqCfvdnf0hvoPtDxfx/SawIx4/spq9xbwFzHtLgoHg1F2wFw7clPDlg1PyD/v2yJG3V9T8GL6jGPE6rhfil/u4HNvf7BW0dQzCTTXcqZy4nByy0+p+S7ZxfMl4Kpxa2gOhhSNjfpfuX0QpL4kME8J1nliFN1d8KqEPuDI18BQHUhD/UfY29oA0x510FjM/BsLZ4n5sppkoIi7CFoQq2aAKVNfC1KilhIpRD+1dyDtZQklN8gZpOESHw6ML1TtU0yfAcbmqoxJ6hhjzwH6CMsTQEEy8MbRBM+QKKkIRdRCPHGFW2LkuR50+J0pF/tr6pqVvJeozfiMnrEilB27TZP4nKTJG8rd//vCWen0y7CA/8dBJ9gdp5tx7NVA45k1gdcxER1gYouNX3n1vG3/B1F4+93wXW9Mqo6Us59TQmrb9Nqa7pSQNSrH06w1GxyBAlkFfqAlafk16aMpq416TJYpGy43WCUiCcLWlhiWgz2sgOLAXP9a5weWAKRC8J6jkcUkDeXnW+HFSiPM6+9Bo6kVhLUhN5rc4mHZOZpF7ptPpsQN4XCqLPsqpRH/8o5XpyC6ZneTeF2ExtjhwZVjXDwMbIzVYIbw1y2PPGw8dC7pc4sIVSnI4oQAH0v650ghfhmMevWW4h4Pylbw/DG4fdeT3+8z0f8MOSTtfipx05/QttHlAvODRxa5yDCvOz6n3f1BO0N/4MbGyOM27qW4x8mBVoryUZWfsmhfi0/GfNvB8+ylTGbYNX27OcEF5WZtNqbWEy7V5GbmZUSxspcg7FBZ3j3/3L7muvZXoZi1f5erq4y3lSH+oF7qPFnvieyLOCwubcQy+oyowj4cD0idRecnp+s+52D7+mzF1gpW+f+cJasoxigbxkOloS4yeiKvgM4nWSpGEj12RPx5o80EmziXN9t4zJzW1htA5hXJJeFympMYtyCUCevRWVmrRYktQ7UnCa6tL/AVPi1ni5XCXHfLOuqfK5KLYxjwVtdhhfmCLvpaKVNCEOCf4nNGHqD98wUb0ejWNwDYBnJGkBRw9RVspfwNdoWoVcn2qv8ywGUp7yc/sk2LpI6VziRyvWqbGNG1ZJqVVutRc46+sOE4VSeS2/CTNKNa6NMCf/hxxaUVsYfKdP+S1Lc4ianKp0sUugPpmtoydC0CP2RwjnHLcnzxCnvqxlLQWXMNPTsu2yBcHlWJLtcmdg/McLagy7l5kG6sN7ZxJLD/kAx8E6uqohP+K/jMSrYuQJnhr4tf4biWFY3Jzf/KpAYyVSq+pbfFKXMbL9nHEoZQT15Wyabd6DMAq3QZOthM5pUzdNqUXkcrI1PwjASAg4dVU/2o+98mTXnyhP7VlEkTnddpwyJF+YCCc01pqa6ZX99pnR+YOpIrb5DtPcKq0t5cFCDyQxN1lUuqZcYujvX/GC7+6IpRTz/NkEY/5cntOfZJyd6SkUx/JR5AJIRPCNeib0rcWnnFn0XK8s22x40wLjEtufXDO9+yraf4kHxlBI3SB0meMjepX50CJhAx7NekkRFnwoecLfqIyrjXrgAZsTMPiX94snjdrzisAXtBKanulilSxOfW2HSmfnk0L2GXHJJ3S+vCbgREml9dekfmHhmwEYtv3kfhth7ERIwVT8j69OtIBCmeTIJuA7o73PXc1LJ2whuZK7FZe6NTOs61MP7JVlIc6tbhtOBk3NXU8pBPOCY6Q55d63FtpUU/KCbQ0K2WQ9o5jNGwNNJ6Cw5FDCt+x5YJeP1d2QzDSiq+cSZqwqCZVcNoAftGGzgrRZ4vkpbP0xQZHCtWsUwHVTjrY4NfRDjHdVPRn8J9Xtooc/hIWkqzFaczYTlapu2RCJqe8aEo3jzWZCEJ7AiBzBYraYDgOfeiQyeJUsbbXHdGKiiw22Bdp4M+Yz671MZ0ie9EWRks0Q2DHGUTne9S4tAAJPGbUf4xtCMoJ40Q4QW05QLSZy3wNFAUFYmXE4ECDFB8+IEeNFFF/tgAnfh8GvFh/1xY+LE+ozMminjSRwhq09zQAEkNFyu0IKgvDkyOI+iccF7doWLTUasPxLH1xGPBSKkeWCnSoX/4C2c9du66BbRXamHqae+VCiS6BN953iYKOu8rMkneW+TFjV0CzAf8PNG9LBayUgY5Uw9x0Sap6zI4PpaZKH4d9ktkFU+91yBbN5SRtdd8/EQatJXmBNfw7kRrll+LjwZweWLEb0jT1RpGuDqHLdLTMMgowSczrp2zCsJdQH/seaYrpwkCFR8dRjM7JRUkVa1A0SvdZAX75vuBvvLBzN2c+Rbt5oqy93lqIBIjKb7w2UkRTb4pBWcNKNPUgt3az/PxQxEasaltdmuyvsXYbur3VPNn8JzN/6J5N+QCnz571TccapCmkPvFE+gayXU+UF1VKYyFcUvlev7lsQlmjqzK2QA8X7Q3TzFapqhvJKlt7dzBHraqZBgPgb9N4oKNayi8FZT71+kaYhIhFrESKYRHfHkj5n3kfhgLy+x+jLDbKxMggkgHnJz9RuEHZXBWtkYAQ==', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_verifier', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAMRdS21FeJfzQhM3s', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7396, 'output_tokens': 770, 'output_tokens_details': {'thinking_tokens': 739}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='skeptic', id='lc_run--01a0fe03-4ae9-76c0-89a6-4f0b114e3030-0', tool_calls=[{'name': 'transfer_to_verifier', 'args': {}, 'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7396, 'output_tokens': 770, 'total_tokens': 8166, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 739}}), ToolMessage(content='Successfully transferred to verifier', name='transfer_to_verifier', id='c064563a-974f-4f0f-ab4a-080e63c01cf5', tool_call_id='toolu_01H6fV8JvRJNvXy47eRnd3jH'), AIMessage(content=[{'signature': 'CAQS9gUKEAgSGAI4AUIIdGhpbmtpbmcSDGFyJ1MXoY+Ai6NkbxoMRd2TrUfUU7WrPpA2IjDgy8dUplcZKc7DnJwNLTBfH5UNR+nGnakSPQia8UVIzrjkOnJ2fLrWUsA2oEO8yRsqkwXkkDCsyhB97dwj57RMt91XzgWhL9luNEGO92yW0rNFGoMSUVOxPIrFc5RjyrhKEc3+1RnpxCkylq+LNlQ5NFxLn6v2cF5DzqpvBHNR7XAig+urJIU3gXydCkaGZZDetJtQOUOXgZi9z9100NpdAEkXDo2ICHCGTGpVc0J/pbXxWRBxb6B1/Gl0Yjn68V6HlesH7IoC7GR/3HwRu9NSDBG4ttfQjEWcg+PeAea4Rn7jzx+Tm9H7L1joTKVgDbQg5iKMLfjvPqlnksCLtXOt8jSIS5OYLN7a/7TX6/w5d34vnw8vPDmU1UYi0Q72qUWhUa2vw8OXNpcKL7Bp0lNfBS8rvHY+vBY8/haAoTL3wvLFU1fT+4z/vA3BcsigZDYlfJeDYcxXmRuqxzxEGpXCRe9s1HWtID+41pHu6lpZHkXaghpc6TZHLxNwcVhTb6fMQ7Qo+2L6Pvklh1m32peodxeg2YiPhdpl3w/xkOEu5/JEF7ZBxpdUL+u3wkO5pUlsqfgX7uhzv7HC2WpSWVimrwiQwQOUx0+JPrLtw+lYfq7I3jruo0GW0guWvnuf7JM3YLj84q+JgHomnSSZnnZmrL0UaRvrVyLo877F39ccLS7XoZnnui8J4P58ZTmiLkEJeGcx1Iuag2e33339O7Yew/59tLbQtIGh9VWMwHBnFx39S8/4LEFrK5HldPQhOrloyTtkRs66iQRKtxMKS0VI6QTKZDAbs7a7H1yBv+mJi9Xg4uKmyU6qnn7zEKjjY2bIlCwezx4LLGP5HD3Zr6fZsmdG1UHzDsBvYG2oCI0oqEKM+valsT6hU8MRjbQBD/JJ8JJN/iBGlhBAUHo0UmhhtR2euTVpZWlMnhmpXAdKDXSfPdaPohgB', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_red_team', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAsabDquegHi6YUpCn', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7509, 'output_tokens': 88, 'output_tokens_details': {'thinking_tokens': 55}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='verifier', id='lc_run--01a0fe03-6682-7d21-8c15-ffbce9422c9f-0', tool_calls=[{'name': 'transfer_to_red_team', 'args': {}, 'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7509, 'output_tokens': 88, 'total_tokens': 7597, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 55}}), ToolMessage(content='Successfully transferred to red_team', name='transfer_to_red_team', tool_call_id='toolu_01XEAuhQHCBdyCwtymZwFfDi')], 'active_agent': 'red_team'}, goto='red_team')" + } + }, + { + "key": "exception.stacktrace", + "value": { + "stringValue": "Exception: ParentCommand(Command(graph='__parent__', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', id='389058d1-083f-4632-bbc5-09bfd3a0ffd2', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9'), AIMessage(content=[{'signature': 'CAQS1QgKEAgSGAI4AUIIdGhpbmtpbmcSDN6TfU6NDM971lEbzxoMvRUA1J9ox3fJQYT9IjArCQQGHBP4pRHPavXzRlbMbaYHC57Yj+Gbxgr0helvd2OIL/o6Rbt4PFA3FaIFUlgq8gc7KQI9FtGnvSAkLBRzIsUp/2d5Rd2zgEsfkFVeXsn8WYYZkinAQVVy6A7B3LaEJW0xb/x88Di67fedwx0Q+FlZtwcmT3Dcaa11JveyHRr3dm+zGSZP6CXeXNJ8nrTZ7W1YRTysD6w7o7Lp8h1Rwq4f4o8j+QUfY0vje+zJZsxHPmQNt3ykeKh99hgn6dbiz6/EHqmbHjSAu1M4SqEDfqIKThHYenamAZmcQ4XAOo6mdQ6+77VgpV1lC1/k52IiUoJjkGlQrnLCvutiI7KN+sgK8c2+2KCJrPRCvVV+BkStvP5jWWqzytpKQ+nHzhL448OrIA9gdJC39C8eDrXeRnI7t4FqlkXGoMlZAV9UMcrQHRxOMi8tOyTzi9oyBFDTSsQ+LJqer0tz1Ft9gek6vzg50SpVAFQdLERv8R1YqMaeI+xsvTAEW9MRgvRMkzLuzRT2guw3jD+Fjip37xgK5QI0R+CsOAmQPoTZizzcIyAUAuCJFCgq/gD/CibLxoymiVUI1an9Snt3KpNRoWdvWIne33V0pv8PHg9yW2JuLOqQGilLCsK/Q3oQr3upEMXxXGjCL7WUizvRZGFEEV+VplYt7v4tKZMj5pk3vdeM0MJ5XZg7PvUsRzycxcnrT3Knoc3qM+ttqZfTsb8d6U/pQifq7GuahFOHrvroqgJLj+/81Yy1pZFkjfJgY0XklzAnA6CFq/YT0gxbV+TH5IS+WaYOUzK36S8s5qqMwhngv4IzE3ctXXqnhIZXNznyVBqbYa/Gx2bCjcEj2n5RyGYRS/+MculgLBznQ9MkG2D7QP3Mq/nwHXKMIaLS8VK8hG3d0rAjlvnauXShNrJxtkj4qkwnxD4LrEjk1ue+PLcbLqYWwAr8n0s+41jrsIPVyyK9vFaEZrA9KaBxkJyiqx50GZNn7ckx+mkI18gH52iyVPtU8FKMXjugCl6mPLiHYiDQd+G63LudmOKys9itVnBDBNMP2b/I7+bIVRlheGaZWr6MATKl86B3y+0kuOrLRMyWcdibc2iQkZm+8f7dTUwbKbBJsKOFY73/8nlL/JiYpjz6nUIk+IqSjaFyK6XFlCcukWpWHpDJnSvbEzey7qHyCqJ+I+on8iP8GOtempQaS0/4kZGSwlrE6xOj6EwGpfjbDZKHkP/2RAdNYPQq3z7bdVJMJNnFMz6e37mzMpCBihwPdSFTRE3SCY66wvTgJzGCUyhHbSi5i1azzOXd8uHCSpT/6L1o6VU0pjoR8Kaa4wCWHGf7P/WEd+avyp02Jnu1cmpW0+Gq+vK9h0tdb2IClM/eNfdouQP+ep8MJUjraDRICmAJQY5obS71vniOo+reaFy0zRgB', 'thinking': '', 'type': 'thinking'}, {'signature': 'CAQS/REKEQgSGAI4AUIJbmFycmF0aW9uEgwFh3AVJZP3s8y1Hq0aDG76uhHONaScXAGShiIwGgjLXHUx1yZrkLPljjzTOQycpLJiKVwXcxm/EzKJm1s8M5WxuREWEldXcoaN0/msKpkRMT+eLb5ih9rAHULTomUNsUwJeu0jrJe4GLazy7sP5KtjEOEBKQsXUfoQ7fDkZOQUHZi9zzQN4hFNmYJUf3OVqf9TQNdC1+m//HAZPfWmAy6HtDvm7kgBJkCEoiFnm7tn/PX1hXGPRgWc6BTXUMr2WA8aKyqggRUbMH2+JYv5iWZddCc1up1Nbr3sF7WPcot9ZBhyHQXr692zeJk6SJgygIVrASbMly2bBxTWJB623gVgzpCmVSLZFpkKXgPcp8hGyDKOvF6SiQSHqGRxQofr7Vs1aPhfZixROJGD+MlrkggVAnGMyYIDUZpavtCeiU1JmBSz0084bXPtT/tnpsKOnSPYZQo3B0BXKbqA/WsKng8/V3Z8Uuajo6IVZ2hgA1WDduqQOQkExGU6pFuLhebIjuWxSNI/DffvR//LNqCfvdnf0hvoPtDxfx/SawIx4/spq9xbwFzHtLgoHg1F2wFw7clPDlg1PyD/v2yJG3V9T8GL6jGPE6rhfil/u4HNvf7BW0dQzCTTXcqZy4nByy0+p+S7ZxfMl4Kpxa2gOhhSNjfpfuX0QpL4kME8J1nliFN1d8KqEPuDI18BQHUhD/UfY29oA0x510FjM/BsLZ4n5sppkoIi7CFoQq2aAKVNfC1KilhIpRD+1dyDtZQklN8gZpOESHw6ML1TtU0yfAcbmqoxJ6hhjzwH6CMsTQEEy8MbRBM+QKKkIRdRCPHGFW2LkuR50+J0pF/tr6pqVvJeozfiMnrEilB27TZP4nKTJG8rd//vCWen0y7CA/8dBJ9gdp5tx7NVA45k1gdcxER1gYouNX3n1vG3/B1F4+93wXW9Mqo6Us59TQmrb9Nqa7pSQNSrH06w1GxyBAlkFfqAlafk16aMpq416TJYpGy43WCUiCcLWlhiWgz2sgOLAXP9a5weWAKRC8J6jkcUkDeXnW+HFSiPM6+9Bo6kVhLUhN5rc4mHZOZpF7ptPpsQN4XCqLPsqpRH/8o5XpyC6ZneTeF2ExtjhwZVjXDwMbIzVYIbw1y2PPGw8dC7pc4sIVSnI4oQAH0v650ghfhmMevWW4h4Pylbw/DG4fdeT3+8z0f8MOSTtfipx05/QttHlAvODRxa5yDCvOz6n3f1BO0N/4MbGyOM27qW4x8mBVoryUZWfsmhfi0/GfNvB8+ylTGbYNX27OcEF5WZtNqbWEy7V5GbmZUSxspcg7FBZ3j3/3L7muvZXoZi1f5erq4y3lSH+oF7qPFnvieyLOCwubcQy+oyowj4cD0idRecnp+s+52D7+mzF1gpW+f+cJasoxigbxkOloS4yeiKvgM4nWSpGEj12RPx5o80EmziXN9t4zJzW1htA5hXJJeFympMYtyCUCevRWVmrRYktQ7UnCa6tL/AVPi1ni5XCXHfLOuqfK5KLYxjwVtdhhfmCLvpaKVNCEOCf4nNGHqD98wUb0ejWNwDYBnJGkBRw9RVspfwNdoWoVcn2qv8ywGUp7yc/sk2LpI6VziRyvWqbGNG1ZJqVVutRc46+sOE4VSeS2/CTNKNa6NMCf/hxxaUVsYfKdP+S1Lc4ianKp0sUugPpmtoydC0CP2RwjnHLcnzxCnvqxlLQWXMNPTsu2yBcHlWJLtcmdg/McLagy7l5kG6sN7ZxJLD/kAx8E6uqohP+K/jMSrYuQJnhr4tf4biWFY3Jzf/KpAYyVSq+pbfFKXMbL9nHEoZQT15Wyabd6DMAq3QZOthM5pUzdNqUXkcrI1PwjASAg4dVU/2o+98mTXnyhP7VlEkTnddpwyJF+YCCc01pqa6ZX99pnR+YOpIrb5DtPcKq0t5cFCDyQxN1lUuqZcYujvX/GC7+6IpRTz/NkEY/5cntOfZJyd6SkUx/JR5AJIRPCNeib0rcWnnFn0XK8s22x40wLjEtufXDO9+yraf4kHxlBI3SB0meMjepX50CJhAx7NekkRFnwoecLfqIyrjXrgAZsTMPiX94snjdrzisAXtBKanulilSxOfW2HSmfnk0L2GXHJJ3S+vCbgREml9dekfmHhmwEYtv3kfhth7ERIwVT8j69OtIBCmeTIJuA7o73PXc1LJ2whuZK7FZe6NTOs61MP7JVlIc6tbhtOBk3NXU8pBPOCY6Q55d63FtpUU/KCbQ0K2WQ9o5jNGwNNJ6Cw5FDCt+x5YJeP1d2QzDSiq+cSZqwqCZVcNoAftGGzgrRZ4vkpbP0xQZHCtWsUwHVTjrY4NfRDjHdVPRn8J9Xtooc/hIWkqzFaczYTlapu2RCJqe8aEo3jzWZCEJ7AiBzBYraYDgOfeiQyeJUsbbXHdGKiiw22Bdp4M+Yz671MZ0ie9EWRks0Q2DHGUTne9S4tAAJPGbUf4xtCMoJ40Q4QW05QLSZy3wNFAUFYmXE4ECDFB8+IEeNFFF/tgAnfh8GvFh/1xY+LE+ozMminjSRwhq09zQAEkNFyu0IKgvDkyOI+iccF7doWLTUasPxLH1xGPBSKkeWCnSoX/4C2c9du66BbRXamHqae+VCiS6BN953iYKOu8rMkneW+TFjV0CzAf8PNG9LBayUgY5Uw9x0Sap6zI4PpaZKH4d9ktkFU+91yBbN5SRtdd8/EQatJXmBNfw7kRrll+LjwZweWLEb0jT1RpGuDqHLdLTMMgowSczrp2zCsJdQH/seaYrpwkCFR8dRjM7JRUkVa1A0SvdZAX75vuBvvLBzN2c+Rbt5oqy93lqIBIjKb7w2UkRTb4pBWcNKNPUgt3az/PxQxEasaltdmuyvsXYbur3VPNn8JzN/6J5N+QCnz571TccapCmkPvFE+gayXU+UF1VKYyFcUvlev7lsQlmjqzK2QA8X7Q3TzFapqhvJKlt7dzBHraqZBgPgb9N4oKNayi8FZT71+kaYhIhFrESKYRHfHkj5n3kfhgLy+x+jLDbKxMggkgHnJz9RuEHZXBWtkYAQ==', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_verifier', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAMRdS21FeJfzQhM3s', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7396, 'output_tokens': 770, 'output_tokens_details': {'thinking_tokens': 739}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='skeptic', id='lc_run--01a0fe03-4ae9-76c0-89a6-4f0b114e3030-0', tool_calls=[{'name': 'transfer_to_verifier', 'args': {}, 'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7396, 'output_tokens': 770, 'total_tokens': 8166, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 739}}), ToolMessage(content='Successfully transferred to verifier', name='transfer_to_verifier', id='c064563a-974f-4f0f-ab4a-080e63c01cf5', tool_call_id='toolu_01H6fV8JvRJNvXy47eRnd3jH'), AIMessage(content=[{'signature': 'CAQS9gUKEAgSGAI4AUIIdGhpbmtpbmcSDGFyJ1MXoY+Ai6NkbxoMRd2TrUfUU7WrPpA2IjDgy8dUplcZKc7DnJwNLTBfH5UNR+nGnakSPQia8UVIzrjkOnJ2fLrWUsA2oEO8yRsqkwXkkDCsyhB97dwj57RMt91XzgWhL9luNEGO92yW0rNFGoMSUVOxPIrFc5RjyrhKEc3+1RnpxCkylq+LNlQ5NFxLn6v2cF5DzqpvBHNR7XAig+urJIU3gXydCkaGZZDetJtQOUOXgZi9z9100NpdAEkXDo2ICHCGTGpVc0J/pbXxWRBxb6B1/Gl0Yjn68V6HlesH7IoC7GR/3HwRu9NSDBG4ttfQjEWcg+PeAea4Rn7jzx+Tm9H7L1joTKVgDbQg5iKMLfjvPqlnksCLtXOt8jSIS5OYLN7a/7TX6/w5d34vnw8vPDmU1UYi0Q72qUWhUa2vw8OXNpcKL7Bp0lNfBS8rvHY+vBY8/haAoTL3wvLFU1fT+4z/vA3BcsigZDYlfJeDYcxXmRuqxzxEGpXCRe9s1HWtID+41pHu6lpZHkXaghpc6TZHLxNwcVhTb6fMQ7Qo+2L6Pvklh1m32peodxeg2YiPhdpl3w/xkOEu5/JEF7ZBxpdUL+u3wkO5pUlsqfgX7uhzv7HC2WpSWVimrwiQwQOUx0+JPrLtw+lYfq7I3jruo0GW0guWvnuf7JM3YLj84q+JgHomnSSZnnZmrL0UaRvrVyLo877F39ccLS7XoZnnui8J4P58ZTmiLkEJeGcx1Iuag2e33339O7Yew/59tLbQtIGh9VWMwHBnFx39S8/4LEFrK5HldPQhOrloyTtkRs66iQRKtxMKS0VI6QTKZDAbs7a7H1yBv+mJi9Xg4uKmyU6qnn7zEKjjY2bIlCwezx4LLGP5HD3Zr6fZsmdG1UHzDsBvYG2oCI0oqEKM+valsT6hU8MRjbQBD/JJ8JJN/iBGlhBAUHo0UmhhtR2euTVpZWlMnhmpXAdKDXSfPdaPohgB', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_red_team', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAsabDquegHi6YUpCn', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7509, 'output_tokens': 88, 'output_tokens_details': {'thinking_tokens': 55}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='verifier', id='lc_run--01a0fe03-6682-7d21-8c15-ffbce9422c9f-0', tool_calls=[{'name': 'transfer_to_red_team', 'args': {}, 'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7509, 'output_tokens': 88, 'total_tokens': 7597, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 55}}), ToolMessage(content='Successfully transferred to red_team', name='transfer_to_red_team', tool_call_id='toolu_01XEAuhQHCBdyCwtymZwFfDi')], 'active_agent': 'red_team'}, goto='red_team'))Traceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 709, in invoke\n input = step.invoke(input, config)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 447, in invoke\n ret = self.func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 84, in _write\n self.do_write(\n ~~~~~~~~~~~~~^\n config,\n ^^^^^^^\n writes,\n ^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 126, in do_write\n write(_assemble_writes(writes))\n ~~~~~~~~~~~~~~~~^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 181, in _assemble_writes\n if ww := w.mapper(w.value):\n ~~~~~~~~^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/graph/state.py\", line 1762, in _control_branch\n raise ParentCommand(command)\n\n\nlanggraph.errors.ParentCommand: Command(graph='__parent__', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', id='389058d1-083f-4632-bbc5-09bfd3a0ffd2', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9'), AIMessage(content=[{'signature': 'CAQS1QgKEAgSGAI4AUIIdGhpbmtpbmcSDN6TfU6NDM971lEbzxoMvRUA1J9ox3fJQYT9IjArCQQGHBP4pRHPavXzRlbMbaYHC57Yj+Gbxgr0helvd2OIL/o6Rbt4PFA3FaIFUlgq8gc7KQI9FtGnvSAkLBRzIsUp/2d5Rd2zgEsfkFVeXsn8WYYZkinAQVVy6A7B3LaEJW0xb/x88Di67fedwx0Q+FlZtwcmT3Dcaa11JveyHRr3dm+zGSZP6CXeXNJ8nrTZ7W1YRTysD6w7o7Lp8h1Rwq4f4o8j+QUfY0vje+zJZsxHPmQNt3ykeKh99hgn6dbiz6/EHqmbHjSAu1M4SqEDfqIKThHYenamAZmcQ4XAOo6mdQ6+77VgpV1lC1/k52IiUoJjkGlQrnLCvutiI7KN+sgK8c2+2KCJrPRCvVV+BkStvP5jWWqzytpKQ+nHzhL448OrIA9gdJC39C8eDrXeRnI7t4FqlkXGoMlZAV9UMcrQHRxOMi8tOyTzi9oyBFDTSsQ+LJqer0tz1Ft9gek6vzg50SpVAFQdLERv8R1YqMaeI+xsvTAEW9MRgvRMkzLuzRT2guw3jD+Fjip37xgK5QI0R+CsOAmQPoTZizzcIyAUAuCJFCgq/gD/CibLxoymiVUI1an9Snt3KpNRoWdvWIne33V0pv8PHg9yW2JuLOqQGilLCsK/Q3oQr3upEMXxXGjCL7WUizvRZGFEEV+VplYt7v4tKZMj5pk3vdeM0MJ5XZg7PvUsRzycxcnrT3Knoc3qM+ttqZfTsb8d6U/pQifq7GuahFOHrvroqgJLj+/81Yy1pZFkjfJgY0XklzAnA6CFq/YT0gxbV+TH5IS+WaYOUzK36S8s5qqMwhngv4IzE3ctXXqnhIZXNznyVBqbYa/Gx2bCjcEj2n5RyGYRS/+MculgLBznQ9MkG2D7QP3Mq/nwHXKMIaLS8VK8hG3d0rAjlvnauXShNrJxtkj4qkwnxD4LrEjk1ue+PLcbLqYWwAr8n0s+41jrsIPVyyK9vFaEZrA9KaBxkJyiqx50GZNn7ckx+mkI18gH52iyVPtU8FKMXjugCl6mPLiHYiDQd+G63LudmOKys9itVnBDBNMP2b/I7+bIVRlheGaZWr6MATKl86B3y+0kuOrLRMyWcdibc2iQkZm+8f7dTUwbKbBJsKOFY73/8nlL/JiYpjz6nUIk+IqSjaFyK6XFlCcukWpWHpDJnSvbEzey7qHyCqJ+I+on8iP8GOtempQaS0/4kZGSwlrE6xOj6EwGpfjbDZKHkP/2RAdNYPQq3z7bdVJMJNnFMz6e37mzMpCBihwPdSFTRE3SCY66wvTgJzGCUyhHbSi5i1azzOXd8uHCSpT/6L1o6VU0pjoR8Kaa4wCWHGf7P/WEd+avyp02Jnu1cmpW0+Gq+vK9h0tdb2IClM/eNfdouQP+ep8MJUjraDRICmAJQY5obS71vniOo+reaFy0zRgB', 'thinking': '', 'type': 'thinking'}, {'signature': 'CAQS/REKEQgSGAI4AUIJbmFycmF0aW9uEgwFh3AVJZP3s8y1Hq0aDG76uhHONaScXAGShiIwGgjLXHUx1yZrkLPljjzTOQycpLJiKVwXcxm/EzKJm1s8M5WxuREWEldXcoaN0/msKpkRMT+eLb5ih9rAHULTomUNsUwJeu0jrJe4GLazy7sP5KtjEOEBKQsXUfoQ7fDkZOQUHZi9zzQN4hFNmYJUf3OVqf9TQNdC1+m//HAZPfWmAy6HtDvm7kgBJkCEoiFnm7tn/PX1hXGPRgWc6BTXUMr2WA8aKyqggRUbMH2+JYv5iWZddCc1up1Nbr3sF7WPcot9ZBhyHQXr692zeJk6SJgygIVrASbMly2bBxTWJB623gVgzpCmVSLZFpkKXgPcp8hGyDKOvF6SiQSHqGRxQofr7Vs1aPhfZixROJGD+MlrkggVAnGMyYIDUZpavtCeiU1JmBSz0084bXPtT/tnpsKOnSPYZQo3B0BXKbqA/WsKng8/V3Z8Uuajo6IVZ2hgA1WDduqQOQkExGU6pFuLhebIjuWxSNI/DffvR//LNqCfvdnf0hvoPtDxfx/SawIx4/spq9xbwFzHtLgoHg1F2wFw7clPDlg1PyD/v2yJG3V9T8GL6jGPE6rhfil/u4HNvf7BW0dQzCTTXcqZy4nByy0+p+S7ZxfMl4Kpxa2gOhhSNjfpfuX0QpL4kME8J1nliFN1d8KqEPuDI18BQHUhD/UfY29oA0x510FjM/BsLZ4n5sppkoIi7CFoQq2aAKVNfC1KilhIpRD+1dyDtZQklN8gZpOESHw6ML1TtU0yfAcbmqoxJ6hhjzwH6CMsTQEEy8MbRBM+QKKkIRdRCPHGFW2LkuR50+J0pF/tr6pqVvJeozfiMnrEilB27TZP4nKTJG8rd//vCWen0y7CA/8dBJ9gdp5tx7NVA45k1gdcxER1gYouNX3n1vG3/B1F4+93wXW9Mqo6Us59TQmrb9Nqa7pSQNSrH06w1GxyBAlkFfqAlafk16aMpq416TJYpGy43WCUiCcLWlhiWgz2sgOLAXP9a5weWAKRC8J6jkcUkDeXnW+HFSiPM6+9Bo6kVhLUhN5rc4mHZOZpF7ptPpsQN4XCqLPsqpRH/8o5XpyC6ZneTeF2ExtjhwZVjXDwMbIzVYIbw1y2PPGw8dC7pc4sIVSnI4oQAH0v650ghfhmMevWW4h4Pylbw/DG4fdeT3+8z0f8MOSTtfipx05/QttHlAvODRxa5yDCvOz6n3f1BO0N/4MbGyOM27qW4x8mBVoryUZWfsmhfi0/GfNvB8+ylTGbYNX27OcEF5WZtNqbWEy7V5GbmZUSxspcg7FBZ3j3/3L7muvZXoZi1f5erq4y3lSH+oF7qPFnvieyLOCwubcQy+oyowj4cD0idRecnp+s+52D7+mzF1gpW+f+cJasoxigbxkOloS4yeiKvgM4nWSpGEj12RPx5o80EmziXN9t4zJzW1htA5hXJJeFympMYtyCUCevRWVmrRYktQ7UnCa6tL/AVPi1ni5XCXHfLOuqfK5KLYxjwVtdhhfmCLvpaKVNCEOCf4nNGHqD98wUb0ejWNwDYBnJGkBRw9RVspfwNdoWoVcn2qv8ywGUp7yc/sk2LpI6VziRyvWqbGNG1ZJqVVutRc46+sOE4VSeS2/CTNKNa6NMCf/hxxaUVsYfKdP+S1Lc4ianKp0sUugPpmtoydC0CP2RwjnHLcnzxCnvqxlLQWXMNPTsu2yBcHlWJLtcmdg/McLagy7l5kG6sN7ZxJLD/kAx8E6uqohP+K/jMSrYuQJnhr4tf4biWFY3Jzf/KpAYyVSq+pbfFKXMbL9nHEoZQT15Wyabd6DMAq3QZOthM5pUzdNqUXkcrI1PwjASAg4dVU/2o+98mTXnyhP7VlEkTnddpwyJF+YCCc01pqa6ZX99pnR+YOpIrb5DtPcKq0t5cFCDyQxN1lUuqZcYujvX/GC7+6IpRTz/NkEY/5cntOfZJyd6SkUx/JR5AJIRPCNeib0rcWnnFn0XK8s22x40wLjEtufXDO9+yraf4kHxlBI3SB0meMjepX50CJhAx7NekkRFnwoecLfqIyrjXrgAZsTMPiX94snjdrzisAXtBKanulilSxOfW2HSmfnk0L2GXHJJ3S+vCbgREml9dekfmHhmwEYtv3kfhth7ERIwVT8j69OtIBCmeTIJuA7o73PXc1LJ2whuZK7FZe6NTOs61MP7JVlIc6tbhtOBk3NXU8pBPOCY6Q55d63FtpUU/KCbQ0K2WQ9o5jNGwNNJ6Cw5FDCt+x5YJeP1d2QzDSiq+cSZqwqCZVcNoAftGGzgrRZ4vkpbP0xQZHCtWsUwHVTjrY4NfRDjHdVPRn8J9Xtooc/hIWkqzFaczYTlapu2RCJqe8aEo3jzWZCEJ7AiBzBYraYDgOfeiQyeJUsbbXHdGKiiw22Bdp4M+Yz671MZ0ie9EWRks0Q2DHGUTne9S4tAAJPGbUf4xtCMoJ40Q4QW05QLSZy3wNFAUFYmXE4ECDFB8+IEeNFFF/tgAnfh8GvFh/1xY+LE+ozMminjSRwhq09zQAEkNFyu0IKgvDkyOI+iccF7doWLTUasPxLH1xGPBSKkeWCnSoX/4C2c9du66BbRXamHqae+VCiS6BN953iYKOu8rMkneW+TFjV0CzAf8PNG9LBayUgY5Uw9x0Sap6zI4PpaZKH4d9ktkFU+91yBbN5SRtdd8/EQatJXmBNfw7kRrll+LjwZweWLEb0jT1RpGuDqHLdLTMMgowSczrp2zCsJdQH/seaYrpwkCFR8dRjM7JRUkVa1A0SvdZAX75vuBvvLBzN2c+Rbt5oqy93lqIBIjKb7w2UkRTb4pBWcNKNPUgt3az/PxQxEasaltdmuyvsXYbur3VPNn8JzN/6J5N+QCnz571TccapCmkPvFE+gayXU+UF1VKYyFcUvlev7lsQlmjqzK2QA8X7Q3TzFapqhvJKlt7dzBHraqZBgPgb9N4oKNayi8FZT71+kaYhIhFrESKYRHfHkj5n3kfhgLy+x+jLDbKxMggkgHnJz9RuEHZXBWtkYAQ==', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_verifier', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAMRdS21FeJfzQhM3s', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7396, 'output_tokens': 770, 'output_tokens_details': {'thinking_tokens': 739}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='skeptic', id='lc_run--01a0fe03-4ae9-76c0-89a6-4f0b114e3030-0', tool_calls=[{'name': 'transfer_to_verifier', 'args': {}, 'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7396, 'output_tokens': 770, 'total_tokens': 8166, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 739}}), ToolMessage(content='Successfully transferred to verifier', name='transfer_to_verifier', id='c064563a-974f-4f0f-ab4a-080e63c01cf5', tool_call_id='toolu_01H6fV8JvRJNvXy47eRnd3jH'), AIMessage(content=[{'signature': 'CAQS9gUKEAgSGAI4AUIIdGhpbmtpbmcSDGFyJ1MXoY+Ai6NkbxoMRd2TrUfUU7WrPpA2IjDgy8dUplcZKc7DnJwNLTBfH5UNR+nGnakSPQia8UVIzrjkOnJ2fLrWUsA2oEO8yRsqkwXkkDCsyhB97dwj57RMt91XzgWhL9luNEGO92yW0rNFGoMSUVOxPIrFc5RjyrhKEc3+1RnpxCkylq+LNlQ5NFxLn6v2cF5DzqpvBHNR7XAig+urJIU3gXydCkaGZZDetJtQOUOXgZi9z9100NpdAEkXDo2ICHCGTGpVc0J/pbXxWRBxb6B1/Gl0Yjn68V6HlesH7IoC7GR/3HwRu9NSDBG4ttfQjEWcg+PeAea4Rn7jzx+Tm9H7L1joTKVgDbQg5iKMLfjvPqlnksCLtXOt8jSIS5OYLN7a/7TX6/w5d34vnw8vPDmU1UYi0Q72qUWhUa2vw8OXNpcKL7Bp0lNfBS8rvHY+vBY8/haAoTL3wvLFU1fT+4z/vA3BcsigZDYlfJeDYcxXmRuqxzxEGpXCRe9s1HWtID+41pHu6lpZHkXaghpc6TZHLxNwcVhTb6fMQ7Qo+2L6Pvklh1m32peodxeg2YiPhdpl3w/xkOEu5/JEF7ZBxpdUL+u3wkO5pUlsqfgX7uhzv7HC2WpSWVimrwiQwQOUx0+JPrLtw+lYfq7I3jruo0GW0guWvnuf7JM3YLj84q+JgHomnSSZnnZmrL0UaRvrVyLo877F39ccLS7XoZnnui8J4P58ZTmiLkEJeGcx1Iuag2e33339O7Yew/59tLbQtIGh9VWMwHBnFx39S8/4LEFrK5HldPQhOrloyTtkRs66iQRKtxMKS0VI6QTKZDAbs7a7H1yBv+mJi9Xg4uKmyU6qnn7zEKjjY2bIlCwezx4LLGP5HD3Zr6fZsmdG1UHzDsBvYG2oCI0oqEKM+valsT6hU8MRjbQBD/JJ8JJN/iBGlhBAUHo0UmhhtR2euTVpZWlMnhmpXAdKDXSfPdaPohgB', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_red_team', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAsabDquegHi6YUpCn', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7509, 'output_tokens': 88, 'output_tokens_details': {'thinking_tokens': 55}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='verifier', id='lc_run--01a0fe03-6682-7d21-8c15-ffbce9422c9f-0', tool_calls=[{'name': 'transfer_to_red_team', 'args': {}, 'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7509, 'output_tokens': 88, 'total_tokens': 7597, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 55}}), ToolMessage(content='Successfully transferred to red_team', name='transfer_to_red_team', tool_call_id='toolu_01XEAuhQHCBdyCwtymZwFfDi')], 'active_agent': 'red_team'}, goto='red_team')\n" + } + }, + { + "key": "exception.escaped", + "value": { + "stringValue": "False" + } + } + ] + } + ], + "status": { + "code": 2 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "4127f167d1d653d6", + "parentSpanId": "8e84b2f5d3c9d733", + "name": "FilesystemMiddleware.wrap_tool_call", + "kind": 1, + "startTimeUnixNano": "1790968032472590080", + "endTimeUnixNano": "1790968032473856000", + "attributes": [ + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "FilesystemMiddleware.wrap_tool_call" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "2" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "tools" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"__pregel_push\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_push\",0,false]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "verifier:544dea40-e16b-458c-1464-128e1b9b10a9|tools:74348550-0598-498b-1732-6675e778549b" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "verifier" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "verifier:544dea40-e16b-458c-1464-128e1b9b10a9" + } + }, + { + "key": "langsmith.metadata.ls_method", + "value": { + "stringValue": "traceable" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "e30=" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJvdXRwdXQiOnsiZ3JhcGgiOiJfX3BhcmVudF9fIiwidXBkYXRlIjp7Im1lc3NhZ2VzIjpbeyJjb250ZW50IjoiRm9yIGEgc3ludGhldGljIHRyYWNpbmcgZml4dHVyZSwgZXhwbGFpbiBQeXRob24gdHVwbGUgdmVyc3VzIGxpc3QgaW1tdXRhYmlsaXR5IGluIGF0IG1vc3QgMTIwIHdvcmRzLiBVc2Ugb25seSBvZmZpY2lhbCBQeXRob24gZG9jdW1lbnRhdGlvbiBpZiB5b3Ugc2VhcmNoLiBIYXZlIHRoZSBza2VwdGljIGFuZCB2ZXJpZmllciBjaGVjayBpdCwgdGhlbiBoYXZlIHRoZSBlZGl0b3Igd3JpdGUgL2Fuc3dlci5tZC4gS2VlcCB0aGUgcnVuIHNob3J0IGFuZCBkbyBub3QgYXNrIGZvbGxvdy11cCBxdWVzdGlvbnMuIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJodW1hbiIsImlkIjoiNDYyMDQzMDUtZjhlZS00Mzg0LTljZTgtZmNkODBiZmNkZjc1In0seyJjb250ZW50IjpbeyJpZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7InF1ZXJ5IjoiZG9jcy5weXRob24ub3JnIHR1cGxlcyBpbW11dGFibGUgc2VxdWVuY2VzIGxpc3RzIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIHR1dG9yaWFsIn0sIm5hbWUiOiJ3ZWJfc2VhcmNoIiwidHlwZSI6InRvb2xfdXNlIiwidG9vbHNldF9uYW1lIjpudWxsfV0sImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2ZkdzlhVzlicmVuUHFneWJmTndQIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjI4NDUsIm91dHB1dF90b2tlbnMiOjczLCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjowfSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCIsIm1vZGVsX3Byb3ZpZGVyIjoiYW50aHJvcGljIn0sInR5cGUiOiJhaSIsIm5hbWUiOiJyZXNlYXJjaGVyIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTIxYTMtNzAwMC1hMzgxLTI4NGE2YTBjMDNmNy0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6IndlYl9zZWFyY2giLCJhcmdzIjp7InF1ZXJ5IjoiZG9jcy5weXRob24ub3JnIHR1cGxlcyBpbW11dGFibGUgc2VxdWVuY2VzIGxpc3RzIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIHR1dG9yaWFsIn0sImlkIjoidG9vbHVfMDFCMkxmSEdEallTRkpobWlYTmo0Q3VRIiwidHlwZSI6InRvb2xfY2FsbCJ9XSwiaW52YWxpZF90b29sX2NhbGxzIjpbXSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjoyODQ1LCJvdXRwdXRfdG9rZW5zIjo3MywidG90YWxfdG9rZW5zIjoyOTE4LCJpbnB1dF90b2tlbl9kZXRhaWxzIjp7ImNhY2hlX3JlYWQiOjAsImNhY2hlX2NyZWF0aW9uIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VuX2RldGFpbHMiOnsicmVhc29uaW5nIjowfX19LHsiY29udGVudCI6Ilt7XCJ0aXRsZVwiOiBcIjUuIERhdGEgU3RydWN0dXJlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiWW91IG1pZ2h0IGhhdmUgbm90aWNlZCB0aGF0IG1ldGhvZHMgbGlrZWBpbnNlcnRgLGByZW1vdmVgIG9yYHNvcnRgIHRoYXQgb25seSBtb2RpZnkgdGhlIGxpc3QgaGF2ZSBubyByZXR1cm4gdmFsdWUgcHJpbnRlZCBcXHUyMDEzIHRoZXkgcmV0dXJuIHRoZSBkZWZhdWx0YE5vbmVgLiBbMV0gVGhpcyBpcyBhIGRlc2lnbiBwcmluY2lwbGUgZm9yIGFsbCBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyBpbiBQeXRob24uXFxuLi4uXFxuIyMgNS4zLiBUdXBsZXMgYW5kIFNlcXVlbmNlc1xcdTAwYjZcXG5cXG5XZSBzYXcgdGhhdCBsaXN0cyBhbmQgc3RyaW5ncyBoYXZlIG1hbnkgY29tbW9uIHByb3BlcnRpZXMsIHN1Y2ggYXMgaW5kZXhpbmcgYW5kIHNsaWNpbmcgb3BlcmF0aW9ucy4gVGhleSBhcmUgdHdvIGV4YW1wbGVzIG9mIHNlcXVlbmNlIGRhdGEgdHlwZXMgKHNlZSBTZXF1ZW5jZSBUeXBlcyBcXHUyMDE0IGxpc3QsIHR1cGxlLCByYW5nZSkuIFNpbmNlIFB5dGhvbiBpcyBhbiBldm9sdmluZyBsYW5ndWFnZSwgb3RoZXIgc2VxdWVuY2UgZGF0YSB0eXBlcyBtYXkgYmUgYWRkZWQuIFRoZXJlIGlzIGFsc28gYW5vdGhlciBzdGFuZGFyZCBzZXF1ZW5jZSBkYXRhIHR5cGU6IHRoZSB0dXBsZS5cXG5cXG5BIHR1cGxlIGNvbnNpc3RzIG9mIGEgbnVtYmVyIG9mIHZhbHVlcyBzZXBhcmF0ZWQgYnkgY29tbWFzLCBmb3IgaW5zdGFuY2U6XFxuLi4uXFxuPj4+ICMgVHVwbGVzIGFyZSBpbW11dGFibGU6XFxuPj4+IHRbMF0gPSA4ODg4OFxcblRyYWNlYmFjayAobW9zdCByZWNlbnQgY2FsbCBsYXN0KTpcXG4gIEZpbGUgXFxcIjxzdGRpbj5cXFwiLCBsaW5lIDEsIGluIDxtb2R1bGU+XFxuVHlwZUVycm9yOiAndHVwbGUnIG9iamVjdCBkb2VzIG5vdCBzdXBwb3J0IGl0ZW0gYXNzaWdubWVudFxcbj4+PiAjIGJ1dCB0aGV5IGNhbiBjb250YWluIG11dGFibGUgb2JqZWN0czpcXG4+Pj4gdiA9IChbMSwgMiwgM10sIFszLCAyLCAxXSlcXG4+Pj4gdlxcbihbMSwgMiwgM10sIFszLCAyLCAxXSlcXG4uLi5cXG5BcyB5b3Ugc2VlLCBvbiBvdXRwdXQgdHVwbGVzIGFyZSBhbHdheXMgZW5jbG9zZWQgaW4gcGFyZW50aGVzZXMsIHNvIHRoYXQgbmVzdGVkIHR1cGxlcyBhcmUgaW50ZXJwcmV0ZWQgY29ycmVjdGx5OyB0aGV5IG1heSBiZSBpbnB1dCB3aXRoIG9yIHdpdGhvdXQgc3Vycm91bmRpbmcgcGFyZW50aGVzZXMsIGFsdGhvdWdoIG9mdGVuIHBhcmVudGhlc2VzIGFyZSBuZWNlc3NhcnkgYW55d2F5IChpZiB0aGUgdHVwbGUgaXMgcGFydCBvZiBhIGxhcmdlciBleHByZXNzaW9uKS4gSXQgaXMgbm90IHBvc3NpYmxlIHRvIGFzc2lnbiB0byB0aGUgaW5kaXZpZHVhbCBpdGVtcyBvZiBhIHR1cGxlLCBob3dldmVyIGl0IGlzIHBvc3NpYmxlIHRvIGNyZWF0ZSB0dXBsZXMgd2hpY2ggY29udGFpbiBtdXRhYmxlIG9iamVjdHMsIHN1Y2ggYXMgbGlzdHMuXFxuXFxuVGhvdWdoIHR1cGxlcyBtYXkgc2VlbSBzaW1pbGFyIHRvIGxpc3RzLCB0aGV5IGFyZSBvZnRlbiB1c2VkIGluIGRpZmZlcmVudCBzaXR1YXRpb25zIGFuZCBmb3IgZGlmZmVyZW50IHB1cnBvc2VzLiBUdXBsZXMgYXJlIGltbXV0YWJsZSwgYW5kIHVzdWFsbHkgY29udGFpbiBhIGhldGVyb2dlbmVvdXMgc2VxdWVuY2Ugb2YgZWxlbWVudHMgdGhhdCBhcmUgYWNjZXNzZWQgdmlhIHVucGFja2luZyAoc2VlIGxhdGVyIGluIHRoaXMgc2VjdGlvbikgb3IgaW5kZXhpbmcgKG9yIGV2ZW4gYnkgYXR0cmlidXRlIGluIHRoZSBjYXNlIG9mIG5hbWVkdHVwbGVzKS4gTGlzdHMgYXJlIG11dGFibGUsIGFuZCB0aGVpciBlbGVtZW50cyBhcmUgdXN1YWxseSBob21vZ2VuZW91cyBhbmQgYXJlIGFjY2Vzc2VkIGJ5IGl0ZXJhdGluZyBvdmVyIHRoZSBsaXN0Llxcbi4uLlxcbkFub3RoZXIgdXNlZnVsIGRhdGEgdHlwZSBidWlsdCBpbnRvIFB5dGhvbiBpcyB0aGUgZGljdGlvbmFyeSAoc2VlIE1hcHBpbmcgVHlwZXMgXFx1MjAxNCBkaWN0KS4gRGljdGlvbmFyaWVzIGFyZSBzb21ldGltZXMgZm91bmQgaW4gb3RoZXIgbGFuZ3VhZ2VzIGFzIFxcdTIwMWNhc3NvY2lhdGl2ZSBtZW1vcmllc1xcdTIwMWQgb3IgXFx1MjAxY2Fzc29jaWF0aXZlIGFycmF5c1xcdTIwMWQuIFVubGlrZSBzZXF1ZW5jZXMsIHdoaWNoIGFyZSBpbmRleGVkIGJ5IGEgcmFuZ2Ugb2YgbnVtYmVycywgZGljdGlvbmFyaWVzIGFyZSBpbmRleGVkIGJ5IGtleXMsIHdoaWNoIGNhbiBiZSBhbnkgaW1tdXRhYmxlIHR5cGU7IHN0cmluZ3MgYW5kIG51bWJlcnMgY2FuIGFsd2F5cyBiZSBrZXlzLiBUdXBsZXMgY2FuIGJlIHVzZWQgYXMga2V5cyBpZiB0aGV5IGNvbnRhaW4gb25seSBzdHJpbmdzLCBudW1iZXJzLCBvciB0dXBsZXM7IGlmIGEgdHVwbGUgY29udGFpbnMgYW55IG11dGFibGUgb2JqZWN0IGVpdGhlciBkaXJlY3RseSBvciBpbmRpcmVjdGx5LCBpdCBjYW5ub3QgYmUgdXNlZCBhcyBhIGtleS4gWW91IGNhblxcdTIwMTl0IHVzZSBsaXN0cyBhcyBrZXlzLCBzaW5jZSBsaXN0cyBjYW4gYmUgbW9kaWZpZWQgaW4gcGxhY2UgdXNpbmcgaW5kZXggYXNzaWdubWVudHMsIHNsaWNlIGFzc2lnbm1lbnRzLCBvciBtZXRob2RzIGxpa2UgYXBwZW5kKCkgYW5kIGV4dGVuZCgpLlwiXX0sIHtcInRpdGxlXCI6IFwiMy4gRGF0YSBtb2RlbCBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvcmVmZXJlbmNlL2RhdGFtb2RlbC5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJUaGUgdmFsdWUgb2Ygc29tZSBvYmplY3RzIGNhbiBjaGFuZ2UuIE9iamVjdHMgd2hvc2UgdmFsdWUgY2FuIGNoYW5nZSBhcmUgc2FpZCB0byBiZSBtdXRhYmxlOyBvYmplY3RzIHdob3NlIHZhbHVlIGlzIHVuY2hhbmdlYWJsZSBvbmNlIHRoZXkgYXJlIGNyZWF0ZWQgYXJlIGNhbGxlZCBpbW11dGFibGUuIChUaGUgdmFsdWUgb2YgYW4gaW1tdXRhYmxlIGNvbnRhaW5lciBvYmplY3QgdGhhdCBjb250YWlucyBhIHJlZmVyZW5jZSB0byBhIG11dGFibGUgb2JqZWN0IGNhbiBjaGFuZ2Ugd2hlbiB0aGUgbGF0dGVyXFx1MjAxOXMgdmFsdWUgaXMgY2hhbmdlZDsgaG93ZXZlciB0aGUgY29udGFpbmVyIGlzIHN0aWxsIGNvbnNpZGVyZWQgaW1tdXRhYmxlLCBiZWNhdXNlIHRoZSBjb2xsZWN0aW9uIG9mIG9iamVjdHMgaXQgY29udGFpbnMgY2Fubm90IGJlIGNoYW5nZWQuIFNvLCBpbW11dGFiaWxpdHkgaXMgbm90IHN0cmljdGx5IHRoZSBzYW1lIGFzIGhhdmluZyBhbiB1bmNoYW5nZWFibGUgdmFsdWUsIGl0IGlzIG1vcmUgc3VidGxlLikgQW4gb2JqZWN0XFx1MjAxOXMgbXV0YWJpbGl0eSBpcyBkZXRlcm1pbmVkIGJ5IGl0cyB0eXBlOyBmb3IgaW5zdGFuY2UsIG51bWJlcnMsIHN0cmluZ3MgYW5kIHR1cGxlcyBhcmUgaW1tdXRhYmxlLCB3aGlsZSBkaWN0aW9uYXJpZXMgYW5kIGxpc3RzIGFyZSBtdXRhYmxlLlxcbi4uLlxcblNvbWUgb2JqZWN0cyBjb250YWluIHJlZmVyZW5jZXMgdG8gb3RoZXIgb2JqZWN0czsgdGhlc2UgYXJlIGNhbGxlZCBjb250YWluZXJzLiBFeGFtcGxlcyBvZiBjb250YWluZXJzIGFyZSB0dXBsZXMsIGxpc3RzIGFuZCBkaWN0aW9uYXJpZXMuIFRoZSByZWZlcmVuY2VzIGFyZSBwYXJ0IG9mIGEgY29udGFpbmVyXFx1MjAxOXMgdmFsdWUuIEluIG1vc3QgY2FzZXMsIHdoZW4gd2UgdGFsayBhYm91dCB0aGUgdmFsdWUgb2YgYSBjb250YWluZXIsIHdlIGltcGx5IHRoZSB2YWx1ZXMsIG5vdCB0aGUgaWRlbnRpdGllcyBvZiB0aGUgY29udGFpbmVkIG9iamVjdHM7IGhvd2V2ZXIsIHdoZW4gd2UgdGFsayBhYm91dCB0aGUgbXV0YWJpbGl0eSBvZiBhIGNvbnRhaW5lciwgb25seSB0aGUgaWRlbnRpdGllcyBvZiB0aGUgaW1tZWRpYXRlbHkgY29udGFpbmVkIG9iamVjdHMgYXJlIGltcGxpZWQuIFNvLCBpZiBhbiBpbW11dGFibGUgY29udGFpbmVyIChsaWtlIGEgdHVwbGUpIGNvbnRhaW5zIGEgcmVmZXJlbmNlIHRvIGEgbXV0YWJsZSBvYmplY3QsIGl0cyB2YWx1ZSBjaGFuZ2VzIGlmIHRoYXQgbXV0YWJsZSBvYmplY3QgaXMgY2hhbmdlZC5cXG4uLi5cXG4jIyMgMy4yLjUuIFNlcXVlbmNlc1xcdTAwYjZcXG4uLi5cXG4jIyMjIDMuMi41LjEuIEltbXV0YWJsZSBzZXF1ZW5jZXNcXHUwMGI2XFxuXFxuQW4gb2JqZWN0IG9mIGFuIGltbXV0YWJsZSBzZXF1ZW5jZSB0eXBlIGNhbm5vdCBjaGFuZ2Ugb25jZSBpdCBpcyBjcmVhdGVkLiAoSWYgdGhlIG9iamVjdCBjb250YWlucyByZWZlcmVuY2VzIHRvIG90aGVyIG9iamVjdHMsIHRoZXNlIG90aGVyIG9iamVjdHMgbWF5IGJlIG11dGFibGUgYW5kIG1heSBiZSBjaGFuZ2VkOyBob3dldmVyLCB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGRpcmVjdGx5IHJlZmVyZW5jZWQgYnkgYW4gaW1tdXRhYmxlIG9iamVjdCBjYW5ub3QgY2hhbmdlLilcXG5cXG5UaGUgZm9sbG93aW5nIHR5cGVzIGFyZSBpbW11dGFibGUgc2VxdWVuY2VzOlxcbi4uLlxcblR1cGxlc1xcbjogVGhlIGl0ZW1zIG9mIGEgYHR1cGxlYCBhcmUgYXJiaXRyYXJ5IFB5dGhvbiBvYmplY3RzLiBUdXBsZXMgb2YgdHdvIG9yIG1vcmUgaXRlbXMgYXJlIGZvcm1lZCBieSBjb21tYS1zZXBhcmF0ZWQgbGlzdHMgb2YgZXhwcmVzc2lvbnMuIEEgdHVwbGUgb2Ygb25lIGl0ZW0gKGEgXFx1MjAxOHNpbmdsZXRvblxcdTIwMTkpIGNhbiBiZSBmb3JtZWQgYnkgYWZmaXhpbmcgYSBjb21tYSB0byBhbiBleHByZXNzaW9uIChhbiBleHByZXNzaW9uIGJ5IGl0c2VsZiBkb2VzIG5vdCBjcmVhdGUgYSB0dXBsZSwgc2luY2UgcGFyZW50aGVzZXMgbXVzdCBiZSB1c2FibGUgZm9yIGdyb3VwaW5nIG9mIGV4cHJlc3Npb25zKS4gQW4gZW1wdHkgdHVwbGUgY2FuIGJlIGZvcm1lZCBieSBhbiBlbXB0eSBwYWlyIG9mIHBhcmVudGhlc2VzLlxcbi4uLlxcbiMjIyMgMy4yLjUuMi4gTXV0YWJsZSBzZXF1ZW5jZXNcXHUwMGI2XFxuXFxuTXV0YWJsZSBzZXF1ZW5jZXMgY2FuIGJlIGNoYW5nZWQgYWZ0ZXIgdGhleSBhcmUgY3JlYXRlZC4gVGhlIHN1YnNjcmlwdGlvbiBhbmQgc2xpY2luZyBub3RhdGlvbnMgY2FuIGJlIHVzZWQgYXMgdGhlIHRhcmdldCBvZiBhc3NpZ25tZW50IGFuZCBgZGVsYCAoZGVsZXRlKSBzdGF0ZW1lbnRzLlxcbi4uLlxcblRoZXJlIGFyZSBjdXJyZW50bHkgdHdvIGludHJpbnNpYyBtdXRhYmxlIHNlcXVlbmNlIHR5cGVzOlxcblxcbkxpc3RzXFxuOiBUaGUgaXRlbXMgb2YgYSBsaXN0IGFyZSBhcmJpdHJhcnkgUHl0aG9uIG9iamVjdHMuIExpc3RzIGFyZSBmb3JtZWQgYnkgcGxhY2luZyBhIGNvbW1hLXNlcGFyYXRlZCBsaXN0IG9mIGV4cHJlc3Npb25zIGluIHNxdWFyZSBicmFja2V0cy4gKE5vdGUgdGhhdCB0aGVyZSBhcmUgbm8gc3BlY2lhbCBjYXNlcyBuZWVkZWQgdG8gZm9ybSBsaXN0cyBvZiBsZW5ndGggMCBvciAxLilcIl19LCB7XCJ0aXRsZVwiOiBcIkJ1aWx0LWluIFR5cGVzIFxcdTIwMTQgUHl0aG9uIDMuMTQuNyBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9idWlsdGlucy9zdGR0eXBlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJjb2xsZWN0aW9uIGNsYXNzZXMgYXJlIG11dGFibGUuIFRoZSBtZXRob2RzIHRoYXQgYWRkLCBzdWJ0cmFjdCwgb3IgcmVhcnJhbmdlIHRoZWlyIC4uLiBpbiBwbGFjZSwgYW5kIGRvblxcdTIwMTl0IHJldHVybiBhIC4uLiAsIG5ldmVyIHJldHVybiB0aGUgY29sbGVjdGlvbiBpbnN0YW5jZSBpdHNlbGYgYnV0IGBOb25lYC5cXG4uLi5cXG4jIyBTZXF1ZW5jZSBUeXBlcyBcXHUyMDE0IGBsaXN0YCwgYHR1cGxlYCwgYHJhbmdlYFxcdTAwYjZcXG5cXG5UaGVyZSBhcmUgdGhyZWUgYmFzaWMgc2VxdWVuY2UgdHlwZXM6IGxpc3RzLCB0dXBsZXMsIGFuZCByYW5nZSBvYmplY3RzLiBBZGRpdGlvbmFsIHNlcXVlbmNlIHR5cGVzIHRhaWxvcmVkIGZvciBwcm9jZXNzaW5nIG9mIGJpbmFyeSBkYXRhIGFuZCB0ZXh0IHN0cmluZ3MgYXJlIGRlc2NyaWJlZCBpbiBkZWRpY2F0ZWQgc2VjdGlvbnMuXFxuLi4uXFxuVGhlIG9wZXJhdGlvbnMgaW4gdGhlIGZvbGxvd2luZyB0YWJsZSAuLi4gbXV0YWJsZSBhbmQgaW1tdXRhYmxlLiBUaGUgYGNvbGxlY3Rpb25zLiAuLi4gaXMgcHJvdmlkZWQgdG8gbWFrZSAuLi4gZWFzaWVyIHRvIGNvcnJlY3RseSBpbXBsZW1lbnQgdGhlc2Ugb3BlcmF0aW9ucyBvbiBjdXN0b20gc2VxdWVuY2UgdHlwZXNcXG4uLi5cXG4jIyMgSW1tdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbm11dGFibGUgc2VxdWVuY2UgdHlwZXMgaXNcXG4uLi5cXG5zdXBwb3J0IGFsbG93cyBpbW11dGFibGUgc2VxdWVuY2VzLCAuLi4gLCB0byBiZSB1c2VkIGFzIGBkaWN0YCBrZXlzIGFuZCBzdG9yZWQgaW4gLi4uIGVuc2V0YCBpbnN0YW5jZXNcXG4uLi5cXG4jIyMgTXV0YWJsZSBTZXF1ZW5jZSBUeXBlc1xcdTAwYjZcXG4uLi5cXG4jIyMgTGlzdHNcXHUwMGI2XFxuXFxuTGlzdHMgYXJlIG11dGFibGUgc2VxdWVuY2VzLCB0eXBpY2FsbHkgdXNlZCB0byBzdG9yZSBjb2xsZWN0aW9ucyBvZiBob21vZ2VuZW91cyBpdGVtcyAod2hlcmUgdGhlIHByZWNpc2UgZGVncmVlIG9mIHNpbWlsYXJpdHkgd2lsbCB2YXJ5IGJ5IGFwcGxpY2F0aW9uKS5cXG4uLi5cXG4jIyMgVHVwbGVzXFx1MDBiNlxcblxcblR1cGxlcyBhcmUgaW1tdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaGV0ZXJvZ2VuZW91cyBkYXRhIChzdWNoIGFzIHRoZSAyLXR1cGxlcyBwcm9kdWNlZCBieSB0aGUgYGVudW1lcmF0ZSgpYCBidWlsdC1pbikuIFR1cGxlcyBhcmUgYWxzbyB1c2VkIGZvciBjYXNlcyB3aGVyZSBhbiBpbW11dGFibGUgc2VxdWVuY2Ugb2YgaG9tb2dlbmVvdXMgZGF0YSBpcyBuZWVkZWQgKHN1Y2ggYXMgYWxsb3dpbmcgc3RvcmFnZSBpbiBhIGBzZXRgIG9yIGBkaWN0YCBpbnN0YW5jZSkuXFxuLi4uXFxudHVwbGUoaXRlcmFibGU9XFxuLi4uXFxuVGhlIGNvbnN0cnVjdG9yIGJ1aWxkcyBhIHR1cGxlIHdob3NlIGl0ZW1zIGFyZSB0aGUgc2FtZSBhbmQgaW4gdGhlIHNhbWUgb3JkZXIgYXMgaXRlcmFibGVcXHUyMDE5cyBpdGVtcy4gaXRlcmFibGUgbWF5IGJlIGVpdGhlciBhIHNlcXVlbmNlLCBhIGNvbnRhaW5lciB0aGF0IHN1cHBvcnRzIGl0ZXJhdGlvbiwgb3IgYW4gaXRlcmF0b3Igb2JqZWN0LiBJZiBpdGVyYWJsZSBpcyBhbHJlYWR5IGEgdHVwbGUsIGl0IGlzIHJldHVybmVkIHVuY2hhbmdlZC4gRm9yIGV4YW1wbGUsIGB0dXBsZSgnYWJjJylgIHJldHVybnMgYCgnYScsICdiJywgJ2MnKWAgYW5kIGB0dXBsZSggWzEsIDIsIDNdIClgIHJldHVybnMgYCgxLCAyLCAzKWAuIElmIG5vIGFyZ3VtZW50IGlzIGdpdmVuLCB0aGUgY29uc3RydWN0b3IgY3JlYXRlcyBhIG5ldyBlbXB0eSB0dXBsZSwgYCgpYC5cXG4uLi5cXG5UdXBsZXMgaW1wbGVtZW50IGFsbCBvZiB0aGUgY29tbW9uIHNlcXVlbmNlIG9wZXJhdGlvbnMuXFxuLi4uXFxuRm9yIGhldGVyb2dlbmVvdXMgY29sbGVjdGlvbnMgb2YgZGF0YSB3aGVyZSBhY2Nlc3MgYnkgbmFtZSBpcyBjbGVhcmVyIHRoYW4gYWNjZXNzIGJ5IGluZGV4LCBgY29sbGVjdGlvbnMubmFtZWR0dXBsZSgpYCBtYXkgYmUgYSBtb3JlIGFwcHJvcHJpYXRlIGNob2ljZSB0aGFuIGEgc2ltcGxlIHR1cGxlIG9iamVjdC5cIl19LCB7XCJ0aXRsZVwiOiBcIkJ1aWx0LWluIFR5cGVzIFxcdTIwMTQgUHl0aG9uIDMuMTQuNSBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9saWJyYXJ5L3N0ZHR5cGVzLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcIlNvbWUgY29sbGVjdGlvbiBjbGFzc2VzIGFyZSBtdXRhYmxlLiBUaGUgbWV0aG9kcyB0aGF0IGFkZCwgc3VidHJhY3QsIG9yIHJlYXJyYW5nZSB0aGVpciAuLi4gaW4gcGxhY2UsIGFuZCBkb25cXHUyMDE5dCByZXR1cm4gYSAuLi4gLCBuZXZlciByZXR1cm4gdGhlIGNvbGxlY3Rpb24gaW5zdGFuY2UgaXRzZWxmIGJ1dCBgTm9uZWAuXFxuLi4uXFxuIyMgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBgbGlzdGAsIGB0dXBsZWAsIGByYW5nZWBcXHUwMGI2XFxuXFxuVGhlcmUgYXJlIHRocmVlIGJhc2ljIHNlcXVlbmNlIHR5cGVzOiBsaXN0cywgdHVwbGVzLCBhbmQgLi4uIG9iamVjdHMuIEFkZGl0aW9uYWwgc2VxdWVuY2UgdHlwZXMgdGFpbG9yZWQgZm9yIHByb2Nlc3Npbmcgb2YgYmluYXJ5IGRhdGEgYW5kIHRleHQgc3RyaW5ncyBhcmUgZGVzY3JpYmVkIGluIGRlZGljYXRlZCBzZWN0aW9ucy5cXG4uLi5cXG5UaGUgb3BlcmF0aW9ucyBpbiB0aGUgZm9sbG93aW5nIHRhYmxlIC4uLiBpbW11dGFibGUuIFRoZSBgIC4uLiBpcyBwcm92aWRlZCB0byBtYWtlIC4uLiBlYXNpZXIgdG8gY29ycmVjdGx5IGltcGxlbWVudCB0aGVzZSBvcGVyYXRpb25zIG9uIGN1c3RvbSBzZXF1ZW5jZSB0eXBlc1xcbi4uLlxcbiMjIyBJbW11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxub3BlcmF0aW9uIHRoYXQgaW1tdXRhYmxlIHNlcXVlbmNlIC4uLiBieSBtdXRhYmxlIHNlcXVlbmNlIHR5cGVzIGlzIC4uLiBgaGFzaCgpYFxcbi4uLlxcblRoaXMgc3VwcG9ydCBhbGxvd3MgaW1tdXRhYmxlIHNlcXVlbmNlcywgc3VjaCBhcyBgdHVwbGVgIGluc3RhbmNlcywgdG8gYmUgdXNlZCBhcyBgZGljdGAga2V5cyBhbmQgc3RvcmVkIGluIGBzZXRgIGFuZCBgZnJvemVuc2V0YCBpbnN0YW5jZXMuXFxuLi4uXFxuIyMjIE11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxuIyMjIExpc3RzXFx1MDBiNlxcblxcbkxpc3RzIGFyZSBtdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaG9tb2dlbmVvdXMgaXRlbXMgKHdoZXJlIHRoZSBwcmVjaXNlIGRlZ3JlZSBvZiBzaW1pbGFyaXR5IHdpbGwgdmFyeSBieSBhcHBsaWNhdGlvbikuXFxuLi4uXFxuIyMjIFR1cGxlc1xcdTAwYjZcXG5cXG5UdXBsZXMgYXJlIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhldGVyb2dlbmVvdXMgZGF0YSAoc3VjaCBhcyB0aGUgMi10dXBsZXMgcHJvZHVjZWQgYnkgdGhlIGBlbnVtZXJhdGUoKWAgYnVpbHQtaW4pLiBUdXBsZXMgYXJlIGFsc28gdXNlZCBmb3IgY2FzZXMgd2hlcmUgYW4gaW1tdXRhYmxlIHNlcXVlbmNlIG9mIGhvbW9nZW5lb3VzIGRhdGEgaXMgbmVlZGVkIChzdWNoIGFzIGFsbG93aW5nIHN0b3JhZ2UgaW4gYSBgc2V0YCBvciBgZGljdGAgaW5zdGFuY2UpLlxcbi4uLlxcblRoZSBjb25zdHJ1Y3RvciBidWlsZHMgYSB0dXBsZSB3aG9zZSBpdGVtcyBhcmUgdGhlIHNhbWUgYW5kIGluIHRoZSBzYW1lIG9yZGVyIGFzIGl0ZXJhYmxlXFx1MjAxOXMgaXRlbXMuIGl0ZXJhYmxlIG1heSBiZSBlaXRoZXIgYSBzZXF1ZW5jZSwgYSBjb250YWluZXIgdGhhdCBzdXBwb3J0cyBpdGVyYXRpb24sIG9yIGFuIGl0ZXJhdG9yIG9iamVjdC4gSWYgaXRlcmFibGUgaXMgYWxyZWFkeSBhIHR1cGxlLCBpdCBpcyByZXR1cm5lZCB1bmNoYW5nZWQuIEZvciBleGFtcGxlLCBgdHVwbGUoJ2FiYycpYCByZXR1cm5zIGAoJ2EnLCAnYicsICdjJylgIGFuZCBgdHVwbGUoIFsxLCAyLCAzXSApYCByZXR1cm5zIGAoMSwgMiwgMylgLiBJZiBubyBhcmd1bWVudCBpcyBnaXZlbiwgdGhlIGNvbnN0cnVjdG9yIGNyZWF0ZXMgYSBuZXcgZW1wdHkgdHVwbGUsIGAoKWAuXFxuLi4uXFxuVHVwbGVzIGltcGxlbWVudCBhbGwgb2YgdGhlIGNvbW1vbiBzZXF1ZW5jZSBvcGVyYXRpb25zLlxcbi4uLlxcbkZvciBoZXRlcm9nZW5lb3VzIGNvbGxlY3Rpb25zIG9mIGRhdGEgd2hlcmUgYWNjZXNzIGJ5IG5hbWUgaXMgY2xlYXJlciB0aGFuIGFjY2VzcyBieSBpbmRleCwgYGNvbGxlY3Rpb25zLm5hbWVkdHVwbGUoKWAgbWF5IGJlIGEgbW9yZSBhcHByb3ByaWF0ZSBjaG9pY2UgdGhhbiBhIHNpbXBsZSB0dXBsZSBvYmplY3QuXCJdfSwge1widGl0bGVcIjogXCI1LiBEYXRhIFN0cnVjdHVyZXMgXFx1MjAxNCBQeXRob24gMy4xMC4yMCBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy4xMC90dXRvcmlhbC9kYXRhc3RydWN0dXJlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJZb3UgbWlnaHQgaGF2ZSBub3RpY2VkIHRoYXQgbWV0aG9kcyBsaWtlIGBpbnNlcnRgLCBgcmVtb3ZlYCBvciBgc29ydGAgdGhhdCBvbmx5IG1vZGlmeSB0aGUgbGlzdCBoYXZlIG5vIHJldHVybiB2YWx1ZSBwcmludGVkIFxcdTIwMTMgdGhleSByZXR1cm4gdGhlIGRlZmF1bHQgYE5vbmVgLiAxIFRoaXMgaXMgYSBkZXNpZ24gcHJpbmNpcGxlIGZvciBhbGwgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgaW4gUHl0aG9uLlxcbi4uLlxcbiMjIDUuMy4gVHVwbGVzIGFuZCBTZXF1ZW5jZXNcXHUwMGI2XFxuXFxuV2Ugc2F3IHRoYXQgbGlzdHMgYW5kIHN0cmluZ3MgaGF2ZSBtYW55IGNvbW1vbiBwcm9wZXJ0aWVzLCBzdWNoIGFzIGluZGV4aW5nIGFuZCBzbGljaW5nIG9wZXJhdGlvbnMuIFRoZXkgYXJlIHR3byBleGFtcGxlcyBvZiBzZXF1ZW5jZSBkYXRhIHR5cGVzIChzZWUgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBsaXN0LCB0dXBsZSwgcmFuZ2UpLiBTaW5jZSBQeXRob24gaXMgYW4gZXZvbHZpbmcgbGFuZ3VhZ2UsIG90aGVyIHNlcXVlbmNlIGRhdGEgdHlwZXMgbWF5IGJlIGFkZGVkLiBUaGVyZSBpcyBhbHNvIGFub3RoZXIgc3RhbmRhcmQgc2VxdWVuY2UgZGF0YSB0eXBlOiB0aGUgdHVwbGUuXFxuXFxuQSB0dXBsZSBjb25zaXN0cyBvZiBhIG51bWJlciBvZiB2YWx1ZXMgc2VwYXJhdGVkIGJ5IGNvbW1hcywgZm9yIGluc3RhbmNlOlxcbi4uLlxcbj4+PiAjIFR1cGxlcyBhcmUgaW1tdXRhYmxlOlxcbi4uLiB0WzBdID0gODg4ODhcXG5UcmFjZWJhY2sgKG1vc3QgcmVjZW50IGNhbGwgbGFzdCk6XFxuICBGaWxlIFxcXCI8c3RkaW4+XFxcIiwgbGluZSAxLCBpbiA8bW9kdWxlPlxcblR5cGVFcnJvcjogJ3R1cGxlJyBvYmplY3QgZG9lcyBub3Qgc3VwcG9ydCBpdGVtIGFzc2lnbm1lbnRcXG4+Pj4gIyBidXQgdGhleSBjYW4gY29udGFpbiBtdXRhYmxlIG9iamVjdHM6XFxuLi4uIHYgPSAoWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuPj4+IHZcXG4oWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuLi4uXFxuQXMgeW91IHNlZSwgb24gb3V0cHV0IHR1cGxlcyBhcmUgYWx3YXlzIGVuY2xvc2VkIGluIHBhcmVudGhlc2VzLCBzbyB0aGF0IG5lc3RlZCB0dXBsZXMgYXJlIGludGVycHJldGVkIGNvcnJlY3RseTsgdGhleSBtYXkgYmUgaW5wdXQgd2l0aCBvciB3aXRob3V0IHN1cnJvdW5kaW5nIHBhcmVudGhlc2VzLCBhbHRob3VnaCBvZnRlbiBwYXJlbnRoZXNlcyBhcmUgbmVjZXNzYXJ5IGFueXdheSAoaWYgdGhlIHR1cGxlIGlzIHBhcnQgb2YgYSBsYXJnZXIgZXhwcmVzc2lvbikuIEl0IGlzIG5vdCBwb3NzaWJsZSB0byBhc3NpZ24gdG8gdGhlIGluZGl2aWR1YWwgaXRlbXMgb2YgYSB0dXBsZSwgaG93ZXZlciBpdCBpcyBwb3NzaWJsZSB0byBjcmVhdGUgdHVwbGVzIHdoaWNoIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzLCBzdWNoIGFzIGxpc3RzLlxcblxcblRob3VnaCB0dXBsZXMgbWF5IHNlZW0gc2ltaWxhciB0byBsaXN0cywgdGhleSBhcmUgb2Z0ZW4gdXNlZCBpbiBkaWZmZXJlbnQgc2l0dWF0aW9ucyBhbmQgZm9yIGRpZmZlcmVudCBwdXJwb3Nlcy4gVHVwbGVzIGFyZSBpbW11dGFibGUsIGFuZCB1c3VhbGx5IGNvbnRhaW4gYSBoZXRlcm9nZW5lb3VzIHNlcXVlbmNlIG9mIGVsZW1lbnRzIHRoYXQgYXJlIGFjY2Vzc2VkIHZpYSB1bnBhY2tpbmcgKHNlZSBsYXRlciBpbiB0aGlzIHNlY3Rpb24pIG9yIGluZGV4aW5nIChvciBldmVuIGJ5IGF0dHJpYnV0ZSBpbiB0aGUgY2FzZSBvZiBgbmFtZWR0dXBsZXNgKS4gTGlzdHMgYXJlIG11dGFibGUsIGFuZCB0aGVpciBlbGVtZW50cyBhcmUgdXN1YWxseSBob21vZ2VuZW91cyBhbmQgYXJlIGFjY2Vzc2VkIGJ5IGl0ZXJhdGluZyBvdmVyIHRoZSBsaXN0Llxcbi4uLlxcbiMjIDUuNC5cXG4uLi5cXG4uIFNldCBvYmplY3RzXFxuLi4uXFxuIyMgNS41LiBEaWN0aW9uYXJpZXNcXHUwMGI2XFxuXFxuQW5vdGhlciB1c2VmdWwgZGF0YSB0eXBlIGJ1aWx0IGludG8gUHl0aG9uIGlzIHRoZSBkaWN0aW9uYXJ5IChzZWUgTWFwcGluZyBUeXBlcyBcXHUyMDE0IGRpY3QpLiBEaWN0aW9uYXJpZXMgYXJlIHNvbWV0aW1lcyBmb3VuZCBpbiBvdGhlciBsYW5ndWFnZXMgYXMgXFx1MjAxY2Fzc29jaWF0aXZlIG1lbW9yaWVzXFx1MjAxZCBvciBcXHUyMDFjYXNzb2NpYXRpdmUgYXJyYXlzXFx1MjAxZC4gVW5saWtlIHNlcXVlbmNlcywgd2hpY2ggYXJlIGluZGV4ZWQgYnkgYSByYW5nZSBvZiBudW1iZXJzLCBkaWN0aW9uYXJpZXMgYXJlIGluZGV4ZWQgYnkga2V5cywgd2hpY2ggY2FuIGJlIGFueSBpbW11dGFibGUgdHlwZTsgc3RyaW5ncyBhbmQgbnVtYmVycyBjYW4gYWx3YXlzIGJlIGtleXMuIFR1cGxlcyBjYW4gYmUgdXNlZCBhcyBrZXlzIGlmIHRoZXkgY29udGFpbiBvbmx5IHN0cmluZ3MsIG51bWJlcnMsIG9yIHR1cGxlczsgaWYgYSB0dXBsZSBjb250YWlucyBhbnkgbXV0YWJsZSBvYmplY3QgZWl0aGVyIGRpcmVjdGx5IG9yIGluZGlyZWN0bHksIGl0IGNhbm5vdCBiZSB1c2VkIGFzIGEga2V5LiBZb3UgY2FuXFx1MjAxOXQgdXNlIGxpc3RzIGFzIGtleXMsIHNpbmNlIGxpc3RzIGNhbiBiZSBtb2RpZmllZCBpbiBwbGFjZSB1c2luZyBpbmRleCBhc3NpZ25tZW50cywgc2xpY2UgYXNzaWdubWVudHMsIG9yIG1ldGhvZHMgbGlrZSBgYXBwZW5kKClgIGFuZCBgZXh0ZW5kKClgLlwiXX1dIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJ0b29sIiwibmFtZSI6IndlYl9zZWFyY2giLCJpZCI6IjZlNTEyNWFjLTMxNjQtNGE3Ni05YzUyLWFjMzQ2MGNlMzZlNCIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsInN0YXR1cyI6InN1Y2Nlc3MifSx7ImNvbnRlbnQiOlt7ImlkIjoidG9vbHVfMDFYamozYVJkV3loQkpmUG1USkJ4UWE5IiwiY2FsbGVyIjp7InR5cGUiOiJkaXJlY3QifSwiaW5wdXQiOnt9LCJuYW1lIjoidHJhbnNmZXJfdG9fc2tlcHRpYyIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHdBNkY0Y0NONXloRDRDcnM5TSIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo3Mzc4LCJvdXRwdXRfdG9rZW5zIjozMywib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6MH0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoicmVzZWFyY2hlciIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy0zY2Q4LTc5ZjEtODhhMS1jZTZjZWUyOTRlNzktMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ0cmFuc2Zlcl90b19za2VwdGljIiwiYXJncyI6e30sImlkIjoidG9vbHVfMDFYamozYVJkV3loQkpmUG1USkJ4UWE5IiwidHlwZSI6InRvb2xfY2FsbCJ9XSwiaW52YWxpZF90b29sX2NhbGxzIjpbXSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjo3Mzc4LCJvdXRwdXRfdG9rZW5zIjozMywidG90YWxfdG9rZW5zIjo3NDExLCJpbnB1dF90b2tlbl9kZXRhaWxzIjp7ImNhY2hlX3JlYWQiOjAsImNhY2hlX2NyZWF0aW9uIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VuX2RldGFpbHMiOnsicmVhc29uaW5nIjowfX19LHsiY29udGVudCI6IlN1Y2Nlc3NmdWxseSB0cmFuc2ZlcnJlZCB0byBza2VwdGljIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJ0b29sIiwibmFtZSI6InRyYW5zZmVyX3RvX3NrZXB0aWMiLCJpZCI6IjM4OTA1OGQxLTA4M2YtNDYzMi1iYmM1LTA5YmZkM2EwZmZkMiIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsInN0YXR1cyI6InN1Y2Nlc3MifSx7ImNvbnRlbnQiOlt7InNpZ25hdHVyZSI6IkNBUVMxUWdLRUFnU0dBSTRBVUlJZEdocGJtdHBibWNTRE42VGZVNk5ETTk3MWxFYnp4b012UlVBMUo5b3gzZkpRWVQ5SWpBckNRUUdIQlA0cFJIUGF2WHpSbGJNYmFZSEM1N1lqK0dieGdyMGhlbHZkMk9JTC9vNlJidDRQRkEzRmFJRlVsZ3E4Z2M3S1FJOUZ0R252U0FrTEJSeklzVXAvMmQ1UmQyemdFc2ZrRlZlWHNuOFdZWVpraW5BUVZWeTZBN0IzTGFFSlcweGIveDg4RGk2N2ZlZHd4MFErRmxadHdjbVQzRGNhYTExSnZleUhScjNkbSt6R1NaUDZDWGVYTko4bnJUWjdXMVlSVHlzRDZ3N283THA4aDFSd3E0ZjRvOGorUVVmWTB2amUrekpac3hIUG1RTnQzeWtlS2g5OWhnbjZkYml6Ni9FSHFtYkhqU0F1MU00U3FFRGZxSUtUaEhZZW5hbUFabWNRNFhBT282bWRRNis3N1ZncFYxbEMxL2s1MklpVW9KamtHbFFybkxDdnV0aUk3S04rc2dLOGMyKzJLQ0pyUFJDdlZWK0JrU3R2UDVqV1dxenl0cEtRK25IemhMNDQ4T3JJQTlnZEpDMzlDOGVEclhlUm5JN3Q0RnFsa1hHb01sWkFWOVVNY3JRSFJ4T01pOHRPeVR6aTlveUJGRFRTc1ErTEpxZXIwdHoxRnQ5Z2VrNnZ6ZzUwU3BWQUZRZExFUnY4UjFZcU1hZUkreHN2VEFFVzlNUmd2Uk1rekx1elJUMmd1dzNqRCtGamlwMzd4Z0s1UUkwUitDc09BbVFQb1RaaXp6Y0l5QVVBdUNKRkNncS9nRC9DaWJMeG95bWlWVUkxYW45U250M0twTlJvV2R2V0luZTMzVjBwdjhQSGc5eVcySnVMT3FRR2lsTENzSy9RM29RcjN1cEVNWHhYR2pDTDdXVWl6dlJaR0ZFRVYrVnBsWXQ3djR0S1pNajVwazN2ZGVNME1KNVhaZzdQdlVzUnp5Y3hjbnJUM0tub2MzcU0rdHRxWmZUc2I4ZDZVL3BRaWZxN0d1YWhGT0hydnJvcWdKTGorLzgxWXkxcFpGa2pmSmdZMFhrbHpBbkE2Q0ZxL1lUMGd4YlYrVEg1SVMrV2FZT1V6SzM2UzhzNXFxTXdobmd2NEl6RTNjdFhYcW5oSVpYTnpueVZCcWJZYS9HeDJiQ2pjRWoybjVSeUdZUlMvK01jdWxnTEJ6blE5TWtHMkQ3UVAzTXEvbndIWEtNSWFMUzhWSzhoRzNkMHJBamx2bmF1WFNoTnJKeHRrajRxa3dueEQ0THJFamsxdWUrUExjYkxxWVd3QXI4bjBzKzQxanJzSVBWeXlLOXZGYUVackE5S2FCeGtKeWlxeDUwR1pObjdja3grbWtJMThnSDUyaXlWUHRVOEZLTVhqdWdDbDZtUExpSFlpRFFkK0c2M0x1ZG1PS3lzOWl0Vm5CREJOTVAyYi9JNytiSVZSbGhlR2FaV3I2TUFUS2w4NkIzeSswa3VPckxSTXlXY2RpYmMyaVFrWm0rOGY3ZFRVd2JLYkJKc0tPRlk3My84bmxML0ppWXBqejZuVUlrK0lxU2phRnlLNlhGbENjdWtXcFdIcERKblN2YkV6ZXk3cUh5Q3FKK0krb244aVA4R090ZW1wUWFTMC80a1pHU3dsckU2eE9qNkV3R3BmamJEWktIa1AvMlJBZE5ZUFFxM3o3YmRWSk1KTm5GTXo2ZTM3bXpNcENCaWh3UGRTRlRSRTNTQ1k2Nnd2VGdKekdDVXloSGJTaTVpMWF6ek9YZDh1SENTcFQvNkwxbzZWVTBwam9SOEthYTR3Q1dIR2Y3UC9XRWQrYXZ5cDAySm51MWNtcFcwK0dxK3ZLOWgwdGRiMklDbE0vZU5mZG91UVArZXA4TUpVanJhRFJJQ21BSlFZNW9iUzcxdm5pT28rcmVhRnkwelJnQiIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJzaWduYXR1cmUiOiJDQVFTL1JFS0VRZ1NHQUk0QVVJSmJtRnljbUYwYVc5dUVnd0ZoM0FWSlpQM3M4eTFIcTBhREc3NnVoSE9OYVNjWEFHU2hpSXdHZ2pMWEhVeDF5WnJrTFBsamp6VE9ReWNwTEppS1Z3WGN4bS9FektKbTFzOE01V3h1UkVXRWxkWGNvYU4wL21zS3BrUk1UK2VMYjVpaDlyQUhVTFRvbVVOc1V3SmV1MGpySmU0R0xhenk3c1A1S3RqRU9FQktRc1hVZm9RN2ZEa1pPUVVIWmk5enpRTjRoRk5tWUpVZjNPVnFmOVRRTmRDMSttLy9IQVpQZldtQXk2SHREdm03a2dCSmtDRW9pRm5tN3RuL1BYMWhYR1BSZ1djNkJUWFVNcjJXQThhS3lxZ2dSVWJNSDIrSll2NWlXWmRkQ2MxdXAxTmJyM3NGN1dQY290OVpCaHlIUVhyNjkyemVKazZTSmd5Z0lWckFTYk1seTJiQnhUV0pCNjIzZ1ZnenBDbVZTTFpGcGtLWGdQY3A4aEd5REtPdkY2U2lRU0hxR1J4UW9mcjdWczFhUGhmWml4Uk9KR0QrTWxya2dnVkFuR015WUlEVVpwYXZ0Q2VpVTFKbUJTejAwODRiWFB0VC90bnBzS09uU1BZWlFvM0IwQlhLYnFBL1dzS25nOC9WM1o4VXVham82SVZaMmhnQTFXRGR1cVFPUWtFeEdVNnBGdUxoZWJJanVXeFNOSS9EZmZ2Ui8vTE5xQ2Z2ZG5mMGh2b1B0RHhmeC9TYXdJeDQvc3BxOXhid0Z6SHRMZ29IZzFGMndGdzdjbFBEbGcxUHlEL3YyeUpHM1Y5VDhHTDZqR1BFNnJoZmlsL3U0SE52ZjdCVzBkUXpDVFRYY3FaeTRuQnl5MCtwK1M3WnhmTWw0S3B4YTJnT2hoU05qZnBmdVgwUXBMNGtNRThKMW5saUZOMWQ4S3FFUHVESTE4QlFIVWhEL1VmWTI5b0EweDUxMEZqTS9Cc0xaNG41c3Bwa29JaTdDRm9RcTJhQUtWTmZDMUtpbGhJcFJEKzFkeUR0WlFrbE44Z1pwT0VTSHc2TUwxVHRVMHlmQWNibXFveEo2aGhqendINkNNc1RRRUV5OE1iUkJNK1FLS2tJUmRSQ1BIR0ZXMkxrdVI1MCtKMHBGL3RyNnBxVnZKZW96ZmlNbnJFaWxCMjdUWlA0bktUSkc4cmQvL3ZDV2VuMHk3Q0EvOGRCSjlnZHA1dHg3TlZBNDVrMWdkY3hFUjFnWW91TlgzbjF2RzMvQjFGNCs5M3dYVzlNcW82VXM1OVRRbXJiOU5xYTdwU1FOU3JIMDZ3MUd4eUJBbGtGZnFBbGFmazE2YU1wcTQxNlRKWXBHeTQzV0NVaUNjTFdsaGlXZ3oyc2dPTEFYUDlhNXdlV0FLUkM4SjZqa2NVa0RlWG5XK0hGU2lQTTYrOUJvNmtWaExVaE41cmM0bUhaT1pwRjdwdFBwc1FONFhDcUxQc3FwUkgvOG81WHB5QzZabmVUZUYyRXh0amh3WlZqWER3TWJJelZZSWJ3MXkyUFBHdzhkQzdwYzRzSVZTbkk0b1FBSDB2NjUwZ2hmaG1NZXZXVzRoNFB5bGJ3L0RHNGZkZVQzKzh6MGY4TU9TVHRmaXB4MDUvUXR0SGxBdk9EUnhhNXlEQ3ZPejZuM2YxQk8wTi80TWJHeU9NMjdxVzR4OG1CVm9yeVVaV2ZzbWhmaTAvR2ZOdkI4K3lsVEdiWU5YMjdPY0VGNVdadE5xYldFeTdWNUdibVpVU3hzcGNnN0ZCWjNqMy8zTDdtdXZaWG9aaTFmNWVycTR5M2xTSCtvRjdxUEZudmlleUxPQ3d1YmNReStveW93ajRjRDBpZFJlY25wK3MrNTJENyttekYxZ3BXK2YrY0phc294aWdieGtPbG9TNHllaUt2Z000bldTcEdFajEyUlB4NW84MEVtemlYTjl0NHpKelcxaHRBNWhYSkplRnltcE1ZdHlDVUNldlJXVm1yUllrdFE3VW5DYTZ0TC9BVlBpMW5pNVhDWEhmTE91cWZLNUtMWXhqd1Z0ZGhoZm1DTHZwYUtWTkNFT0NmNG5OR0hxRDk4d1ViMGVqV053RFlCbkpHa0JSdzlSVnNwZndOZG9Xb1ZjbjJxdjh5d0dVcDd5Yy9zazJMcEk2VnppUnl2V3FiR05HMVpKcVZWdXRSYzQ2K3NPRTRWU2VTMi9DVE5LTmE2Tk1DZi9oeHhhVVZzWWZLZFArUzFMYzRpYW5LcDBzVXVnUHBtdG95ZEMwQ1AyUndqbkhMY256eENudnF4bExRV1hNTlBUc3UyeUJjSGxXSkx0Y21kZy9NY0xhZ3k3bDVrRzZzTjdaeEpMRC9rQXg4RTZ1cW9oUCtLL2pNU3JZdVFKbmhyNHRmNGJpV0ZZM0p6Zi9LcEFZeVZTcStwYmZGS1hNYkw5bkhFb1pRVDE1V3lhYmQ2RE1BcTNRWk90aE01cFV6ZE5xVVhrY3JJMVB3akFTQWc0ZFZVLzJvKzk4bVRYbnloUDdWbEVrVG5kZHB3eUpGK1lDQ2MwMXBxYTZaWDk5cG5SK1lPcElyYjVEdFBjS3EwdDVjRkNEeVF4TjFsVXVxWmNZdWp2WC9HQzcrNklwUlR6L05rRVkvNWNudE9mWkp5ZDZTa1V4L0pSNUFKSVJQQ05laWIwcmNXbm5GbjBYSzhzMjJ4NDB3TGpFdHVmWERPOSt5cmFmNGtIeGxCSTNTQjBtZU1qZXBYNTBDSmhBeDdOZWtrUkZud29lY0xmcUl5cmpYcmdBWnNUTVBpWDk0c25qZHJ6aXNBWHRCS2FudWxpbFN4T2ZXMkhTbWZuazBMMkdYSEpKM1MrdkNiZ1JFbWw5ZGVrZm1IaG13RVl0djNrZmh0aDdFUkl3VlQ4ajY5T3RJQkNtZVRJSnVBN283M1BYYzFMSjJ3aHVaSzdGWmU2TlRPczYxTVA3SlZsSWM2dGJodE9CazNOWFU4cEJQT0NZNlE1NWQ2M0Z0cFVVL0tDYlEwSzJXUTlvNWpOR3dOTko2Q3c1RkRDdCt4NVlKZVAxZDJRekRTaXErY1NacXdxQ1pWY05vQWZ0R0d6Z3JSWjR2a3BiUDB4UVpIQ3RXc1V3SFZUanJZNE5mUkRqSGRWUFJuOEo5WHRvb2MvaElXa3F6RmFjellUbGFwdTJSQ0pxZThhRW8zanpXWkNFSjdBaUJ6QllyYVlEZ09mZWlReWVKVXNiYlhIZEdLaWl3MjJCZHA0TStZejY3MU1aMGllOUVXUmtzMFEyREhHVVRuZTlTNHRBQUpQR2JVZjR4dENNb0o0MFE0UVcwNVFMU1p5M3dORkFVRlltWEU0RUNERkI4K0lFZU5GRkYvdGdBbmZoOEd2RmgvMXhZK0xFK296TW1pbmpTUndocTA5elFBRWtORnl1MElLZ3ZEa3lPSStpY2NGN2RvV0xUVWFzUHhMSDF4R1BCU0trZVdDblNvWC80QzJjOWR1NjZCYlJYYW1IcWFlK1ZDaVM2Qk45NTNpWUtPdThyTWtuZVcrVEZqVjBDekFmOFBORzlMQmF5VWdZNVV3OXgwU2FwNnpJNFBwYVpLSDRkOWt0a0ZVKzkxeUJiTjVTUnRkZDgvRVFhdEpYbUJOZnc3a1JybGwrTGp3WndlV0xFYjBqVDFScEd1RHFITGRMVE1NZ293U2N6cnAyekNzSmRRSC9zZWFZcnB3a0NGUjhkUmpNN0pSVWtWYTFBMFN2ZFpBWDc1dnVCdnZMQnpOMmMrUmJ0NW9xeTkzbHFJQklqS2I3dzJVa1JUYjRwQldjTktOUFVndDNhei9QeFF4RWFzYWx0ZG11eXZzWFlidXIzVlBObjhKek4vNko1TitRQ256NTcxVGNjYXBDbWtQdkZFK2dheVhVK1VGMVZLWXlGY1V2bGV2N2xzUWxtanF6SzJRQThYN1EzVHpGYXBxaHZKS2x0N2R6QkhyYXFaQmdQZ2I5TjRvS05heWk4RlpUNzEra2FZaEloRnJFU0tZUkhmSGtqNW4za2ZoZ0x5K3grakxEYkt4TWdna2dIbkp6OVJ1RUhaWEJXdGtZQVE9PSIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJpZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3ZlcmlmaWVyIiwidHlwZSI6InRvb2xfdXNlIiwidG9vbHNldF9uYW1lIjpudWxsfV0sImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2Zkd0FNUmRTMjFGZUpmelFoTTNzIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjczOTYsIm91dHB1dF90b2tlbnMiOjc3MCwib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6NzM5fSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCIsIm1vZGVsX3Byb3ZpZGVyIjoiYW50aHJvcGljIn0sInR5cGUiOiJhaSIsIm5hbWUiOiJza2VwdGljIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTRhZTktNzZjMC04OWE2LTRmMGIxMTRlMzAzMC0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6InRyYW5zZmVyX3RvX3ZlcmlmaWVyIiwiYXJncyI6e30sImlkIjoidG9vbHVfMDFINmZWOEp2UkpOdlh5NDdlUm5kM2pIIiwidHlwZSI6InRvb2xfY2FsbCJ9XSwiaW52YWxpZF90b29sX2NhbGxzIjpbXSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjo3Mzk2LCJvdXRwdXRfdG9rZW5zIjo3NzAsInRvdGFsX3Rva2VucyI6ODE2NiwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6NzM5fX19LHsiY29udGVudCI6IlN1Y2Nlc3NmdWxseSB0cmFuc2ZlcnJlZCB0byB2ZXJpZmllciIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsImlkIjoiYzA2NDU2M2EtOTc0Zi00ZjBmLWFiNGEtMDgwZTYzYzAxY2Y1IiwidG9vbF9jYWxsX2lkIjoidG9vbHVfMDFINmZWOEp2UkpOdlh5NDdlUm5kM2pIIiwic3RhdHVzIjoic3VjY2VzcyJ9LHsiY29udGVudCI6W3sic2lnbmF0dXJlIjoiQ0FRUzlnVUtFQWdTR0FJNEFVSUlkR2hwYm10cGJtY1NER0Z5SjFNWG9ZK0FpNk5rYnhvTVJkMlRyVWZVVTdXclBwQTJJakRneThkVXBsY1pLYzdEbkp3TkxUQmZINVVOUituR25ha1NQUWlhOFVWSXpyamtPbkoyZkxyV1VzQTJvRU84eVJzcWt3WGtrRENzeWhCOTdkd2o1N1JNdDkxWHpnV2hMOWx1TkVHTzkyeVcwck5GR29NU1VWT3hQSXJGYzVSanlyaEtFYzMrMVJucHhDa3lscStMTmxRNU5GeExuNnYyY0Y1RHpxcHZCSE5SN1hBaWcrdXJKSVUzZ1h5ZENrYUdaWkRldEp0UU9VT1hnWmk5ejkxMDBOcGRBRWtYRG8ySUNIQ0dUR3BWYzBKL3BiWHhXUkJ4YjZCMS9HbDBZam42OFY2SGxlc0g3SW9DN0dSLzNId1J1OU5TREJHNHR0ZlFqRVdjZytQZUFlYTRSbjdqengrVG05SDdMMWpvVEtWZ0RiUWc1aUtNTGZqdlBxbG5rc0NMdFhPdDhqU0lTNU9ZTE43YS83VFg2L3c1ZDM0dm53OHZQRG1VMVVZaTBRNzJxVVdoVWEydnc4T1hOcGNLTDdCcDBsTmZCUzhydkhZK3ZCWTgvaGFBb1RMM3d2TEZVMWZUKzR6L3ZBM0Jjc2lnWkRZbGZKZURZY3hYbVJ1cXh6eEVHcFhDUmU5czFIV3RJRCs0MXBIdTZscFpIa1hhZ2hwYzZUWkhMeE53Y1ZoVGI2Zk1RN1FvKzJMNlB2a2xoMW0zMnBlb2R4ZWcyWWlQaGRwbDN3L3hrT0V1NS9KRUY3WkJ4cGRVTCt1M3drTzVwVWxzcWZnWDd1aHp2N0hDMldwU1dWaW1yd2lRd1FPVXgwK0pQckx0dytsWWZxN0kzanJ1bzBHVzBndVd2bnVmN0pNM1lMajg0cStKZ0hvbW5TU1publptckwwVWFSdnJWeUxvODc3RjM5Y2NMUzdYb1pubnVpOEo0UDU4WlRtaUxrRUplR2N4MUl1YWcyZTMzMzM5TzdZZXcvNTl0TGJRdElHaDlWV013SEJuRngzOVM4LzRMRUZySzVIbGRQUWhPcmxveVR0a1JzNjZpUVJLdHhNS1MwVkk2UVRLWkRBYnM3YTdIMXlCdittSmk5WGc0dUtteVU2cW5uN3pFS2pqWTJiSWxDd2V6eDRMTEdQNUhEM1pyNmZac21kRzFVSHpEc0J2WUcyb0NJMG9xRUtNK3ZhbHNUNmhVOE1SamJRQkQvSko4SkpOL2lCR2xoQkFVSG8wVW1oaHRSMmV1VFZwWldsTW5obXBYQWRLRFhTZlBkYVBvaGdCIiwidGhpbmtpbmciOiIiLCJ0eXBlIjoidGhpbmtpbmcifSx7ImlkIjoidG9vbHVfMDFYRUF1aFFIQ0JkeUN3dHltWndGZkRpIiwiY2FsbGVyIjp7InR5cGUiOiJkaXJlY3QifSwiaW5wdXQiOnt9LCJuYW1lIjoidHJhbnNmZXJfdG9fcmVkX3RlYW0iLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QXNhYkRxdWVnSGk2WVVwQ24iLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzUwOSwib3V0cHV0X3Rva2VucyI6ODgsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjU1fSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCIsIm1vZGVsX3Byb3ZpZGVyIjoiYW50aHJvcGljIn0sInR5cGUiOiJhaSIsIm5hbWUiOiJ2ZXJpZmllciIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy02NjgyLTdkMjEtOGMxNS1mZmJjZTk0MjJjOWYtMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ0cmFuc2Zlcl90b19yZWRfdGVhbSIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxWEVBdWhRSENCZHlDd3R5bVp3RmZEaSIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzUwOSwib3V0cHV0X3Rva2VucyI6ODgsInRvdGFsX3Rva2VucyI6NzU5NywiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6NTV9fX0seyJjb250ZW50IjoiU3VjY2Vzc2Z1bGx5IHRyYW5zZmVycmVkIHRvIHJlZF90ZWFtIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJ0b29sIiwibmFtZSI6InRyYW5zZmVyX3RvX3JlZF90ZWFtIiwidG9vbF9jYWxsX2lkIjoidG9vbHVfMDFYRUF1aFFIQ0JkeUN3dHltWndGZkRpIiwic3RhdHVzIjoic3VjY2VzcyJ9XSwiYWN0aXZlX2FnZW50IjoicmVkX3RlYW0ifSwicmVzdW1lIjpudWxsLCJnb3RvIjoicmVkX3RlYW0ifX0=" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "b2daa1c17359fac2", + "parentSpanId": "4127f167d1d653d6", + "name": "transfer_to_red_team", + "kind": 1, + "startTimeUnixNano": "1790968032472979968", + "endTimeUnixNano": "1790968032473444864", + "attributes": [ + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "tool" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "transfer_to_red_team" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "execute_tool" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "gen_ai.tool.name", + "value": { + "stringValue": "transfer_to_red_team" + } + }, + { + "key": "gen_ai.tool.call.id", + "value": { + "stringValue": "toolu_01XEAuhQHCBdyCwtymZwFfDi" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "2" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "tools" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"__pregel_push\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_push\",0,false]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "verifier:544dea40-e16b-458c-1464-128e1b9b10a9|tools:74348550-0598-498b-1732-6675e778549b" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "verifier" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "verifier:544dea40-e16b-458c-1464-128e1b9b10a9" + } + }, + { + "key": "langsmith.metadata.__handoff_destination", + "value": { + "stringValue": "red_team" + } + }, + { + "key": "langsmith.metadata.ls_method", + "value": { + "stringValue": "traceable" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "seq:step:1" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "e30=" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJvdXRwdXQiOnsiZ3JhcGgiOiJfX3BhcmVudF9fIiwidXBkYXRlIjp7Im1lc3NhZ2VzIjpbeyJjb250ZW50IjoiRm9yIGEgc3ludGhldGljIHRyYWNpbmcgZml4dHVyZSwgZXhwbGFpbiBQeXRob24gdHVwbGUgdmVyc3VzIGxpc3QgaW1tdXRhYmlsaXR5IGluIGF0IG1vc3QgMTIwIHdvcmRzLiBVc2Ugb25seSBvZmZpY2lhbCBQeXRob24gZG9jdW1lbnRhdGlvbiBpZiB5b3Ugc2VhcmNoLiBIYXZlIHRoZSBza2VwdGljIGFuZCB2ZXJpZmllciBjaGVjayBpdCwgdGhlbiBoYXZlIHRoZSBlZGl0b3Igd3JpdGUgL2Fuc3dlci5tZC4gS2VlcCB0aGUgcnVuIHNob3J0IGFuZCBkbyBub3QgYXNrIGZvbGxvdy11cCBxdWVzdGlvbnMuIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJodW1hbiIsImlkIjoiNDYyMDQzMDUtZjhlZS00Mzg0LTljZTgtZmNkODBiZmNkZjc1In0seyJjb250ZW50IjpbeyJpZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7InF1ZXJ5IjoiZG9jcy5weXRob24ub3JnIHR1cGxlcyBpbW11dGFibGUgc2VxdWVuY2VzIGxpc3RzIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIHR1dG9yaWFsIn0sIm5hbWUiOiJ3ZWJfc2VhcmNoIiwidHlwZSI6InRvb2xfdXNlIiwidG9vbHNldF9uYW1lIjpudWxsfV0sImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2ZkdzlhVzlicmVuUHFneWJmTndQIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjI4NDUsIm91dHB1dF90b2tlbnMiOjczLCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjowfSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCIsIm1vZGVsX3Byb3ZpZGVyIjoiYW50aHJvcGljIn0sInR5cGUiOiJhaSIsIm5hbWUiOiJyZXNlYXJjaGVyIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTIxYTMtNzAwMC1hMzgxLTI4NGE2YTBjMDNmNy0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6IndlYl9zZWFyY2giLCJhcmdzIjp7InF1ZXJ5IjoiZG9jcy5weXRob24ub3JnIHR1cGxlcyBpbW11dGFibGUgc2VxdWVuY2VzIGxpc3RzIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIHR1dG9yaWFsIn0sImlkIjoidG9vbHVfMDFCMkxmSEdEallTRkpobWlYTmo0Q3VRIiwidHlwZSI6InRvb2xfY2FsbCJ9XSwiaW52YWxpZF90b29sX2NhbGxzIjpbXSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjoyODQ1LCJvdXRwdXRfdG9rZW5zIjo3MywidG90YWxfdG9rZW5zIjoyOTE4LCJpbnB1dF90b2tlbl9kZXRhaWxzIjp7ImNhY2hlX3JlYWQiOjAsImNhY2hlX2NyZWF0aW9uIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VuX2RldGFpbHMiOnsicmVhc29uaW5nIjowfX19LHsiY29udGVudCI6Ilt7XCJ0aXRsZVwiOiBcIjUuIERhdGEgU3RydWN0dXJlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiWW91IG1pZ2h0IGhhdmUgbm90aWNlZCB0aGF0IG1ldGhvZHMgbGlrZWBpbnNlcnRgLGByZW1vdmVgIG9yYHNvcnRgIHRoYXQgb25seSBtb2RpZnkgdGhlIGxpc3QgaGF2ZSBubyByZXR1cm4gdmFsdWUgcHJpbnRlZCBcXHUyMDEzIHRoZXkgcmV0dXJuIHRoZSBkZWZhdWx0YE5vbmVgLiBbMV0gVGhpcyBpcyBhIGRlc2lnbiBwcmluY2lwbGUgZm9yIGFsbCBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyBpbiBQeXRob24uXFxuLi4uXFxuIyMgNS4zLiBUdXBsZXMgYW5kIFNlcXVlbmNlc1xcdTAwYjZcXG5cXG5XZSBzYXcgdGhhdCBsaXN0cyBhbmQgc3RyaW5ncyBoYXZlIG1hbnkgY29tbW9uIHByb3BlcnRpZXMsIHN1Y2ggYXMgaW5kZXhpbmcgYW5kIHNsaWNpbmcgb3BlcmF0aW9ucy4gVGhleSBhcmUgdHdvIGV4YW1wbGVzIG9mIHNlcXVlbmNlIGRhdGEgdHlwZXMgKHNlZSBTZXF1ZW5jZSBUeXBlcyBcXHUyMDE0IGxpc3QsIHR1cGxlLCByYW5nZSkuIFNpbmNlIFB5dGhvbiBpcyBhbiBldm9sdmluZyBsYW5ndWFnZSwgb3RoZXIgc2VxdWVuY2UgZGF0YSB0eXBlcyBtYXkgYmUgYWRkZWQuIFRoZXJlIGlzIGFsc28gYW5vdGhlciBzdGFuZGFyZCBzZXF1ZW5jZSBkYXRhIHR5cGU6IHRoZSB0dXBsZS5cXG5cXG5BIHR1cGxlIGNvbnNpc3RzIG9mIGEgbnVtYmVyIG9mIHZhbHVlcyBzZXBhcmF0ZWQgYnkgY29tbWFzLCBmb3IgaW5zdGFuY2U6XFxuLi4uXFxuPj4+ICMgVHVwbGVzIGFyZSBpbW11dGFibGU6XFxuPj4+IHRbMF0gPSA4ODg4OFxcblRyYWNlYmFjayAobW9zdCByZWNlbnQgY2FsbCBsYXN0KTpcXG4gIEZpbGUgXFxcIjxzdGRpbj5cXFwiLCBsaW5lIDEsIGluIDxtb2R1bGU+XFxuVHlwZUVycm9yOiAndHVwbGUnIG9iamVjdCBkb2VzIG5vdCBzdXBwb3J0IGl0ZW0gYXNzaWdubWVudFxcbj4+PiAjIGJ1dCB0aGV5IGNhbiBjb250YWluIG11dGFibGUgb2JqZWN0czpcXG4+Pj4gdiA9IChbMSwgMiwgM10sIFszLCAyLCAxXSlcXG4+Pj4gdlxcbihbMSwgMiwgM10sIFszLCAyLCAxXSlcXG4uLi5cXG5BcyB5b3Ugc2VlLCBvbiBvdXRwdXQgdHVwbGVzIGFyZSBhbHdheXMgZW5jbG9zZWQgaW4gcGFyZW50aGVzZXMsIHNvIHRoYXQgbmVzdGVkIHR1cGxlcyBhcmUgaW50ZXJwcmV0ZWQgY29ycmVjdGx5OyB0aGV5IG1heSBiZSBpbnB1dCB3aXRoIG9yIHdpdGhvdXQgc3Vycm91bmRpbmcgcGFyZW50aGVzZXMsIGFsdGhvdWdoIG9mdGVuIHBhcmVudGhlc2VzIGFyZSBuZWNlc3NhcnkgYW55d2F5IChpZiB0aGUgdHVwbGUgaXMgcGFydCBvZiBhIGxhcmdlciBleHByZXNzaW9uKS4gSXQgaXMgbm90IHBvc3NpYmxlIHRvIGFzc2lnbiB0byB0aGUgaW5kaXZpZHVhbCBpdGVtcyBvZiBhIHR1cGxlLCBob3dldmVyIGl0IGlzIHBvc3NpYmxlIHRvIGNyZWF0ZSB0dXBsZXMgd2hpY2ggY29udGFpbiBtdXRhYmxlIG9iamVjdHMsIHN1Y2ggYXMgbGlzdHMuXFxuXFxuVGhvdWdoIHR1cGxlcyBtYXkgc2VlbSBzaW1pbGFyIHRvIGxpc3RzLCB0aGV5IGFyZSBvZnRlbiB1c2VkIGluIGRpZmZlcmVudCBzaXR1YXRpb25zIGFuZCBmb3IgZGlmZmVyZW50IHB1cnBvc2VzLiBUdXBsZXMgYXJlIGltbXV0YWJsZSwgYW5kIHVzdWFsbHkgY29udGFpbiBhIGhldGVyb2dlbmVvdXMgc2VxdWVuY2Ugb2YgZWxlbWVudHMgdGhhdCBhcmUgYWNjZXNzZWQgdmlhIHVucGFja2luZyAoc2VlIGxhdGVyIGluIHRoaXMgc2VjdGlvbikgb3IgaW5kZXhpbmcgKG9yIGV2ZW4gYnkgYXR0cmlidXRlIGluIHRoZSBjYXNlIG9mIG5hbWVkdHVwbGVzKS4gTGlzdHMgYXJlIG11dGFibGUsIGFuZCB0aGVpciBlbGVtZW50cyBhcmUgdXN1YWxseSBob21vZ2VuZW91cyBhbmQgYXJlIGFjY2Vzc2VkIGJ5IGl0ZXJhdGluZyBvdmVyIHRoZSBsaXN0Llxcbi4uLlxcbkFub3RoZXIgdXNlZnVsIGRhdGEgdHlwZSBidWlsdCBpbnRvIFB5dGhvbiBpcyB0aGUgZGljdGlvbmFyeSAoc2VlIE1hcHBpbmcgVHlwZXMgXFx1MjAxNCBkaWN0KS4gRGljdGlvbmFyaWVzIGFyZSBzb21ldGltZXMgZm91bmQgaW4gb3RoZXIgbGFuZ3VhZ2VzIGFzIFxcdTIwMWNhc3NvY2lhdGl2ZSBtZW1vcmllc1xcdTIwMWQgb3IgXFx1MjAxY2Fzc29jaWF0aXZlIGFycmF5c1xcdTIwMWQuIFVubGlrZSBzZXF1ZW5jZXMsIHdoaWNoIGFyZSBpbmRleGVkIGJ5IGEgcmFuZ2Ugb2YgbnVtYmVycywgZGljdGlvbmFyaWVzIGFyZSBpbmRleGVkIGJ5IGtleXMsIHdoaWNoIGNhbiBiZSBhbnkgaW1tdXRhYmxlIHR5cGU7IHN0cmluZ3MgYW5kIG51bWJlcnMgY2FuIGFsd2F5cyBiZSBrZXlzLiBUdXBsZXMgY2FuIGJlIHVzZWQgYXMga2V5cyBpZiB0aGV5IGNvbnRhaW4gb25seSBzdHJpbmdzLCBudW1iZXJzLCBvciB0dXBsZXM7IGlmIGEgdHVwbGUgY29udGFpbnMgYW55IG11dGFibGUgb2JqZWN0IGVpdGhlciBkaXJlY3RseSBvciBpbmRpcmVjdGx5LCBpdCBjYW5ub3QgYmUgdXNlZCBhcyBhIGtleS4gWW91IGNhblxcdTIwMTl0IHVzZSBsaXN0cyBhcyBrZXlzLCBzaW5jZSBsaXN0cyBjYW4gYmUgbW9kaWZpZWQgaW4gcGxhY2UgdXNpbmcgaW5kZXggYXNzaWdubWVudHMsIHNsaWNlIGFzc2lnbm1lbnRzLCBvciBtZXRob2RzIGxpa2UgYXBwZW5kKCkgYW5kIGV4dGVuZCgpLlwiXX0sIHtcInRpdGxlXCI6IFwiMy4gRGF0YSBtb2RlbCBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvcmVmZXJlbmNlL2RhdGFtb2RlbC5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJUaGUgdmFsdWUgb2Ygc29tZSBvYmplY3RzIGNhbiBjaGFuZ2UuIE9iamVjdHMgd2hvc2UgdmFsdWUgY2FuIGNoYW5nZSBhcmUgc2FpZCB0byBiZSBtdXRhYmxlOyBvYmplY3RzIHdob3NlIHZhbHVlIGlzIHVuY2hhbmdlYWJsZSBvbmNlIHRoZXkgYXJlIGNyZWF0ZWQgYXJlIGNhbGxlZCBpbW11dGFibGUuIChUaGUgdmFsdWUgb2YgYW4gaW1tdXRhYmxlIGNvbnRhaW5lciBvYmplY3QgdGhhdCBjb250YWlucyBhIHJlZmVyZW5jZSB0byBhIG11dGFibGUgb2JqZWN0IGNhbiBjaGFuZ2Ugd2hlbiB0aGUgbGF0dGVyXFx1MjAxOXMgdmFsdWUgaXMgY2hhbmdlZDsgaG93ZXZlciB0aGUgY29udGFpbmVyIGlzIHN0aWxsIGNvbnNpZGVyZWQgaW1tdXRhYmxlLCBiZWNhdXNlIHRoZSBjb2xsZWN0aW9uIG9mIG9iamVjdHMgaXQgY29udGFpbnMgY2Fubm90IGJlIGNoYW5nZWQuIFNvLCBpbW11dGFiaWxpdHkgaXMgbm90IHN0cmljdGx5IHRoZSBzYW1lIGFzIGhhdmluZyBhbiB1bmNoYW5nZWFibGUgdmFsdWUsIGl0IGlzIG1vcmUgc3VidGxlLikgQW4gb2JqZWN0XFx1MjAxOXMgbXV0YWJpbGl0eSBpcyBkZXRlcm1pbmVkIGJ5IGl0cyB0eXBlOyBmb3IgaW5zdGFuY2UsIG51bWJlcnMsIHN0cmluZ3MgYW5kIHR1cGxlcyBhcmUgaW1tdXRhYmxlLCB3aGlsZSBkaWN0aW9uYXJpZXMgYW5kIGxpc3RzIGFyZSBtdXRhYmxlLlxcbi4uLlxcblNvbWUgb2JqZWN0cyBjb250YWluIHJlZmVyZW5jZXMgdG8gb3RoZXIgb2JqZWN0czsgdGhlc2UgYXJlIGNhbGxlZCBjb250YWluZXJzLiBFeGFtcGxlcyBvZiBjb250YWluZXJzIGFyZSB0dXBsZXMsIGxpc3RzIGFuZCBkaWN0aW9uYXJpZXMuIFRoZSByZWZlcmVuY2VzIGFyZSBwYXJ0IG9mIGEgY29udGFpbmVyXFx1MjAxOXMgdmFsdWUuIEluIG1vc3QgY2FzZXMsIHdoZW4gd2UgdGFsayBhYm91dCB0aGUgdmFsdWUgb2YgYSBjb250YWluZXIsIHdlIGltcGx5IHRoZSB2YWx1ZXMsIG5vdCB0aGUgaWRlbnRpdGllcyBvZiB0aGUgY29udGFpbmVkIG9iamVjdHM7IGhvd2V2ZXIsIHdoZW4gd2UgdGFsayBhYm91dCB0aGUgbXV0YWJpbGl0eSBvZiBhIGNvbnRhaW5lciwgb25seSB0aGUgaWRlbnRpdGllcyBvZiB0aGUgaW1tZWRpYXRlbHkgY29udGFpbmVkIG9iamVjdHMgYXJlIGltcGxpZWQuIFNvLCBpZiBhbiBpbW11dGFibGUgY29udGFpbmVyIChsaWtlIGEgdHVwbGUpIGNvbnRhaW5zIGEgcmVmZXJlbmNlIHRvIGEgbXV0YWJsZSBvYmplY3QsIGl0cyB2YWx1ZSBjaGFuZ2VzIGlmIHRoYXQgbXV0YWJsZSBvYmplY3QgaXMgY2hhbmdlZC5cXG4uLi5cXG4jIyMgMy4yLjUuIFNlcXVlbmNlc1xcdTAwYjZcXG4uLi5cXG4jIyMjIDMuMi41LjEuIEltbXV0YWJsZSBzZXF1ZW5jZXNcXHUwMGI2XFxuXFxuQW4gb2JqZWN0IG9mIGFuIGltbXV0YWJsZSBzZXF1ZW5jZSB0eXBlIGNhbm5vdCBjaGFuZ2Ugb25jZSBpdCBpcyBjcmVhdGVkLiAoSWYgdGhlIG9iamVjdCBjb250YWlucyByZWZlcmVuY2VzIHRvIG90aGVyIG9iamVjdHMsIHRoZXNlIG90aGVyIG9iamVjdHMgbWF5IGJlIG11dGFibGUgYW5kIG1heSBiZSBjaGFuZ2VkOyBob3dldmVyLCB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGRpcmVjdGx5IHJlZmVyZW5jZWQgYnkgYW4gaW1tdXRhYmxlIG9iamVjdCBjYW5ub3QgY2hhbmdlLilcXG5cXG5UaGUgZm9sbG93aW5nIHR5cGVzIGFyZSBpbW11dGFibGUgc2VxdWVuY2VzOlxcbi4uLlxcblR1cGxlc1xcbjogVGhlIGl0ZW1zIG9mIGEgYHR1cGxlYCBhcmUgYXJiaXRyYXJ5IFB5dGhvbiBvYmplY3RzLiBUdXBsZXMgb2YgdHdvIG9yIG1vcmUgaXRlbXMgYXJlIGZvcm1lZCBieSBjb21tYS1zZXBhcmF0ZWQgbGlzdHMgb2YgZXhwcmVzc2lvbnMuIEEgdHVwbGUgb2Ygb25lIGl0ZW0gKGEgXFx1MjAxOHNpbmdsZXRvblxcdTIwMTkpIGNhbiBiZSBmb3JtZWQgYnkgYWZmaXhpbmcgYSBjb21tYSB0byBhbiBleHByZXNzaW9uIChhbiBleHByZXNzaW9uIGJ5IGl0c2VsZiBkb2VzIG5vdCBjcmVhdGUgYSB0dXBsZSwgc2luY2UgcGFyZW50aGVzZXMgbXVzdCBiZSB1c2FibGUgZm9yIGdyb3VwaW5nIG9mIGV4cHJlc3Npb25zKS4gQW4gZW1wdHkgdHVwbGUgY2FuIGJlIGZvcm1lZCBieSBhbiBlbXB0eSBwYWlyIG9mIHBhcmVudGhlc2VzLlxcbi4uLlxcbiMjIyMgMy4yLjUuMi4gTXV0YWJsZSBzZXF1ZW5jZXNcXHUwMGI2XFxuXFxuTXV0YWJsZSBzZXF1ZW5jZXMgY2FuIGJlIGNoYW5nZWQgYWZ0ZXIgdGhleSBhcmUgY3JlYXRlZC4gVGhlIHN1YnNjcmlwdGlvbiBhbmQgc2xpY2luZyBub3RhdGlvbnMgY2FuIGJlIHVzZWQgYXMgdGhlIHRhcmdldCBvZiBhc3NpZ25tZW50IGFuZCBgZGVsYCAoZGVsZXRlKSBzdGF0ZW1lbnRzLlxcbi4uLlxcblRoZXJlIGFyZSBjdXJyZW50bHkgdHdvIGludHJpbnNpYyBtdXRhYmxlIHNlcXVlbmNlIHR5cGVzOlxcblxcbkxpc3RzXFxuOiBUaGUgaXRlbXMgb2YgYSBsaXN0IGFyZSBhcmJpdHJhcnkgUHl0aG9uIG9iamVjdHMuIExpc3RzIGFyZSBmb3JtZWQgYnkgcGxhY2luZyBhIGNvbW1hLXNlcGFyYXRlZCBsaXN0IG9mIGV4cHJlc3Npb25zIGluIHNxdWFyZSBicmFja2V0cy4gKE5vdGUgdGhhdCB0aGVyZSBhcmUgbm8gc3BlY2lhbCBjYXNlcyBuZWVkZWQgdG8gZm9ybSBsaXN0cyBvZiBsZW5ndGggMCBvciAxLilcIl19LCB7XCJ0aXRsZVwiOiBcIkJ1aWx0LWluIFR5cGVzIFxcdTIwMTQgUHl0aG9uIDMuMTQuNyBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9idWlsdGlucy9zdGR0eXBlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJjb2xsZWN0aW9uIGNsYXNzZXMgYXJlIG11dGFibGUuIFRoZSBtZXRob2RzIHRoYXQgYWRkLCBzdWJ0cmFjdCwgb3IgcmVhcnJhbmdlIHRoZWlyIC4uLiBpbiBwbGFjZSwgYW5kIGRvblxcdTIwMTl0IHJldHVybiBhIC4uLiAsIG5ldmVyIHJldHVybiB0aGUgY29sbGVjdGlvbiBpbnN0YW5jZSBpdHNlbGYgYnV0IGBOb25lYC5cXG4uLi5cXG4jIyBTZXF1ZW5jZSBUeXBlcyBcXHUyMDE0IGBsaXN0YCwgYHR1cGxlYCwgYHJhbmdlYFxcdTAwYjZcXG5cXG5UaGVyZSBhcmUgdGhyZWUgYmFzaWMgc2VxdWVuY2UgdHlwZXM6IGxpc3RzLCB0dXBsZXMsIGFuZCByYW5nZSBvYmplY3RzLiBBZGRpdGlvbmFsIHNlcXVlbmNlIHR5cGVzIHRhaWxvcmVkIGZvciBwcm9jZXNzaW5nIG9mIGJpbmFyeSBkYXRhIGFuZCB0ZXh0IHN0cmluZ3MgYXJlIGRlc2NyaWJlZCBpbiBkZWRpY2F0ZWQgc2VjdGlvbnMuXFxuLi4uXFxuVGhlIG9wZXJhdGlvbnMgaW4gdGhlIGZvbGxvd2luZyB0YWJsZSAuLi4gbXV0YWJsZSBhbmQgaW1tdXRhYmxlLiBUaGUgYGNvbGxlY3Rpb25zLiAuLi4gaXMgcHJvdmlkZWQgdG8gbWFrZSAuLi4gZWFzaWVyIHRvIGNvcnJlY3RseSBpbXBsZW1lbnQgdGhlc2Ugb3BlcmF0aW9ucyBvbiBjdXN0b20gc2VxdWVuY2UgdHlwZXNcXG4uLi5cXG4jIyMgSW1tdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbm11dGFibGUgc2VxdWVuY2UgdHlwZXMgaXNcXG4uLi5cXG5zdXBwb3J0IGFsbG93cyBpbW11dGFibGUgc2VxdWVuY2VzLCAuLi4gLCB0byBiZSB1c2VkIGFzIGBkaWN0YCBrZXlzIGFuZCBzdG9yZWQgaW4gLi4uIGVuc2V0YCBpbnN0YW5jZXNcXG4uLi5cXG4jIyMgTXV0YWJsZSBTZXF1ZW5jZSBUeXBlc1xcdTAwYjZcXG4uLi5cXG4jIyMgTGlzdHNcXHUwMGI2XFxuXFxuTGlzdHMgYXJlIG11dGFibGUgc2VxdWVuY2VzLCB0eXBpY2FsbHkgdXNlZCB0byBzdG9yZSBjb2xsZWN0aW9ucyBvZiBob21vZ2VuZW91cyBpdGVtcyAod2hlcmUgdGhlIHByZWNpc2UgZGVncmVlIG9mIHNpbWlsYXJpdHkgd2lsbCB2YXJ5IGJ5IGFwcGxpY2F0aW9uKS5cXG4uLi5cXG4jIyMgVHVwbGVzXFx1MDBiNlxcblxcblR1cGxlcyBhcmUgaW1tdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaGV0ZXJvZ2VuZW91cyBkYXRhIChzdWNoIGFzIHRoZSAyLXR1cGxlcyBwcm9kdWNlZCBieSB0aGUgYGVudW1lcmF0ZSgpYCBidWlsdC1pbikuIFR1cGxlcyBhcmUgYWxzbyB1c2VkIGZvciBjYXNlcyB3aGVyZSBhbiBpbW11dGFibGUgc2VxdWVuY2Ugb2YgaG9tb2dlbmVvdXMgZGF0YSBpcyBuZWVkZWQgKHN1Y2ggYXMgYWxsb3dpbmcgc3RvcmFnZSBpbiBhIGBzZXRgIG9yIGBkaWN0YCBpbnN0YW5jZSkuXFxuLi4uXFxudHVwbGUoaXRlcmFibGU9XFxuLi4uXFxuVGhlIGNvbnN0cnVjdG9yIGJ1aWxkcyBhIHR1cGxlIHdob3NlIGl0ZW1zIGFyZSB0aGUgc2FtZSBhbmQgaW4gdGhlIHNhbWUgb3JkZXIgYXMgaXRlcmFibGVcXHUyMDE5cyBpdGVtcy4gaXRlcmFibGUgbWF5IGJlIGVpdGhlciBhIHNlcXVlbmNlLCBhIGNvbnRhaW5lciB0aGF0IHN1cHBvcnRzIGl0ZXJhdGlvbiwgb3IgYW4gaXRlcmF0b3Igb2JqZWN0LiBJZiBpdGVyYWJsZSBpcyBhbHJlYWR5IGEgdHVwbGUsIGl0IGlzIHJldHVybmVkIHVuY2hhbmdlZC4gRm9yIGV4YW1wbGUsIGB0dXBsZSgnYWJjJylgIHJldHVybnMgYCgnYScsICdiJywgJ2MnKWAgYW5kIGB0dXBsZSggWzEsIDIsIDNdIClgIHJldHVybnMgYCgxLCAyLCAzKWAuIElmIG5vIGFyZ3VtZW50IGlzIGdpdmVuLCB0aGUgY29uc3RydWN0b3IgY3JlYXRlcyBhIG5ldyBlbXB0eSB0dXBsZSwgYCgpYC5cXG4uLi5cXG5UdXBsZXMgaW1wbGVtZW50IGFsbCBvZiB0aGUgY29tbW9uIHNlcXVlbmNlIG9wZXJhdGlvbnMuXFxuLi4uXFxuRm9yIGhldGVyb2dlbmVvdXMgY29sbGVjdGlvbnMgb2YgZGF0YSB3aGVyZSBhY2Nlc3MgYnkgbmFtZSBpcyBjbGVhcmVyIHRoYW4gYWNjZXNzIGJ5IGluZGV4LCBgY29sbGVjdGlvbnMubmFtZWR0dXBsZSgpYCBtYXkgYmUgYSBtb3JlIGFwcHJvcHJpYXRlIGNob2ljZSB0aGFuIGEgc2ltcGxlIHR1cGxlIG9iamVjdC5cIl19LCB7XCJ0aXRsZVwiOiBcIkJ1aWx0LWluIFR5cGVzIFxcdTIwMTQgUHl0aG9uIDMuMTQuNSBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9saWJyYXJ5L3N0ZHR5cGVzLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcIlNvbWUgY29sbGVjdGlvbiBjbGFzc2VzIGFyZSBtdXRhYmxlLiBUaGUgbWV0aG9kcyB0aGF0IGFkZCwgc3VidHJhY3QsIG9yIHJlYXJyYW5nZSB0aGVpciAuLi4gaW4gcGxhY2UsIGFuZCBkb25cXHUyMDE5dCByZXR1cm4gYSAuLi4gLCBuZXZlciByZXR1cm4gdGhlIGNvbGxlY3Rpb24gaW5zdGFuY2UgaXRzZWxmIGJ1dCBgTm9uZWAuXFxuLi4uXFxuIyMgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBgbGlzdGAsIGB0dXBsZWAsIGByYW5nZWBcXHUwMGI2XFxuXFxuVGhlcmUgYXJlIHRocmVlIGJhc2ljIHNlcXVlbmNlIHR5cGVzOiBsaXN0cywgdHVwbGVzLCBhbmQgLi4uIG9iamVjdHMuIEFkZGl0aW9uYWwgc2VxdWVuY2UgdHlwZXMgdGFpbG9yZWQgZm9yIHByb2Nlc3Npbmcgb2YgYmluYXJ5IGRhdGEgYW5kIHRleHQgc3RyaW5ncyBhcmUgZGVzY3JpYmVkIGluIGRlZGljYXRlZCBzZWN0aW9ucy5cXG4uLi5cXG5UaGUgb3BlcmF0aW9ucyBpbiB0aGUgZm9sbG93aW5nIHRhYmxlIC4uLiBpbW11dGFibGUuIFRoZSBgIC4uLiBpcyBwcm92aWRlZCB0byBtYWtlIC4uLiBlYXNpZXIgdG8gY29ycmVjdGx5IGltcGxlbWVudCB0aGVzZSBvcGVyYXRpb25zIG9uIGN1c3RvbSBzZXF1ZW5jZSB0eXBlc1xcbi4uLlxcbiMjIyBJbW11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxub3BlcmF0aW9uIHRoYXQgaW1tdXRhYmxlIHNlcXVlbmNlIC4uLiBieSBtdXRhYmxlIHNlcXVlbmNlIHR5cGVzIGlzIC4uLiBgaGFzaCgpYFxcbi4uLlxcblRoaXMgc3VwcG9ydCBhbGxvd3MgaW1tdXRhYmxlIHNlcXVlbmNlcywgc3VjaCBhcyBgdHVwbGVgIGluc3RhbmNlcywgdG8gYmUgdXNlZCBhcyBgZGljdGAga2V5cyBhbmQgc3RvcmVkIGluIGBzZXRgIGFuZCBgZnJvemVuc2V0YCBpbnN0YW5jZXMuXFxuLi4uXFxuIyMjIE11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxuIyMjIExpc3RzXFx1MDBiNlxcblxcbkxpc3RzIGFyZSBtdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaG9tb2dlbmVvdXMgaXRlbXMgKHdoZXJlIHRoZSBwcmVjaXNlIGRlZ3JlZSBvZiBzaW1pbGFyaXR5IHdpbGwgdmFyeSBieSBhcHBsaWNhdGlvbikuXFxuLi4uXFxuIyMjIFR1cGxlc1xcdTAwYjZcXG5cXG5UdXBsZXMgYXJlIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhldGVyb2dlbmVvdXMgZGF0YSAoc3VjaCBhcyB0aGUgMi10dXBsZXMgcHJvZHVjZWQgYnkgdGhlIGBlbnVtZXJhdGUoKWAgYnVpbHQtaW4pLiBUdXBsZXMgYXJlIGFsc28gdXNlZCBmb3IgY2FzZXMgd2hlcmUgYW4gaW1tdXRhYmxlIHNlcXVlbmNlIG9mIGhvbW9nZW5lb3VzIGRhdGEgaXMgbmVlZGVkIChzdWNoIGFzIGFsbG93aW5nIHN0b3JhZ2UgaW4gYSBgc2V0YCBvciBgZGljdGAgaW5zdGFuY2UpLlxcbi4uLlxcblRoZSBjb25zdHJ1Y3RvciBidWlsZHMgYSB0dXBsZSB3aG9zZSBpdGVtcyBhcmUgdGhlIHNhbWUgYW5kIGluIHRoZSBzYW1lIG9yZGVyIGFzIGl0ZXJhYmxlXFx1MjAxOXMgaXRlbXMuIGl0ZXJhYmxlIG1heSBiZSBlaXRoZXIgYSBzZXF1ZW5jZSwgYSBjb250YWluZXIgdGhhdCBzdXBwb3J0cyBpdGVyYXRpb24sIG9yIGFuIGl0ZXJhdG9yIG9iamVjdC4gSWYgaXRlcmFibGUgaXMgYWxyZWFkeSBhIHR1cGxlLCBpdCBpcyByZXR1cm5lZCB1bmNoYW5nZWQuIEZvciBleGFtcGxlLCBgdHVwbGUoJ2FiYycpYCByZXR1cm5zIGAoJ2EnLCAnYicsICdjJylgIGFuZCBgdHVwbGUoIFsxLCAyLCAzXSApYCByZXR1cm5zIGAoMSwgMiwgMylgLiBJZiBubyBhcmd1bWVudCBpcyBnaXZlbiwgdGhlIGNvbnN0cnVjdG9yIGNyZWF0ZXMgYSBuZXcgZW1wdHkgdHVwbGUsIGAoKWAuXFxuLi4uXFxuVHVwbGVzIGltcGxlbWVudCBhbGwgb2YgdGhlIGNvbW1vbiBzZXF1ZW5jZSBvcGVyYXRpb25zLlxcbi4uLlxcbkZvciBoZXRlcm9nZW5lb3VzIGNvbGxlY3Rpb25zIG9mIGRhdGEgd2hlcmUgYWNjZXNzIGJ5IG5hbWUgaXMgY2xlYXJlciB0aGFuIGFjY2VzcyBieSBpbmRleCwgYGNvbGxlY3Rpb25zLm5hbWVkdHVwbGUoKWAgbWF5IGJlIGEgbW9yZSBhcHByb3ByaWF0ZSBjaG9pY2UgdGhhbiBhIHNpbXBsZSB0dXBsZSBvYmplY3QuXCJdfSwge1widGl0bGVcIjogXCI1LiBEYXRhIFN0cnVjdHVyZXMgXFx1MjAxNCBQeXRob24gMy4xMC4yMCBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy4xMC90dXRvcmlhbC9kYXRhc3RydWN0dXJlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJZb3UgbWlnaHQgaGF2ZSBub3RpY2VkIHRoYXQgbWV0aG9kcyBsaWtlIGBpbnNlcnRgLCBgcmVtb3ZlYCBvciBgc29ydGAgdGhhdCBvbmx5IG1vZGlmeSB0aGUgbGlzdCBoYXZlIG5vIHJldHVybiB2YWx1ZSBwcmludGVkIFxcdTIwMTMgdGhleSByZXR1cm4gdGhlIGRlZmF1bHQgYE5vbmVgLiAxIFRoaXMgaXMgYSBkZXNpZ24gcHJpbmNpcGxlIGZvciBhbGwgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgaW4gUHl0aG9uLlxcbi4uLlxcbiMjIDUuMy4gVHVwbGVzIGFuZCBTZXF1ZW5jZXNcXHUwMGI2XFxuXFxuV2Ugc2F3IHRoYXQgbGlzdHMgYW5kIHN0cmluZ3MgaGF2ZSBtYW55IGNvbW1vbiBwcm9wZXJ0aWVzLCBzdWNoIGFzIGluZGV4aW5nIGFuZCBzbGljaW5nIG9wZXJhdGlvbnMuIFRoZXkgYXJlIHR3byBleGFtcGxlcyBvZiBzZXF1ZW5jZSBkYXRhIHR5cGVzIChzZWUgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBsaXN0LCB0dXBsZSwgcmFuZ2UpLiBTaW5jZSBQeXRob24gaXMgYW4gZXZvbHZpbmcgbGFuZ3VhZ2UsIG90aGVyIHNlcXVlbmNlIGRhdGEgdHlwZXMgbWF5IGJlIGFkZGVkLiBUaGVyZSBpcyBhbHNvIGFub3RoZXIgc3RhbmRhcmQgc2VxdWVuY2UgZGF0YSB0eXBlOiB0aGUgdHVwbGUuXFxuXFxuQSB0dXBsZSBjb25zaXN0cyBvZiBhIG51bWJlciBvZiB2YWx1ZXMgc2VwYXJhdGVkIGJ5IGNvbW1hcywgZm9yIGluc3RhbmNlOlxcbi4uLlxcbj4+PiAjIFR1cGxlcyBhcmUgaW1tdXRhYmxlOlxcbi4uLiB0WzBdID0gODg4ODhcXG5UcmFjZWJhY2sgKG1vc3QgcmVjZW50IGNhbGwgbGFzdCk6XFxuICBGaWxlIFxcXCI8c3RkaW4+XFxcIiwgbGluZSAxLCBpbiA8bW9kdWxlPlxcblR5cGVFcnJvcjogJ3R1cGxlJyBvYmplY3QgZG9lcyBub3Qgc3VwcG9ydCBpdGVtIGFzc2lnbm1lbnRcXG4+Pj4gIyBidXQgdGhleSBjYW4gY29udGFpbiBtdXRhYmxlIG9iamVjdHM6XFxuLi4uIHYgPSAoWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuPj4+IHZcXG4oWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuLi4uXFxuQXMgeW91IHNlZSwgb24gb3V0cHV0IHR1cGxlcyBhcmUgYWx3YXlzIGVuY2xvc2VkIGluIHBhcmVudGhlc2VzLCBzbyB0aGF0IG5lc3RlZCB0dXBsZXMgYXJlIGludGVycHJldGVkIGNvcnJlY3RseTsgdGhleSBtYXkgYmUgaW5wdXQgd2l0aCBvciB3aXRob3V0IHN1cnJvdW5kaW5nIHBhcmVudGhlc2VzLCBhbHRob3VnaCBvZnRlbiBwYXJlbnRoZXNlcyBhcmUgbmVjZXNzYXJ5IGFueXdheSAoaWYgdGhlIHR1cGxlIGlzIHBhcnQgb2YgYSBsYXJnZXIgZXhwcmVzc2lvbikuIEl0IGlzIG5vdCBwb3NzaWJsZSB0byBhc3NpZ24gdG8gdGhlIGluZGl2aWR1YWwgaXRlbXMgb2YgYSB0dXBsZSwgaG93ZXZlciBpdCBpcyBwb3NzaWJsZSB0byBjcmVhdGUgdHVwbGVzIHdoaWNoIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzLCBzdWNoIGFzIGxpc3RzLlxcblxcblRob3VnaCB0dXBsZXMgbWF5IHNlZW0gc2ltaWxhciB0byBsaXN0cywgdGhleSBhcmUgb2Z0ZW4gdXNlZCBpbiBkaWZmZXJlbnQgc2l0dWF0aW9ucyBhbmQgZm9yIGRpZmZlcmVudCBwdXJwb3Nlcy4gVHVwbGVzIGFyZSBpbW11dGFibGUsIGFuZCB1c3VhbGx5IGNvbnRhaW4gYSBoZXRlcm9nZW5lb3VzIHNlcXVlbmNlIG9mIGVsZW1lbnRzIHRoYXQgYXJlIGFjY2Vzc2VkIHZpYSB1bnBhY2tpbmcgKHNlZSBsYXRlciBpbiB0aGlzIHNlY3Rpb24pIG9yIGluZGV4aW5nIChvciBldmVuIGJ5IGF0dHJpYnV0ZSBpbiB0aGUgY2FzZSBvZiBgbmFtZWR0dXBsZXNgKS4gTGlzdHMgYXJlIG11dGFibGUsIGFuZCB0aGVpciBlbGVtZW50cyBhcmUgdXN1YWxseSBob21vZ2VuZW91cyBhbmQgYXJlIGFjY2Vzc2VkIGJ5IGl0ZXJhdGluZyBvdmVyIHRoZSBsaXN0Llxcbi4uLlxcbiMjIDUuNC5cXG4uLi5cXG4uIFNldCBvYmplY3RzXFxuLi4uXFxuIyMgNS41LiBEaWN0aW9uYXJpZXNcXHUwMGI2XFxuXFxuQW5vdGhlciB1c2VmdWwgZGF0YSB0eXBlIGJ1aWx0IGludG8gUHl0aG9uIGlzIHRoZSBkaWN0aW9uYXJ5IChzZWUgTWFwcGluZyBUeXBlcyBcXHUyMDE0IGRpY3QpLiBEaWN0aW9uYXJpZXMgYXJlIHNvbWV0aW1lcyBmb3VuZCBpbiBvdGhlciBsYW5ndWFnZXMgYXMgXFx1MjAxY2Fzc29jaWF0aXZlIG1lbW9yaWVzXFx1MjAxZCBvciBcXHUyMDFjYXNzb2NpYXRpdmUgYXJyYXlzXFx1MjAxZC4gVW5saWtlIHNlcXVlbmNlcywgd2hpY2ggYXJlIGluZGV4ZWQgYnkgYSByYW5nZSBvZiBudW1iZXJzLCBkaWN0aW9uYXJpZXMgYXJlIGluZGV4ZWQgYnkga2V5cywgd2hpY2ggY2FuIGJlIGFueSBpbW11dGFibGUgdHlwZTsgc3RyaW5ncyBhbmQgbnVtYmVycyBjYW4gYWx3YXlzIGJlIGtleXMuIFR1cGxlcyBjYW4gYmUgdXNlZCBhcyBrZXlzIGlmIHRoZXkgY29udGFpbiBvbmx5IHN0cmluZ3MsIG51bWJlcnMsIG9yIHR1cGxlczsgaWYgYSB0dXBsZSBjb250YWlucyBhbnkgbXV0YWJsZSBvYmplY3QgZWl0aGVyIGRpcmVjdGx5IG9yIGluZGlyZWN0bHksIGl0IGNhbm5vdCBiZSB1c2VkIGFzIGEga2V5LiBZb3UgY2FuXFx1MjAxOXQgdXNlIGxpc3RzIGFzIGtleXMsIHNpbmNlIGxpc3RzIGNhbiBiZSBtb2RpZmllZCBpbiBwbGFjZSB1c2luZyBpbmRleCBhc3NpZ25tZW50cywgc2xpY2UgYXNzaWdubWVudHMsIG9yIG1ldGhvZHMgbGlrZSBgYXBwZW5kKClgIGFuZCBgZXh0ZW5kKClgLlwiXX1dIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJ0b29sIiwibmFtZSI6IndlYl9zZWFyY2giLCJpZCI6IjZlNTEyNWFjLTMxNjQtNGE3Ni05YzUyLWFjMzQ2MGNlMzZlNCIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsInN0YXR1cyI6InN1Y2Nlc3MifSx7ImNvbnRlbnQiOlt7ImlkIjoidG9vbHVfMDFYamozYVJkV3loQkpmUG1USkJ4UWE5IiwiY2FsbGVyIjp7InR5cGUiOiJkaXJlY3QifSwiaW5wdXQiOnt9LCJuYW1lIjoidHJhbnNmZXJfdG9fc2tlcHRpYyIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHdBNkY0Y0NONXloRDRDcnM5TSIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo3Mzc4LCJvdXRwdXRfdG9rZW5zIjozMywib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6MH0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoicmVzZWFyY2hlciIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy0zY2Q4LTc5ZjEtODhhMS1jZTZjZWUyOTRlNzktMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ0cmFuc2Zlcl90b19za2VwdGljIiwiYXJncyI6e30sImlkIjoidG9vbHVfMDFYamozYVJkV3loQkpmUG1USkJ4UWE5IiwidHlwZSI6InRvb2xfY2FsbCJ9XSwiaW52YWxpZF90b29sX2NhbGxzIjpbXSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjo3Mzc4LCJvdXRwdXRfdG9rZW5zIjozMywidG90YWxfdG9rZW5zIjo3NDExLCJpbnB1dF90b2tlbl9kZXRhaWxzIjp7ImNhY2hlX3JlYWQiOjAsImNhY2hlX2NyZWF0aW9uIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VuX2RldGFpbHMiOnsicmVhc29uaW5nIjowfX19LHsiY29udGVudCI6IlN1Y2Nlc3NmdWxseSB0cmFuc2ZlcnJlZCB0byBza2VwdGljIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJ0b29sIiwibmFtZSI6InRyYW5zZmVyX3RvX3NrZXB0aWMiLCJpZCI6IjM4OTA1OGQxLTA4M2YtNDYzMi1iYmM1LTA5YmZkM2EwZmZkMiIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsInN0YXR1cyI6InN1Y2Nlc3MifSx7ImNvbnRlbnQiOlt7InNpZ25hdHVyZSI6IkNBUVMxUWdLRUFnU0dBSTRBVUlJZEdocGJtdHBibWNTRE42VGZVNk5ETTk3MWxFYnp4b012UlVBMUo5b3gzZkpRWVQ5SWpBckNRUUdIQlA0cFJIUGF2WHpSbGJNYmFZSEM1N1lqK0dieGdyMGhlbHZkMk9JTC9vNlJidDRQRkEzRmFJRlVsZ3E4Z2M3S1FJOUZ0R252U0FrTEJSeklzVXAvMmQ1UmQyemdFc2ZrRlZlWHNuOFdZWVpraW5BUVZWeTZBN0IzTGFFSlcweGIveDg4RGk2N2ZlZHd4MFErRmxadHdjbVQzRGNhYTExSnZleUhScjNkbSt6R1NaUDZDWGVYTko4bnJUWjdXMVlSVHlzRDZ3N283THA4aDFSd3E0ZjRvOGorUVVmWTB2amUrekpac3hIUG1RTnQzeWtlS2g5OWhnbjZkYml6Ni9FSHFtYkhqU0F1MU00U3FFRGZxSUtUaEhZZW5hbUFabWNRNFhBT282bWRRNis3N1ZncFYxbEMxL2s1MklpVW9KamtHbFFybkxDdnV0aUk3S04rc2dLOGMyKzJLQ0pyUFJDdlZWK0JrU3R2UDVqV1dxenl0cEtRK25IemhMNDQ4T3JJQTlnZEpDMzlDOGVEclhlUm5JN3Q0RnFsa1hHb01sWkFWOVVNY3JRSFJ4T01pOHRPeVR6aTlveUJGRFRTc1ErTEpxZXIwdHoxRnQ5Z2VrNnZ6ZzUwU3BWQUZRZExFUnY4UjFZcU1hZUkreHN2VEFFVzlNUmd2Uk1rekx1elJUMmd1dzNqRCtGamlwMzd4Z0s1UUkwUitDc09BbVFQb1RaaXp6Y0l5QVVBdUNKRkNncS9nRC9DaWJMeG95bWlWVUkxYW45U250M0twTlJvV2R2V0luZTMzVjBwdjhQSGc5eVcySnVMT3FRR2lsTENzSy9RM29RcjN1cEVNWHhYR2pDTDdXVWl6dlJaR0ZFRVYrVnBsWXQ3djR0S1pNajVwazN2ZGVNME1KNVhaZzdQdlVzUnp5Y3hjbnJUM0tub2MzcU0rdHRxWmZUc2I4ZDZVL3BRaWZxN0d1YWhGT0hydnJvcWdKTGorLzgxWXkxcFpGa2pmSmdZMFhrbHpBbkE2Q0ZxL1lUMGd4YlYrVEg1SVMrV2FZT1V6SzM2UzhzNXFxTXdobmd2NEl6RTNjdFhYcW5oSVpYTnpueVZCcWJZYS9HeDJiQ2pjRWoybjVSeUdZUlMvK01jdWxnTEJ6blE5TWtHMkQ3UVAzTXEvbndIWEtNSWFMUzhWSzhoRzNkMHJBamx2bmF1WFNoTnJKeHRrajRxa3dueEQ0THJFamsxdWUrUExjYkxxWVd3QXI4bjBzKzQxanJzSVBWeXlLOXZGYUVackE5S2FCeGtKeWlxeDUwR1pObjdja3grbWtJMThnSDUyaXlWUHRVOEZLTVhqdWdDbDZtUExpSFlpRFFkK0c2M0x1ZG1PS3lzOWl0Vm5CREJOTVAyYi9JNytiSVZSbGhlR2FaV3I2TUFUS2w4NkIzeSswa3VPckxSTXlXY2RpYmMyaVFrWm0rOGY3ZFRVd2JLYkJKc0tPRlk3My84bmxML0ppWXBqejZuVUlrK0lxU2phRnlLNlhGbENjdWtXcFdIcERKblN2YkV6ZXk3cUh5Q3FKK0krb244aVA4R090ZW1wUWFTMC80a1pHU3dsckU2eE9qNkV3R3BmamJEWktIa1AvMlJBZE5ZUFFxM3o3YmRWSk1KTm5GTXo2ZTM3bXpNcENCaWh3UGRTRlRSRTNTQ1k2Nnd2VGdKekdDVXloSGJTaTVpMWF6ek9YZDh1SENTcFQvNkwxbzZWVTBwam9SOEthYTR3Q1dIR2Y3UC9XRWQrYXZ5cDAySm51MWNtcFcwK0dxK3ZLOWgwdGRiMklDbE0vZU5mZG91UVArZXA4TUpVanJhRFJJQ21BSlFZNW9iUzcxdm5pT28rcmVhRnkwelJnQiIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJzaWduYXR1cmUiOiJDQVFTL1JFS0VRZ1NHQUk0QVVJSmJtRnljbUYwYVc5dUVnd0ZoM0FWSlpQM3M4eTFIcTBhREc3NnVoSE9OYVNjWEFHU2hpSXdHZ2pMWEhVeDF5WnJrTFBsamp6VE9ReWNwTEppS1Z3WGN4bS9FektKbTFzOE01V3h1UkVXRWxkWGNvYU4wL21zS3BrUk1UK2VMYjVpaDlyQUhVTFRvbVVOc1V3SmV1MGpySmU0R0xhenk3c1A1S3RqRU9FQktRc1hVZm9RN2ZEa1pPUVVIWmk5enpRTjRoRk5tWUpVZjNPVnFmOVRRTmRDMSttLy9IQVpQZldtQXk2SHREdm03a2dCSmtDRW9pRm5tN3RuL1BYMWhYR1BSZ1djNkJUWFVNcjJXQThhS3lxZ2dSVWJNSDIrSll2NWlXWmRkQ2MxdXAxTmJyM3NGN1dQY290OVpCaHlIUVhyNjkyemVKazZTSmd5Z0lWckFTYk1seTJiQnhUV0pCNjIzZ1ZnenBDbVZTTFpGcGtLWGdQY3A4aEd5REtPdkY2U2lRU0hxR1J4UW9mcjdWczFhUGhmWml4Uk9KR0QrTWxya2dnVkFuR015WUlEVVpwYXZ0Q2VpVTFKbUJTejAwODRiWFB0VC90bnBzS09uU1BZWlFvM0IwQlhLYnFBL1dzS25nOC9WM1o4VXVham82SVZaMmhnQTFXRGR1cVFPUWtFeEdVNnBGdUxoZWJJanVXeFNOSS9EZmZ2Ui8vTE5xQ2Z2ZG5mMGh2b1B0RHhmeC9TYXdJeDQvc3BxOXhid0Z6SHRMZ29IZzFGMndGdzdjbFBEbGcxUHlEL3YyeUpHM1Y5VDhHTDZqR1BFNnJoZmlsL3U0SE52ZjdCVzBkUXpDVFRYY3FaeTRuQnl5MCtwK1M3WnhmTWw0S3B4YTJnT2hoU05qZnBmdVgwUXBMNGtNRThKMW5saUZOMWQ4S3FFUHVESTE4QlFIVWhEL1VmWTI5b0EweDUxMEZqTS9Cc0xaNG41c3Bwa29JaTdDRm9RcTJhQUtWTmZDMUtpbGhJcFJEKzFkeUR0WlFrbE44Z1pwT0VTSHc2TUwxVHRVMHlmQWNibXFveEo2aGhqendINkNNc1RRRUV5OE1iUkJNK1FLS2tJUmRSQ1BIR0ZXMkxrdVI1MCtKMHBGL3RyNnBxVnZKZW96ZmlNbnJFaWxCMjdUWlA0bktUSkc4cmQvL3ZDV2VuMHk3Q0EvOGRCSjlnZHA1dHg3TlZBNDVrMWdkY3hFUjFnWW91TlgzbjF2RzMvQjFGNCs5M3dYVzlNcW82VXM1OVRRbXJiOU5xYTdwU1FOU3JIMDZ3MUd4eUJBbGtGZnFBbGFmazE2YU1wcTQxNlRKWXBHeTQzV0NVaUNjTFdsaGlXZ3oyc2dPTEFYUDlhNXdlV0FLUkM4SjZqa2NVa0RlWG5XK0hGU2lQTTYrOUJvNmtWaExVaE41cmM0bUhaT1pwRjdwdFBwc1FONFhDcUxQc3FwUkgvOG81WHB5QzZabmVUZUYyRXh0amh3WlZqWER3TWJJelZZSWJ3MXkyUFBHdzhkQzdwYzRzSVZTbkk0b1FBSDB2NjUwZ2hmaG1NZXZXVzRoNFB5bGJ3L0RHNGZkZVQzKzh6MGY4TU9TVHRmaXB4MDUvUXR0SGxBdk9EUnhhNXlEQ3ZPejZuM2YxQk8wTi80TWJHeU9NMjdxVzR4OG1CVm9yeVVaV2ZzbWhmaTAvR2ZOdkI4K3lsVEdiWU5YMjdPY0VGNVdadE5xYldFeTdWNUdibVpVU3hzcGNnN0ZCWjNqMy8zTDdtdXZaWG9aaTFmNWVycTR5M2xTSCtvRjdxUEZudmlleUxPQ3d1YmNReStveW93ajRjRDBpZFJlY25wK3MrNTJENyttekYxZ3BXK2YrY0phc294aWdieGtPbG9TNHllaUt2Z000bldTcEdFajEyUlB4NW84MEVtemlYTjl0NHpKelcxaHRBNWhYSkplRnltcE1ZdHlDVUNldlJXVm1yUllrdFE3VW5DYTZ0TC9BVlBpMW5pNVhDWEhmTE91cWZLNUtMWXhqd1Z0ZGhoZm1DTHZwYUtWTkNFT0NmNG5OR0hxRDk4d1ViMGVqV053RFlCbkpHa0JSdzlSVnNwZndOZG9Xb1ZjbjJxdjh5d0dVcDd5Yy9zazJMcEk2VnppUnl2V3FiR05HMVpKcVZWdXRSYzQ2K3NPRTRWU2VTMi9DVE5LTmE2Tk1DZi9oeHhhVVZzWWZLZFArUzFMYzRpYW5LcDBzVXVnUHBtdG95ZEMwQ1AyUndqbkhMY256eENudnF4bExRV1hNTlBUc3UyeUJjSGxXSkx0Y21kZy9NY0xhZ3k3bDVrRzZzTjdaeEpMRC9rQXg4RTZ1cW9oUCtLL2pNU3JZdVFKbmhyNHRmNGJpV0ZZM0p6Zi9LcEFZeVZTcStwYmZGS1hNYkw5bkhFb1pRVDE1V3lhYmQ2RE1BcTNRWk90aE01cFV6ZE5xVVhrY3JJMVB3akFTQWc0ZFZVLzJvKzk4bVRYbnloUDdWbEVrVG5kZHB3eUpGK1lDQ2MwMXBxYTZaWDk5cG5SK1lPcElyYjVEdFBjS3EwdDVjRkNEeVF4TjFsVXVxWmNZdWp2WC9HQzcrNklwUlR6L05rRVkvNWNudE9mWkp5ZDZTa1V4L0pSNUFKSVJQQ05laWIwcmNXbm5GbjBYSzhzMjJ4NDB3TGpFdHVmWERPOSt5cmFmNGtIeGxCSTNTQjBtZU1qZXBYNTBDSmhBeDdOZWtrUkZud29lY0xmcUl5cmpYcmdBWnNUTVBpWDk0c25qZHJ6aXNBWHRCS2FudWxpbFN4T2ZXMkhTbWZuazBMMkdYSEpKM1MrdkNiZ1JFbWw5ZGVrZm1IaG13RVl0djNrZmh0aDdFUkl3VlQ4ajY5T3RJQkNtZVRJSnVBN283M1BYYzFMSjJ3aHVaSzdGWmU2TlRPczYxTVA3SlZsSWM2dGJodE9CazNOWFU4cEJQT0NZNlE1NWQ2M0Z0cFVVL0tDYlEwSzJXUTlvNWpOR3dOTko2Q3c1RkRDdCt4NVlKZVAxZDJRekRTaXErY1NacXdxQ1pWY05vQWZ0R0d6Z3JSWjR2a3BiUDB4UVpIQ3RXc1V3SFZUanJZNE5mUkRqSGRWUFJuOEo5WHRvb2MvaElXa3F6RmFjellUbGFwdTJSQ0pxZThhRW8zanpXWkNFSjdBaUJ6QllyYVlEZ09mZWlReWVKVXNiYlhIZEdLaWl3MjJCZHA0TStZejY3MU1aMGllOUVXUmtzMFEyREhHVVRuZTlTNHRBQUpQR2JVZjR4dENNb0o0MFE0UVcwNVFMU1p5M3dORkFVRlltWEU0RUNERkI4K0lFZU5GRkYvdGdBbmZoOEd2RmgvMXhZK0xFK296TW1pbmpTUndocTA5elFBRWtORnl1MElLZ3ZEa3lPSStpY2NGN2RvV0xUVWFzUHhMSDF4R1BCU0trZVdDblNvWC80QzJjOWR1NjZCYlJYYW1IcWFlK1ZDaVM2Qk45NTNpWUtPdThyTWtuZVcrVEZqVjBDekFmOFBORzlMQmF5VWdZNVV3OXgwU2FwNnpJNFBwYVpLSDRkOWt0a0ZVKzkxeUJiTjVTUnRkZDgvRVFhdEpYbUJOZnc3a1JybGwrTGp3WndlV0xFYjBqVDFScEd1RHFITGRMVE1NZ293U2N6cnAyekNzSmRRSC9zZWFZcnB3a0NGUjhkUmpNN0pSVWtWYTFBMFN2ZFpBWDc1dnVCdnZMQnpOMmMrUmJ0NW9xeTkzbHFJQklqS2I3dzJVa1JUYjRwQldjTktOUFVndDNhei9QeFF4RWFzYWx0ZG11eXZzWFlidXIzVlBObjhKek4vNko1TitRQ256NTcxVGNjYXBDbWtQdkZFK2dheVhVK1VGMVZLWXlGY1V2bGV2N2xzUWxtanF6SzJRQThYN1EzVHpGYXBxaHZKS2x0N2R6QkhyYXFaQmdQZ2I5TjRvS05heWk4RlpUNzEra2FZaEloRnJFU0tZUkhmSGtqNW4za2ZoZ0x5K3grakxEYkt4TWdna2dIbkp6OVJ1RUhaWEJXdGtZQVE9PSIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJpZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3ZlcmlmaWVyIiwidHlwZSI6InRvb2xfdXNlIiwidG9vbHNldF9uYW1lIjpudWxsfV0sImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2Zkd0FNUmRTMjFGZUpmelFoTTNzIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjczOTYsIm91dHB1dF90b2tlbnMiOjc3MCwib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6NzM5fSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCIsIm1vZGVsX3Byb3ZpZGVyIjoiYW50aHJvcGljIn0sInR5cGUiOiJhaSIsIm5hbWUiOiJza2VwdGljIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTRhZTktNzZjMC04OWE2LTRmMGIxMTRlMzAzMC0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6InRyYW5zZmVyX3RvX3ZlcmlmaWVyIiwiYXJncyI6e30sImlkIjoidG9vbHVfMDFINmZWOEp2UkpOdlh5NDdlUm5kM2pIIiwidHlwZSI6InRvb2xfY2FsbCJ9XSwiaW52YWxpZF90b29sX2NhbGxzIjpbXSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjo3Mzk2LCJvdXRwdXRfdG9rZW5zIjo3NzAsInRvdGFsX3Rva2VucyI6ODE2NiwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6NzM5fX19LHsiY29udGVudCI6IlN1Y2Nlc3NmdWxseSB0cmFuc2ZlcnJlZCB0byB2ZXJpZmllciIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsImlkIjoiYzA2NDU2M2EtOTc0Zi00ZjBmLWFiNGEtMDgwZTYzYzAxY2Y1IiwidG9vbF9jYWxsX2lkIjoidG9vbHVfMDFINmZWOEp2UkpOdlh5NDdlUm5kM2pIIiwic3RhdHVzIjoic3VjY2VzcyJ9LHsiY29udGVudCI6W3sic2lnbmF0dXJlIjoiQ0FRUzlnVUtFQWdTR0FJNEFVSUlkR2hwYm10cGJtY1NER0Z5SjFNWG9ZK0FpNk5rYnhvTVJkMlRyVWZVVTdXclBwQTJJakRneThkVXBsY1pLYzdEbkp3TkxUQmZINVVOUituR25ha1NQUWlhOFVWSXpyamtPbkoyZkxyV1VzQTJvRU84eVJzcWt3WGtrRENzeWhCOTdkd2o1N1JNdDkxWHpnV2hMOWx1TkVHTzkyeVcwck5GR29NU1VWT3hQSXJGYzVSanlyaEtFYzMrMVJucHhDa3lscStMTmxRNU5GeExuNnYyY0Y1RHpxcHZCSE5SN1hBaWcrdXJKSVUzZ1h5ZENrYUdaWkRldEp0UU9VT1hnWmk5ejkxMDBOcGRBRWtYRG8ySUNIQ0dUR3BWYzBKL3BiWHhXUkJ4YjZCMS9HbDBZam42OFY2SGxlc0g3SW9DN0dSLzNId1J1OU5TREJHNHR0ZlFqRVdjZytQZUFlYTRSbjdqengrVG05SDdMMWpvVEtWZ0RiUWc1aUtNTGZqdlBxbG5rc0NMdFhPdDhqU0lTNU9ZTE43YS83VFg2L3c1ZDM0dm53OHZQRG1VMVVZaTBRNzJxVVdoVWEydnc4T1hOcGNLTDdCcDBsTmZCUzhydkhZK3ZCWTgvaGFBb1RMM3d2TEZVMWZUKzR6L3ZBM0Jjc2lnWkRZbGZKZURZY3hYbVJ1cXh6eEVHcFhDUmU5czFIV3RJRCs0MXBIdTZscFpIa1hhZ2hwYzZUWkhMeE53Y1ZoVGI2Zk1RN1FvKzJMNlB2a2xoMW0zMnBlb2R4ZWcyWWlQaGRwbDN3L3hrT0V1NS9KRUY3WkJ4cGRVTCt1M3drTzVwVWxzcWZnWDd1aHp2N0hDMldwU1dWaW1yd2lRd1FPVXgwK0pQckx0dytsWWZxN0kzanJ1bzBHVzBndVd2bnVmN0pNM1lMajg0cStKZ0hvbW5TU1publptckwwVWFSdnJWeUxvODc3RjM5Y2NMUzdYb1pubnVpOEo0UDU4WlRtaUxrRUplR2N4MUl1YWcyZTMzMzM5TzdZZXcvNTl0TGJRdElHaDlWV013SEJuRngzOVM4LzRMRUZySzVIbGRQUWhPcmxveVR0a1JzNjZpUVJLdHhNS1MwVkk2UVRLWkRBYnM3YTdIMXlCdittSmk5WGc0dUtteVU2cW5uN3pFS2pqWTJiSWxDd2V6eDRMTEdQNUhEM1pyNmZac21kRzFVSHpEc0J2WUcyb0NJMG9xRUtNK3ZhbHNUNmhVOE1SamJRQkQvSko4SkpOL2lCR2xoQkFVSG8wVW1oaHRSMmV1VFZwWldsTW5obXBYQWRLRFhTZlBkYVBvaGdCIiwidGhpbmtpbmciOiIiLCJ0eXBlIjoidGhpbmtpbmcifSx7ImlkIjoidG9vbHVfMDFYRUF1aFFIQ0JkeUN3dHltWndGZkRpIiwiY2FsbGVyIjp7InR5cGUiOiJkaXJlY3QifSwiaW5wdXQiOnt9LCJuYW1lIjoidHJhbnNmZXJfdG9fcmVkX3RlYW0iLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QXNhYkRxdWVnSGk2WVVwQ24iLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzUwOSwib3V0cHV0X3Rva2VucyI6ODgsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjU1fSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCIsIm1vZGVsX3Byb3ZpZGVyIjoiYW50aHJvcGljIn0sInR5cGUiOiJhaSIsIm5hbWUiOiJ2ZXJpZmllciIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy02NjgyLTdkMjEtOGMxNS1mZmJjZTk0MjJjOWYtMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ0cmFuc2Zlcl90b19yZWRfdGVhbSIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxWEVBdWhRSENCZHlDd3R5bVp3RmZEaSIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzUwOSwib3V0cHV0X3Rva2VucyI6ODgsInRvdGFsX3Rva2VucyI6NzU5NywiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6NTV9fX0seyJjb250ZW50IjoiU3VjY2Vzc2Z1bGx5IHRyYW5zZmVycmVkIHRvIHJlZF90ZWFtIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJ0b29sIiwibmFtZSI6InRyYW5zZmVyX3RvX3JlZF90ZWFtIiwidG9vbF9jYWxsX2lkIjoidG9vbHVfMDFYRUF1aFFIQ0JkeUN3dHltWndGZkRpIiwic3RhdHVzIjoic3VjY2VzcyJ9XSwiYWN0aXZlX2FnZW50IjoicmVkX3RlYW0ifSwicmVzdW1lIjpudWxsLCJnb3RvIjoicmVkX3RlYW0ifX0=" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "38a909e757fa3574", + "parentSpanId": "4862a8dc9e239ae2", + "name": "model", + "kind": 1, + "startTimeUnixNano": "1790968030846537984", + "endTimeUnixNano": "1790968032470715136", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "1" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "verifier:544dea40-e16b-458c-1464-128e1b9b10a9|model:6b48cacc-ef10-0459-49d7-052bc435753c" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "verifier" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "verifier:544dea40-e16b-458c-1464-128e1b9b10a9" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "graph:step:1" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W3siY29udGVudCI6IkZvciBhIHN5bnRoZXRpYyB0cmFjaW5nIGZpeHR1cmUsIGV4cGxhaW4gUHl0aG9uIHR1cGxlIHZlcnN1cyBsaXN0IGltbXV0YWJpbGl0eSBpbiBhdCBtb3N0IDEyMCB3b3Jkcy4gVXNlIG9ubHkgb2ZmaWNpYWwgUHl0aG9uIGRvY3VtZW50YXRpb24gaWYgeW91IHNlYXJjaC4gSGF2ZSB0aGUgc2tlcHRpYyBhbmQgdmVyaWZpZXIgY2hlY2sgaXQsIHRoZW4gaGF2ZSB0aGUgZWRpdG9yIHdyaXRlIC9hbnN3ZXIubWQuIEtlZXAgdGhlIHJ1biBzaG9ydCBhbmQgZG8gbm90IGFzayBmb2xsb3ctdXAgcXVlc3Rpb25zLiIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoiaHVtYW4iLCJpZCI6IjQ2MjA0MzA1LWY4ZWUtNDM4NC05Y2U4LWZjZDgwYmZjZGY3NSJ9LHsiY29udGVudCI6W3siaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9LCJuYW1lIjoid2ViX3NlYXJjaCIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHc5YVc5YnJlblBxZ3liZk53UCIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjoyODQ1LCJvdXRwdXRfdG9rZW5zIjo3Mywib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6MH0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoicmVzZWFyY2hlciIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy0yMWEzLTcwMDAtYTM4MS0yODRhNmEwYzAzZjctMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ3ZWJfc2VhcmNoIiwiYXJncyI6eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9LCJpZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6Mjg0NSwib3V0cHV0X3Rva2VucyI6NzMsInRvdGFsX3Rva2VucyI6MjkxOCwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fSx7ImNvbnRlbnQiOiJbe1widGl0bGVcIjogXCI1LiBEYXRhIFN0cnVjdHVyZXMgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3R1dG9yaWFsL2RhdGFzdHJ1Y3R1cmVzLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcIllvdSBtaWdodCBoYXZlIG5vdGljZWQgdGhhdCBtZXRob2RzIGxpa2VgaW5zZXJ0YCxgcmVtb3ZlYCBvcmBzb3J0YCB0aGF0IG9ubHkgbW9kaWZ5IHRoZSBsaXN0IGhhdmUgbm8gcmV0dXJuIHZhbHVlIHByaW50ZWQgXFx1MjAxMyB0aGV5IHJldHVybiB0aGUgZGVmYXVsdGBOb25lYC4gWzFdIFRoaXMgaXMgYSBkZXNpZ24gcHJpbmNpcGxlIGZvciBhbGwgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgaW4gUHl0aG9uLlxcbi4uLlxcbiMjIDUuMy4gVHVwbGVzIGFuZCBTZXF1ZW5jZXNcXHUwMGI2XFxuXFxuV2Ugc2F3IHRoYXQgbGlzdHMgYW5kIHN0cmluZ3MgaGF2ZSBtYW55IGNvbW1vbiBwcm9wZXJ0aWVzLCBzdWNoIGFzIGluZGV4aW5nIGFuZCBzbGljaW5nIG9wZXJhdGlvbnMuIFRoZXkgYXJlIHR3byBleGFtcGxlcyBvZiBzZXF1ZW5jZSBkYXRhIHR5cGVzIChzZWUgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBsaXN0LCB0dXBsZSwgcmFuZ2UpLiBTaW5jZSBQeXRob24gaXMgYW4gZXZvbHZpbmcgbGFuZ3VhZ2UsIG90aGVyIHNlcXVlbmNlIGRhdGEgdHlwZXMgbWF5IGJlIGFkZGVkLiBUaGVyZSBpcyBhbHNvIGFub3RoZXIgc3RhbmRhcmQgc2VxdWVuY2UgZGF0YSB0eXBlOiB0aGUgdHVwbGUuXFxuXFxuQSB0dXBsZSBjb25zaXN0cyBvZiBhIG51bWJlciBvZiB2YWx1ZXMgc2VwYXJhdGVkIGJ5IGNvbW1hcywgZm9yIGluc3RhbmNlOlxcbi4uLlxcbj4+PiAjIFR1cGxlcyBhcmUgaW1tdXRhYmxlOlxcbj4+PiB0WzBdID0gODg4ODhcXG5UcmFjZWJhY2sgKG1vc3QgcmVjZW50IGNhbGwgbGFzdCk6XFxuICBGaWxlIFxcXCI8c3RkaW4+XFxcIiwgbGluZSAxLCBpbiA8bW9kdWxlPlxcblR5cGVFcnJvcjogJ3R1cGxlJyBvYmplY3QgZG9lcyBub3Qgc3VwcG9ydCBpdGVtIGFzc2lnbm1lbnRcXG4+Pj4gIyBidXQgdGhleSBjYW4gY29udGFpbiBtdXRhYmxlIG9iamVjdHM6XFxuPj4+IHYgPSAoWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuPj4+IHZcXG4oWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuLi4uXFxuQXMgeW91IHNlZSwgb24gb3V0cHV0IHR1cGxlcyBhcmUgYWx3YXlzIGVuY2xvc2VkIGluIHBhcmVudGhlc2VzLCBzbyB0aGF0IG5lc3RlZCB0dXBsZXMgYXJlIGludGVycHJldGVkIGNvcnJlY3RseTsgdGhleSBtYXkgYmUgaW5wdXQgd2l0aCBvciB3aXRob3V0IHN1cnJvdW5kaW5nIHBhcmVudGhlc2VzLCBhbHRob3VnaCBvZnRlbiBwYXJlbnRoZXNlcyBhcmUgbmVjZXNzYXJ5IGFueXdheSAoaWYgdGhlIHR1cGxlIGlzIHBhcnQgb2YgYSBsYXJnZXIgZXhwcmVzc2lvbikuIEl0IGlzIG5vdCBwb3NzaWJsZSB0byBhc3NpZ24gdG8gdGhlIGluZGl2aWR1YWwgaXRlbXMgb2YgYSB0dXBsZSwgaG93ZXZlciBpdCBpcyBwb3NzaWJsZSB0byBjcmVhdGUgdHVwbGVzIHdoaWNoIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzLCBzdWNoIGFzIGxpc3RzLlxcblxcblRob3VnaCB0dXBsZXMgbWF5IHNlZW0gc2ltaWxhciB0byBsaXN0cywgdGhleSBhcmUgb2Z0ZW4gdXNlZCBpbiBkaWZmZXJlbnQgc2l0dWF0aW9ucyBhbmQgZm9yIGRpZmZlcmVudCBwdXJwb3Nlcy4gVHVwbGVzIGFyZSBpbW11dGFibGUsIGFuZCB1c3VhbGx5IGNvbnRhaW4gYSBoZXRlcm9nZW5lb3VzIHNlcXVlbmNlIG9mIGVsZW1lbnRzIHRoYXQgYXJlIGFjY2Vzc2VkIHZpYSB1bnBhY2tpbmcgKHNlZSBsYXRlciBpbiB0aGlzIHNlY3Rpb24pIG9yIGluZGV4aW5nIChvciBldmVuIGJ5IGF0dHJpYnV0ZSBpbiB0aGUgY2FzZSBvZiBuYW1lZHR1cGxlcykuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG5Bbm90aGVyIHVzZWZ1bCBkYXRhIHR5cGUgYnVpbHQgaW50byBQeXRob24gaXMgdGhlIGRpY3Rpb25hcnkgKHNlZSBNYXBwaW5nIFR5cGVzIFxcdTIwMTQgZGljdCkuIERpY3Rpb25hcmllcyBhcmUgc29tZXRpbWVzIGZvdW5kIGluIG90aGVyIGxhbmd1YWdlcyBhcyBcXHUyMDFjYXNzb2NpYXRpdmUgbWVtb3JpZXNcXHUyMDFkIG9yIFxcdTIwMWNhc3NvY2lhdGl2ZSBhcnJheXNcXHUyMDFkLiBVbmxpa2Ugc2VxdWVuY2VzLCB3aGljaCBhcmUgaW5kZXhlZCBieSBhIHJhbmdlIG9mIG51bWJlcnMsIGRpY3Rpb25hcmllcyBhcmUgaW5kZXhlZCBieSBrZXlzLCB3aGljaCBjYW4gYmUgYW55IGltbXV0YWJsZSB0eXBlOyBzdHJpbmdzIGFuZCBudW1iZXJzIGNhbiBhbHdheXMgYmUga2V5cy4gVHVwbGVzIGNhbiBiZSB1c2VkIGFzIGtleXMgaWYgdGhleSBjb250YWluIG9ubHkgc3RyaW5ncywgbnVtYmVycywgb3IgdHVwbGVzOyBpZiBhIHR1cGxlIGNvbnRhaW5zIGFueSBtdXRhYmxlIG9iamVjdCBlaXRoZXIgZGlyZWN0bHkgb3IgaW5kaXJlY3RseSwgaXQgY2Fubm90IGJlIHVzZWQgYXMgYSBrZXkuIFlvdSBjYW5cXHUyMDE5dCB1c2UgbGlzdHMgYXMga2V5cywgc2luY2UgbGlzdHMgY2FuIGJlIG1vZGlmaWVkIGluIHBsYWNlIHVzaW5nIGluZGV4IGFzc2lnbm1lbnRzLCBzbGljZSBhc3NpZ25tZW50cywgb3IgbWV0aG9kcyBsaWtlIGFwcGVuZCgpIGFuZCBleHRlbmQoKS5cIl19LCB7XCJ0aXRsZVwiOiBcIjMuIERhdGEgbW9kZWwgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3JlZmVyZW5jZS9kYXRhbW9kZWwuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiVGhlIHZhbHVlIG9mIHNvbWUgb2JqZWN0cyBjYW4gY2hhbmdlLiBPYmplY3RzIHdob3NlIHZhbHVlIGNhbiBjaGFuZ2UgYXJlIHNhaWQgdG8gYmUgbXV0YWJsZTsgb2JqZWN0cyB3aG9zZSB2YWx1ZSBpcyB1bmNoYW5nZWFibGUgb25jZSB0aGV5IGFyZSBjcmVhdGVkIGFyZSBjYWxsZWQgaW1tdXRhYmxlLiAoVGhlIHZhbHVlIG9mIGFuIGltbXV0YWJsZSBjb250YWluZXIgb2JqZWN0IHRoYXQgY29udGFpbnMgYSByZWZlcmVuY2UgdG8gYSBtdXRhYmxlIG9iamVjdCBjYW4gY2hhbmdlIHdoZW4gdGhlIGxhdHRlclxcdTIwMTlzIHZhbHVlIGlzIGNoYW5nZWQ7IGhvd2V2ZXIgdGhlIGNvbnRhaW5lciBpcyBzdGlsbCBjb25zaWRlcmVkIGltbXV0YWJsZSwgYmVjYXVzZSB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGl0IGNvbnRhaW5zIGNhbm5vdCBiZSBjaGFuZ2VkLiBTbywgaW1tdXRhYmlsaXR5IGlzIG5vdCBzdHJpY3RseSB0aGUgc2FtZSBhcyBoYXZpbmcgYW4gdW5jaGFuZ2VhYmxlIHZhbHVlLCBpdCBpcyBtb3JlIHN1YnRsZS4pIEFuIG9iamVjdFxcdTIwMTlzIG11dGFiaWxpdHkgaXMgZGV0ZXJtaW5lZCBieSBpdHMgdHlwZTsgZm9yIGluc3RhbmNlLCBudW1iZXJzLCBzdHJpbmdzIGFuZCB0dXBsZXMgYXJlIGltbXV0YWJsZSwgd2hpbGUgZGljdGlvbmFyaWVzIGFuZCBsaXN0cyBhcmUgbXV0YWJsZS5cXG4uLi5cXG5Tb21lIG9iamVjdHMgY29udGFpbiByZWZlcmVuY2VzIHRvIG90aGVyIG9iamVjdHM7IHRoZXNlIGFyZSBjYWxsZWQgY29udGFpbmVycy4gRXhhbXBsZXMgb2YgY29udGFpbmVycyBhcmUgdHVwbGVzLCBsaXN0cyBhbmQgZGljdGlvbmFyaWVzLiBUaGUgcmVmZXJlbmNlcyBhcmUgcGFydCBvZiBhIGNvbnRhaW5lclxcdTIwMTlzIHZhbHVlLiBJbiBtb3N0IGNhc2VzLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIHZhbHVlIG9mIGEgY29udGFpbmVyLCB3ZSBpbXBseSB0aGUgdmFsdWVzLCBub3QgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGNvbnRhaW5lZCBvYmplY3RzOyBob3dldmVyLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIG11dGFiaWxpdHkgb2YgYSBjb250YWluZXIsIG9ubHkgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGltbWVkaWF0ZWx5IGNvbnRhaW5lZCBvYmplY3RzIGFyZSBpbXBsaWVkLiBTbywgaWYgYW4gaW1tdXRhYmxlIGNvbnRhaW5lciAobGlrZSBhIHR1cGxlKSBjb250YWlucyBhIHJlZmVyZW5jZSB0byBhIG11dGFibGUgb2JqZWN0LCBpdHMgdmFsdWUgY2hhbmdlcyBpZiB0aGF0IG11dGFibGUgb2JqZWN0IGlzIGNoYW5nZWQuXFxuLi4uXFxuIyMjIDMuMi41LiBTZXF1ZW5jZXNcXHUwMGI2XFxuLi4uXFxuIyMjIyAzLjIuNS4xLiBJbW11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbkFuIG9iamVjdCBvZiBhbiBpbW11dGFibGUgc2VxdWVuY2UgdHlwZSBjYW5ub3QgY2hhbmdlIG9uY2UgaXQgaXMgY3JlYXRlZC4gKElmIHRoZSBvYmplY3QgY29udGFpbnMgcmVmZXJlbmNlcyB0byBvdGhlciBvYmplY3RzLCB0aGVzZSBvdGhlciBvYmplY3RzIG1heSBiZSBtdXRhYmxlIGFuZCBtYXkgYmUgY2hhbmdlZDsgaG93ZXZlciwgdGhlIGNvbGxlY3Rpb24gb2Ygb2JqZWN0cyBkaXJlY3RseSByZWZlcmVuY2VkIGJ5IGFuIGltbXV0YWJsZSBvYmplY3QgY2Fubm90IGNoYW5nZS4pXFxuXFxuVGhlIGZvbGxvd2luZyB0eXBlcyBhcmUgaW1tdXRhYmxlIHNlcXVlbmNlczpcXG4uLi5cXG5UdXBsZXNcXG46IFRoZSBpdGVtcyBvZiBhIGB0dXBsZWAgYXJlIGFyYml0cmFyeSBQeXRob24gb2JqZWN0cy4gVHVwbGVzIG9mIHR3byBvciBtb3JlIGl0ZW1zIGFyZSBmb3JtZWQgYnkgY29tbWEtc2VwYXJhdGVkIGxpc3RzIG9mIGV4cHJlc3Npb25zLiBBIHR1cGxlIG9mIG9uZSBpdGVtIChhIFxcdTIwMThzaW5nbGV0b25cXHUyMDE5KSBjYW4gYmUgZm9ybWVkIGJ5IGFmZml4aW5nIGEgY29tbWEgdG8gYW4gZXhwcmVzc2lvbiAoYW4gZXhwcmVzc2lvbiBieSBpdHNlbGYgZG9lcyBub3QgY3JlYXRlIGEgdHVwbGUsIHNpbmNlIHBhcmVudGhlc2VzIG11c3QgYmUgdXNhYmxlIGZvciBncm91cGluZyBvZiBleHByZXNzaW9ucykuIEFuIGVtcHR5IHR1cGxlIGNhbiBiZSBmb3JtZWQgYnkgYW4gZW1wdHkgcGFpciBvZiBwYXJlbnRoZXNlcy5cXG4uLi5cXG4jIyMjIDMuMi41LjIuIE11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbk11dGFibGUgc2VxdWVuY2VzIGNhbiBiZSBjaGFuZ2VkIGFmdGVyIHRoZXkgYXJlIGNyZWF0ZWQuIFRoZSBzdWJzY3JpcHRpb24gYW5kIHNsaWNpbmcgbm90YXRpb25zIGNhbiBiZSB1c2VkIGFzIHRoZSB0YXJnZXQgb2YgYXNzaWdubWVudCBhbmQgYGRlbGAgKGRlbGV0ZSkgc3RhdGVtZW50cy5cXG4uLi5cXG5UaGVyZSBhcmUgY3VycmVudGx5IHR3byBpbnRyaW5zaWMgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlczpcXG5cXG5MaXN0c1xcbjogVGhlIGl0ZW1zIG9mIGEgbGlzdCBhcmUgYXJiaXRyYXJ5IFB5dGhvbiBvYmplY3RzLiBMaXN0cyBhcmUgZm9ybWVkIGJ5IHBsYWNpbmcgYSBjb21tYS1zZXBhcmF0ZWQgbGlzdCBvZiBleHByZXNzaW9ucyBpbiBzcXVhcmUgYnJhY2tldHMuIChOb3RlIHRoYXQgdGhlcmUgYXJlIG5vIHNwZWNpYWwgY2FzZXMgbmVlZGVkIHRvIGZvcm0gbGlzdHMgb2YgbGVuZ3RoIDAgb3IgMS4pXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjcgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvYnVpbHRpbnMvc3RkdHlwZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiY29sbGVjdGlvbiBjbGFzc2VzIGFyZSBtdXRhYmxlLiBUaGUgbWV0aG9kcyB0aGF0IGFkZCwgc3VidHJhY3QsIG9yIHJlYXJyYW5nZSB0aGVpciAuLi4gaW4gcGxhY2UsIGFuZCBkb25cXHUyMDE5dCByZXR1cm4gYSAuLi4gLCBuZXZlciByZXR1cm4gdGhlIGNvbGxlY3Rpb24gaW5zdGFuY2UgaXRzZWxmIGJ1dCBgTm9uZWAuXFxuLi4uXFxuIyMgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBgbGlzdGAsIGB0dXBsZWAsIGByYW5nZWBcXHUwMGI2XFxuXFxuVGhlcmUgYXJlIHRocmVlIGJhc2ljIHNlcXVlbmNlIHR5cGVzOiBsaXN0cywgdHVwbGVzLCBhbmQgcmFuZ2Ugb2JqZWN0cy4gQWRkaXRpb25hbCBzZXF1ZW5jZSB0eXBlcyB0YWlsb3JlZCBmb3IgcHJvY2Vzc2luZyBvZiBiaW5hcnkgZGF0YSBhbmQgdGV4dCBzdHJpbmdzIGFyZSBkZXNjcmliZWQgaW4gZGVkaWNhdGVkIHNlY3Rpb25zLlxcbi4uLlxcblRoZSBvcGVyYXRpb25zIGluIHRoZSBmb2xsb3dpbmcgdGFibGUgLi4uIG11dGFibGUgYW5kIGltbXV0YWJsZS4gVGhlIGBjb2xsZWN0aW9ucy4gLi4uIGlzIHByb3ZpZGVkIHRvIG1ha2UgLi4uIGVhc2llciB0byBjb3JyZWN0bHkgaW1wbGVtZW50IHRoZXNlIG9wZXJhdGlvbnMgb24gY3VzdG9tIHNlcXVlbmNlIHR5cGVzXFxuLi4uXFxuIyMjIEltbXV0YWJsZSBTZXF1ZW5jZSBUeXBlc1xcdTAwYjZcXG4uLi5cXG5tdXRhYmxlIHNlcXVlbmNlIHR5cGVzIGlzXFxuLi4uXFxuc3VwcG9ydCBhbGxvd3MgaW1tdXRhYmxlIHNlcXVlbmNlcywgLi4uICwgdG8gYmUgdXNlZCBhcyBgZGljdGAga2V5cyBhbmQgc3RvcmVkIGluIC4uLiBlbnNldGAgaW5zdGFuY2VzXFxuLi4uXFxuIyMjIE11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxuIyMjIExpc3RzXFx1MDBiNlxcblxcbkxpc3RzIGFyZSBtdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaG9tb2dlbmVvdXMgaXRlbXMgKHdoZXJlIHRoZSBwcmVjaXNlIGRlZ3JlZSBvZiBzaW1pbGFyaXR5IHdpbGwgdmFyeSBieSBhcHBsaWNhdGlvbikuXFxuLi4uXFxuIyMjIFR1cGxlc1xcdTAwYjZcXG5cXG5UdXBsZXMgYXJlIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhldGVyb2dlbmVvdXMgZGF0YSAoc3VjaCBhcyB0aGUgMi10dXBsZXMgcHJvZHVjZWQgYnkgdGhlIGBlbnVtZXJhdGUoKWAgYnVpbHQtaW4pLiBUdXBsZXMgYXJlIGFsc28gdXNlZCBmb3IgY2FzZXMgd2hlcmUgYW4gaW1tdXRhYmxlIHNlcXVlbmNlIG9mIGhvbW9nZW5lb3VzIGRhdGEgaXMgbmVlZGVkIChzdWNoIGFzIGFsbG93aW5nIHN0b3JhZ2UgaW4gYSBgc2V0YCBvciBgZGljdGAgaW5zdGFuY2UpLlxcbi4uLlxcbnR1cGxlKGl0ZXJhYmxlPVxcbi4uLlxcblRoZSBjb25zdHJ1Y3RvciBidWlsZHMgYSB0dXBsZSB3aG9zZSBpdGVtcyBhcmUgdGhlIHNhbWUgYW5kIGluIHRoZSBzYW1lIG9yZGVyIGFzIGl0ZXJhYmxlXFx1MjAxOXMgaXRlbXMuIGl0ZXJhYmxlIG1heSBiZSBlaXRoZXIgYSBzZXF1ZW5jZSwgYSBjb250YWluZXIgdGhhdCBzdXBwb3J0cyBpdGVyYXRpb24sIG9yIGFuIGl0ZXJhdG9yIG9iamVjdC4gSWYgaXRlcmFibGUgaXMgYWxyZWFkeSBhIHR1cGxlLCBpdCBpcyByZXR1cm5lZCB1bmNoYW5nZWQuIEZvciBleGFtcGxlLCBgdHVwbGUoJ2FiYycpYCByZXR1cm5zIGAoJ2EnLCAnYicsICdjJylgIGFuZCBgdHVwbGUoIFsxLCAyLCAzXSApYCByZXR1cm5zIGAoMSwgMiwgMylgLiBJZiBubyBhcmd1bWVudCBpcyBnaXZlbiwgdGhlIGNvbnN0cnVjdG9yIGNyZWF0ZXMgYSBuZXcgZW1wdHkgdHVwbGUsIGAoKWAuXFxuLi4uXFxuVHVwbGVzIGltcGxlbWVudCBhbGwgb2YgdGhlIGNvbW1vbiBzZXF1ZW5jZSBvcGVyYXRpb25zLlxcbi4uLlxcbkZvciBoZXRlcm9nZW5lb3VzIGNvbGxlY3Rpb25zIG9mIGRhdGEgd2hlcmUgYWNjZXNzIGJ5IG5hbWUgaXMgY2xlYXJlciB0aGFuIGFjY2VzcyBieSBpbmRleCwgYGNvbGxlY3Rpb25zLm5hbWVkdHVwbGUoKWAgbWF5IGJlIGEgbW9yZSBhcHByb3ByaWF0ZSBjaG9pY2UgdGhhbiBhIHNpbXBsZSB0dXBsZSBvYmplY3QuXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvbGlicmFyeS9zdGR0eXBlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJTb21lIGNvbGxlY3Rpb24gY2xhc3NlcyBhcmUgbXV0YWJsZS4gVGhlIG1ldGhvZHMgdGhhdCBhZGQsIHN1YnRyYWN0LCBvciByZWFycmFuZ2UgdGhlaXIgLi4uIGluIHBsYWNlLCBhbmQgZG9uXFx1MjAxOXQgcmV0dXJuIGEgLi4uICwgbmV2ZXIgcmV0dXJuIHRoZSBjb2xsZWN0aW9uIGluc3RhbmNlIGl0c2VsZiBidXQgYE5vbmVgLlxcbi4uLlxcbiMjIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgYGxpc3RgLCBgdHVwbGVgLCBgcmFuZ2VgXFx1MDBiNlxcblxcblRoZXJlIGFyZSB0aHJlZSBiYXNpYyBzZXF1ZW5jZSB0eXBlczogbGlzdHMsIHR1cGxlcywgYW5kIC4uLiBvYmplY3RzLiBBZGRpdGlvbmFsIHNlcXVlbmNlIHR5cGVzIHRhaWxvcmVkIGZvciBwcm9jZXNzaW5nIG9mIGJpbmFyeSBkYXRhIGFuZCB0ZXh0IHN0cmluZ3MgYXJlIGRlc2NyaWJlZCBpbiBkZWRpY2F0ZWQgc2VjdGlvbnMuXFxuLi4uXFxuVGhlIG9wZXJhdGlvbnMgaW4gdGhlIGZvbGxvd2luZyB0YWJsZSAuLi4gaW1tdXRhYmxlLiBUaGUgYCAuLi4gaXMgcHJvdmlkZWQgdG8gbWFrZSAuLi4gZWFzaWVyIHRvIGNvcnJlY3RseSBpbXBsZW1lbnQgdGhlc2Ugb3BlcmF0aW9ucyBvbiBjdXN0b20gc2VxdWVuY2UgdHlwZXNcXG4uLi5cXG4jIyMgSW1tdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbm9wZXJhdGlvbiB0aGF0IGltbXV0YWJsZSBzZXF1ZW5jZSAuLi4gYnkgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlcyBpcyAuLi4gYGhhc2goKWBcXG4uLi5cXG5UaGlzIHN1cHBvcnQgYWxsb3dzIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHN1Y2ggYXMgYHR1cGxlYCBpbnN0YW5jZXMsIHRvIGJlIHVzZWQgYXMgYGRpY3RgIGtleXMgYW5kIHN0b3JlZCBpbiBgc2V0YCBhbmQgYGZyb3plbnNldGAgaW5zdGFuY2VzLlxcbi4uLlxcbiMjIyBNdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbiMjIyBMaXN0c1xcdTAwYjZcXG5cXG5MaXN0cyBhcmUgbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhvbW9nZW5lb3VzIGl0ZW1zICh3aGVyZSB0aGUgcHJlY2lzZSBkZWdyZWUgb2Ygc2ltaWxhcml0eSB3aWxsIHZhcnkgYnkgYXBwbGljYXRpb24pLlxcbi4uLlxcbiMjIyBUdXBsZXNcXHUwMGI2XFxuXFxuVHVwbGVzIGFyZSBpbW11dGFibGUgc2VxdWVuY2VzLCB0eXBpY2FsbHkgdXNlZCB0byBzdG9yZSBjb2xsZWN0aW9ucyBvZiBoZXRlcm9nZW5lb3VzIGRhdGEgKHN1Y2ggYXMgdGhlIDItdHVwbGVzIHByb2R1Y2VkIGJ5IHRoZSBgZW51bWVyYXRlKClgIGJ1aWx0LWluKS4gVHVwbGVzIGFyZSBhbHNvIHVzZWQgZm9yIGNhc2VzIHdoZXJlIGFuIGltbXV0YWJsZSBzZXF1ZW5jZSBvZiBob21vZ2VuZW91cyBkYXRhIGlzIG5lZWRlZCAoc3VjaCBhcyBhbGxvd2luZyBzdG9yYWdlIGluIGEgYHNldGAgb3IgYGRpY3RgIGluc3RhbmNlKS5cXG4uLi5cXG5UaGUgY29uc3RydWN0b3IgYnVpbGRzIGEgdHVwbGUgd2hvc2UgaXRlbXMgYXJlIHRoZSBzYW1lIGFuZCBpbiB0aGUgc2FtZSBvcmRlciBhcyBpdGVyYWJsZVxcdTIwMTlzIGl0ZW1zLiBpdGVyYWJsZSBtYXkgYmUgZWl0aGVyIGEgc2VxdWVuY2UsIGEgY29udGFpbmVyIHRoYXQgc3VwcG9ydHMgaXRlcmF0aW9uLCBvciBhbiBpdGVyYXRvciBvYmplY3QuIElmIGl0ZXJhYmxlIGlzIGFscmVhZHkgYSB0dXBsZSwgaXQgaXMgcmV0dXJuZWQgdW5jaGFuZ2VkLiBGb3IgZXhhbXBsZSwgYHR1cGxlKCdhYmMnKWAgcmV0dXJucyBgKCdhJywgJ2InLCAnYycpYCBhbmQgYHR1cGxlKCBbMSwgMiwgM10gKWAgcmV0dXJucyBgKDEsIDIsIDMpYC4gSWYgbm8gYXJndW1lbnQgaXMgZ2l2ZW4sIHRoZSBjb25zdHJ1Y3RvciBjcmVhdGVzIGEgbmV3IGVtcHR5IHR1cGxlLCBgKClgLlxcbi4uLlxcblR1cGxlcyBpbXBsZW1lbnQgYWxsIG9mIHRoZSBjb21tb24gc2VxdWVuY2Ugb3BlcmF0aW9ucy5cXG4uLi5cXG5Gb3IgaGV0ZXJvZ2VuZW91cyBjb2xsZWN0aW9ucyBvZiBkYXRhIHdoZXJlIGFjY2VzcyBieSBuYW1lIGlzIGNsZWFyZXIgdGhhbiBhY2Nlc3MgYnkgaW5kZXgsIGBjb2xsZWN0aW9ucy5uYW1lZHR1cGxlKClgIG1heSBiZSBhIG1vcmUgYXBwcm9wcmlhdGUgY2hvaWNlIHRoYW4gYSBzaW1wbGUgdHVwbGUgb2JqZWN0LlwiXX0sIHtcInRpdGxlXCI6IFwiNS4gRGF0YSBTdHJ1Y3R1cmVzIFxcdTIwMTQgUHl0aG9uIDMuMTAuMjAgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMuMTAvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiWW91IG1pZ2h0IGhhdmUgbm90aWNlZCB0aGF0IG1ldGhvZHMgbGlrZSBgaW5zZXJ0YCwgYHJlbW92ZWAgb3IgYHNvcnRgIHRoYXQgb25seSBtb2RpZnkgdGhlIGxpc3QgaGF2ZSBubyByZXR1cm4gdmFsdWUgcHJpbnRlZCBcXHUyMDEzIHRoZXkgcmV0dXJuIHRoZSBkZWZhdWx0IGBOb25lYC4gMSBUaGlzIGlzIGEgZGVzaWduIHByaW5jaXBsZSBmb3IgYWxsIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIGluIFB5dGhvbi5cXG4uLi5cXG4jIyA1LjMuIFR1cGxlcyBhbmQgU2VxdWVuY2VzXFx1MDBiNlxcblxcbldlIHNhdyB0aGF0IGxpc3RzIGFuZCBzdHJpbmdzIGhhdmUgbWFueSBjb21tb24gcHJvcGVydGllcywgc3VjaCBhcyBpbmRleGluZyBhbmQgc2xpY2luZyBvcGVyYXRpb25zLiBUaGV5IGFyZSB0d28gZXhhbXBsZXMgb2Ygc2VxdWVuY2UgZGF0YSB0eXBlcyAoc2VlIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgbGlzdCwgdHVwbGUsIHJhbmdlKS4gU2luY2UgUHl0aG9uIGlzIGFuIGV2b2x2aW5nIGxhbmd1YWdlLCBvdGhlciBzZXF1ZW5jZSBkYXRhIHR5cGVzIG1heSBiZSBhZGRlZC4gVGhlcmUgaXMgYWxzbyBhbm90aGVyIHN0YW5kYXJkIHNlcXVlbmNlIGRhdGEgdHlwZTogdGhlIHR1cGxlLlxcblxcbkEgdHVwbGUgY29uc2lzdHMgb2YgYSBudW1iZXIgb2YgdmFsdWVzIHNlcGFyYXRlZCBieSBjb21tYXMsIGZvciBpbnN0YW5jZTpcXG4uLi5cXG4+Pj4gIyBUdXBsZXMgYXJlIGltbXV0YWJsZTpcXG4uLi4gdFswXSA9IDg4ODg4XFxuVHJhY2ViYWNrIChtb3N0IHJlY2VudCBjYWxsIGxhc3QpOlxcbiAgRmlsZSBcXFwiPHN0ZGluPlxcXCIsIGxpbmUgMSwgaW4gPG1vZHVsZT5cXG5UeXBlRXJyb3I6ICd0dXBsZScgb2JqZWN0IGRvZXMgbm90IHN1cHBvcnQgaXRlbSBhc3NpZ25tZW50XFxuPj4+ICMgYnV0IHRoZXkgY2FuIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzOlxcbi4uLiB2ID0gKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbj4+PiB2XFxuKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbi4uLlxcbkFzIHlvdSBzZWUsIG9uIG91dHB1dCB0dXBsZXMgYXJlIGFsd2F5cyBlbmNsb3NlZCBpbiBwYXJlbnRoZXNlcywgc28gdGhhdCBuZXN0ZWQgdHVwbGVzIGFyZSBpbnRlcnByZXRlZCBjb3JyZWN0bHk7IHRoZXkgbWF5IGJlIGlucHV0IHdpdGggb3Igd2l0aG91dCBzdXJyb3VuZGluZyBwYXJlbnRoZXNlcywgYWx0aG91Z2ggb2Z0ZW4gcGFyZW50aGVzZXMgYXJlIG5lY2Vzc2FyeSBhbnl3YXkgKGlmIHRoZSB0dXBsZSBpcyBwYXJ0IG9mIGEgbGFyZ2VyIGV4cHJlc3Npb24pLiBJdCBpcyBub3QgcG9zc2libGUgdG8gYXNzaWduIHRvIHRoZSBpbmRpdmlkdWFsIGl0ZW1zIG9mIGEgdHVwbGUsIGhvd2V2ZXIgaXQgaXMgcG9zc2libGUgdG8gY3JlYXRlIHR1cGxlcyB3aGljaCBjb250YWluIG11dGFibGUgb2JqZWN0cywgc3VjaCBhcyBsaXN0cy5cXG5cXG5UaG91Z2ggdHVwbGVzIG1heSBzZWVtIHNpbWlsYXIgdG8gbGlzdHMsIHRoZXkgYXJlIG9mdGVuIHVzZWQgaW4gZGlmZmVyZW50IHNpdHVhdGlvbnMgYW5kIGZvciBkaWZmZXJlbnQgcHVycG9zZXMuIFR1cGxlcyBhcmUgaW1tdXRhYmxlLCBhbmQgdXN1YWxseSBjb250YWluIGEgaGV0ZXJvZ2VuZW91cyBzZXF1ZW5jZSBvZiBlbGVtZW50cyB0aGF0IGFyZSBhY2Nlc3NlZCB2aWEgdW5wYWNraW5nIChzZWUgbGF0ZXIgaW4gdGhpcyBzZWN0aW9uKSBvciBpbmRleGluZyAob3IgZXZlbiBieSBhdHRyaWJ1dGUgaW4gdGhlIGNhc2Ugb2YgYG5hbWVkdHVwbGVzYCkuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG4jIyA1LjQuXFxuLi4uXFxuLiBTZXQgb2JqZWN0c1xcbi4uLlxcbiMjIDUuNS4gRGljdGlvbmFyaWVzXFx1MDBiNlxcblxcbkFub3RoZXIgdXNlZnVsIGRhdGEgdHlwZSBidWlsdCBpbnRvIFB5dGhvbiBpcyB0aGUgZGljdGlvbmFyeSAoc2VlIE1hcHBpbmcgVHlwZXMgXFx1MjAxNCBkaWN0KS4gRGljdGlvbmFyaWVzIGFyZSBzb21ldGltZXMgZm91bmQgaW4gb3RoZXIgbGFuZ3VhZ2VzIGFzIFxcdTIwMWNhc3NvY2lhdGl2ZSBtZW1vcmllc1xcdTIwMWQgb3IgXFx1MjAxY2Fzc29jaWF0aXZlIGFycmF5c1xcdTIwMWQuIFVubGlrZSBzZXF1ZW5jZXMsIHdoaWNoIGFyZSBpbmRleGVkIGJ5IGEgcmFuZ2Ugb2YgbnVtYmVycywgZGljdGlvbmFyaWVzIGFyZSBpbmRleGVkIGJ5IGtleXMsIHdoaWNoIGNhbiBiZSBhbnkgaW1tdXRhYmxlIHR5cGU7IHN0cmluZ3MgYW5kIG51bWJlcnMgY2FuIGFsd2F5cyBiZSBrZXlzLiBUdXBsZXMgY2FuIGJlIHVzZWQgYXMga2V5cyBpZiB0aGV5IGNvbnRhaW4gb25seSBzdHJpbmdzLCBudW1iZXJzLCBvciB0dXBsZXM7IGlmIGEgdHVwbGUgY29udGFpbnMgYW55IG11dGFibGUgb2JqZWN0IGVpdGhlciBkaXJlY3RseSBvciBpbmRpcmVjdGx5LCBpdCBjYW5ub3QgYmUgdXNlZCBhcyBhIGtleS4gWW91IGNhblxcdTIwMTl0IHVzZSBsaXN0cyBhcyBrZXlzLCBzaW5jZSBsaXN0cyBjYW4gYmUgbW9kaWZpZWQgaW4gcGxhY2UgdXNpbmcgaW5kZXggYXNzaWdubWVudHMsIHNsaWNlIGFzc2lnbm1lbnRzLCBvciBtZXRob2RzIGxpa2UgYGFwcGVuZCgpYCBhbmQgYGV4dGVuZCgpYC5cIl19XSIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ3ZWJfc2VhcmNoIiwiaWQiOiI2ZTUxMjVhYy0zMTY0LTRhNzYtOWM1Mi1hYzM0NjBjZTM2ZTQiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJzdGF0dXMiOiJzdWNjZXNzIn0seyJjb250ZW50IjpbeyJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3NrZXB0aWMiLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QTZGNGNDTjV5aEQ0Q3JzOU0iLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjB9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InJlc2VhcmNoZXIiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtM2NkOC03OWYxLTg4YTEtY2U2Y2VlMjk0ZTc5LTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoidHJhbnNmZXJfdG9fc2tlcHRpYyIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsInRvdGFsX3Rva2VucyI6NzQxMSwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fSx7ImNvbnRlbnQiOiJTdWNjZXNzZnVsbHkgdHJhbnNmZXJyZWQgdG8gc2tlcHRpYyIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ0cmFuc2Zlcl90b19za2VwdGljIiwiaWQiOiIzODkwNThkMS0wODNmLTQ2MzItYmJjNS0wOWJmZDNhMGZmZDIiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMVhqajNhUmRXeWhCSmZQbVRKQnhRYTkiLCJzdGF0dXMiOiJzdWNjZXNzIn0seyJjb250ZW50IjpbeyJzaWduYXR1cmUiOiJDQVFTMVFnS0VBZ1NHQUk0QVVJSWRHaHBibXRwYm1jU0RONlRmVTZORE05NzFsRWJ6eG9NdlJVQTFKOW94M2ZKUVlUOUlqQXJDUVFHSEJQNHBSSFBhdlh6UmxiTWJhWUhDNTdZaitHYnhncjBoZWx2ZDJPSUwvbzZSYnQ0UEZBM0ZhSUZVbGdxOGdjN0tRSTlGdEdudlNBa0xCUnpJc1VwLzJkNVJkMnpnRXNma0ZWZVhzbjhXWVlaa2luQVFWVnk2QTdCM0xhRUpXMHhiL3g4OERpNjdmZWR3eDBRK0ZsWnR3Y21UM0RjYWExMUp2ZXlIUnIzZG0rekdTWlA2Q1hlWE5KOG5yVFo3VzFZUlR5c0Q2dzdvN0xwOGgxUndxNGY0bzhqK1FVZlkwdmplK3pKWnN4SFBtUU50M3lrZUtoOTloZ242ZGJpejYvRUhxbWJIalNBdTFNNFNxRURmcUlLVGhIWWVuYW1BWm1jUTRYQU9vNm1kUTYrNzdWZ3BWMWxDMS9rNTJJaVVvSmprR2xRcm5MQ3Z1dGlJN0tOK3NnSzhjMisyS0NKclBSQ3ZWVitCa1N0dlA1aldXcXp5dHBLUStuSHpoTDQ0OE9ySUE5Z2RKQzM5QzhlRHJYZVJuSTd0NEZxbGtYR29NbFpBVjlVTWNyUUhSeE9NaTh0T3lUemk5b3lCRkRUU3NRK0xKcWVyMHR6MUZ0OWdlazZ2emc1MFNwVkFGUWRMRVJ2OFIxWXFNYWVJK3hzdlRBRVc5TVJndlJNa3pMdXpSVDJndXczakQrRmppcDM3eGdLNVFJMFIrQ3NPQW1RUG9UWml6emNJeUFVQXVDSkZDZ3EvZ0QvQ2liTHhveW1pVlVJMWFuOVNudDNLcE5Sb1dkdldJbmUzM1YwcHY4UEhnOXlXMkp1TE9xUUdpbExDc0svUTNvUXIzdXBFTVh4WEdqQ0w3V1VpenZSWkdGRUVWK1ZwbFl0N3Y0dEtaTWo1cGszdmRlTTBNSjVYWmc3UHZVc1J6eWN4Y25yVDNLbm9jM3FNK3R0cVpmVHNiOGQ2VS9wUWlmcTdHdWFoRk9IcnZyb3FnSkxqKy84MVl5MXBaRmtqZkpnWTBYa2x6QW5BNkNGcS9ZVDBneGJWK1RINUlTK1dhWU9VekszNlM4czVxcU13aG5ndjRJekUzY3RYWHFuaElaWE56bnlWQnFiWWEvR3gyYkNqY0VqMm41UnlHWVJTLytNY3VsZ0xCem5ROU1rRzJEN1FQM01xL253SFhLTUlhTFM4Vks4aEczZDByQWpsdm5hdVhTaE5ySnh0a2o0cWt3bnhENExyRWprMXVlK1BMY2JMcVlXd0FyOG4wcys0MWpyc0lQVnl5Szl2RmFFWnJBOUthQnhrSnlpcXg1MEdaTm43Y2t4K21rSTE4Z0g1Mml5VlB0VThGS01YanVnQ2w2bVBMaUhZaURRZCtHNjNMdWRtT0t5czlpdFZuQkRCTk1QMmIvSTcrYklWUmxoZUdhWldyNk1BVEtsODZCM3krMGt1T3JMUk15V2NkaWJjMmlRa1ptKzhmN2RUVXdiS2JCSnNLT0ZZNzMvOG5sTC9KaVlwano2blVJaytJcVNqYUZ5SzZYRmxDY3VrV3BXSHBESm5TdmJFemV5N3FIeUNxSitJK29uOGlQOEdPdGVtcFFhUzAvNGtaR1N3bHJFNnhPajZFd0dwZmpiRFpLSGtQLzJSQWROWVBRcTN6N2JkVkpNSk5uRk16NmUzN216TXBDQmlod1BkU0ZUUkUzU0NZNjZ3dlRnSnpHQ1V5aEhiU2k1aTFhenpPWGQ4dUhDU3BULzZMMW82VlUwcGpvUjhLYWE0d0NXSEdmN1AvV0VkK2F2eXAwMkpudTFjbXBXMCtHcSt2SzloMHRkYjJJQ2xNL2VOZmRvdVFQK2VwOE1KVWpyYURSSUNtQUpRWTVvYlM3MXZuaU9vK3JlYUZ5MHpSZ0IiLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsic2lnbmF0dXJlIjoiQ0FRUy9SRUtFUWdTR0FJNEFVSUpibUZ5Y21GMGFXOXVFZ3dGaDNBVkpaUDNzOHkxSHEwYURHNzZ1aEhPTmFTY1hBR1NoaUl3R2dqTFhIVXgxeVpya0xQbGpqelRPUXljcExKaUtWd1hjeG0vRXpLSm0xczhNNVd4dVJFV0VsZFhjb2FOMC9tc0twa1JNVCtlTGI1aWg5ckFIVUxUb21VTnNVd0pldTBqckplNEdMYXp5N3NQNUt0akVPRUJLUXNYVWZvUTdmRGtaT1FVSFppOXp6UU40aEZObVlKVWYzT1ZxZjlUUU5kQzErbS8vSEFaUGZXbUF5Nkh0RHZtN2tnQkprQ0VvaUZubTd0bi9QWDFoWEdQUmdXYzZCVFhVTXIyV0E4YUt5cWdnUlViTUgyK0pZdjVpV1pkZENjMXVwMU5icjNzRjdXUGNvdDlaQmh5SFFYcjY5MnplSms2U0pneWdJVnJBU2JNbHkyYkJ4VFdKQjYyM2dWZ3pwQ21WU0xaRnBrS1hnUGNwOGhHeURLT3ZGNlNpUVNIcUdSeFFvZnI3VnMxYVBoZlppeFJPSkdEK01scmtnZ1ZBbkdNeVlJRFVacGF2dENlaVUxSm1CU3owMDg0YlhQdFQvdG5wc0tPblNQWVpRbzNCMEJYS2JxQS9Xc0tuZzgvVjNaOFV1YWpvNklWWjJoZ0ExV0RkdXFRT1FrRXhHVTZwRnVMaGViSWp1V3hTTkkvRGZmdlIvL0xOcUNmdmRuZjBodm9QdER4ZngvU2F3SXg0L3NwcTl4YndGekh0TGdvSGcxRjJ3Rnc3Y2xQRGxnMVB5RC92MnlKRzNWOVQ4R0w2akdQRTZyaGZpbC91NEhOdmY3QlcwZFF6Q1RUWGNxWnk0bkJ5eTArcCtTN1p4Zk1sNEtweGEyZ09oaFNOamZwZnVYMFFwTDRrTUU4SjFubGlGTjFkOEtxRVB1REkxOEJRSFVoRC9VZlkyOW9BMHg1MTBGak0vQnNMWjRuNXNwcGtvSWk3Q0ZvUXEyYUFLVk5mQzFLaWxoSXBSRCsxZHlEdFpRa2xOOGdacE9FU0h3Nk1MMVR0VTB5ZkFjYm1xb3hKNmhoanp3SDZDTXNUUUVFeThNYlJCTStRS0trSVJkUkNQSEdGVzJMa3VSNTArSjBwRi90cjZwcVZ2SmVvemZpTW5yRWlsQjI3VFpQNG5LVEpHOHJkLy92Q1dlbjB5N0NBLzhkQko5Z2RwNXR4N05WQTQ1azFnZGN4RVIxZ1lvdU5YM24xdkczL0IxRjQrOTN3WFc5TXFvNlVzNTlUUW1yYjlOcWE3cFNRTlNySDA2dzFHeHlCQWxrRmZxQWxhZmsxNmFNcHE0MTZUSllwR3k0M1dDVWlDY0xXbGhpV2d6MnNnT0xBWFA5YTV3ZVdBS1JDOEo2amtjVWtEZVhuVytIRlNpUE02KzlCbzZrVmhMVWhONXJjNG1IWk9acEY3cHRQcHNRTjRYQ3FMUHNxcFJILzhvNVhweUM2Wm5lVGVGMkV4dGpod1pWalhEd01iSXpWWUlidzF5MlBQR3c4ZEM3cGM0c0lWU25JNG9RQUgwdjY1MGdoZmhtTWV2V1c0aDRQeWxidy9ERzRmZGVUMys4ejBmOE1PU1R0ZmlweDA1L1F0dEhsQXZPRFJ4YTV5REN2T3o2bjNmMUJPME4vNE1iR3lPTTI3cVc0eDhtQlZvcnlVWldmc21oZmkwL0dmTnZCOCt5bFRHYllOWDI3T2NFRjVXWnROcWJXRXk3VjVHYm1aVVN4c3BjZzdGQlozajMvM0w3bXV2WlhvWmkxZjVlcnE0eTNsU0grb0Y3cVBGbnZpZXlMT0N3dWJjUXkrb3lvd2o0Y0QwaWRSZWNucCtzKzUyRDcrbXpGMWdwVytmK2NKYXNveGlnYnhrT2xvUzR5ZWlLdmdNNG5XU3BHRWoxMlJQeDVvODBFbXppWE45dDR6SnpXMWh0QTVoWEpKZUZ5bXBNWXR5Q1VDZXZSV1ZtclJZa3RRN1VuQ2E2dEwvQVZQaTFuaTVYQ1hIZkxPdXFmSzVLTFl4andWdGRoaGZtQ0x2cGFLVk5DRU9DZjRuTkdIcUQ5OHdVYjBlaldOd0RZQm5KR2tCUnc5UlZzcGZ3TmRvV29WY24ycXY4eXdHVXA3eWMvc2syTHBJNlZ6aVJ5dldxYkdORzFaSnFWVnV0UmM0NitzT0U0VlNlUzIvQ1ROS05hNk5NQ2YvaHh4YVVWc1lmS2RQK1MxTGM0aWFuS3Awc1V1Z1BwbXRveWRDMENQMlJ3am5ITGNuenhDbnZxeGxMUVdYTU5QVHN1MnlCY0hsV0pMdGNtZGcvTWNMYWd5N2w1a0c2c043WnhKTEQva0F4OEU2dXFvaFArSy9qTVNyWXVRSm5ocjR0ZjRiaVdGWTNKemYvS3BBWXlWU3ErcGJmRktYTWJMOW5IRW9aUVQxNVd5YWJkNkRNQXEzUVpPdGhNNXBVemROcVVYa2NySTFQd2pBU0FnNGRWVS8ybys5OG1UWG55aFA3VmxFa1RuZGRwd3lKRitZQ0NjMDFwcWE2Wlg5OXBuUitZT3BJcmI1RHRQY0txMHQ1Y0ZDRHlReE4xbFV1cVpjWXVqdlgvR0M3KzZJcFJUei9Oa0VZLzVjbnRPZlpKeWQ2U2tVeC9KUjVBSklSUENOZWliMHJjV25uRm4wWEs4czIyeDQwd0xqRXR1ZlhETzkreXJhZjRrSHhsQkkzU0IwbWVNamVwWDUwQ0poQXg3TmVra1JGbndvZWNMZnFJeXJqWHJnQVpzVE1QaVg5NHNuamRyemlzQVh0QkthbnVsaWxTeE9mVzJIU21mbmswTDJHWEhKSjNTK3ZDYmdSRW1sOWRla2ZtSGhtd0VZdHYza2ZodGg3RVJJd1ZUOGo2OU90SUJDbWVUSUp1QTdvNzNQWGMxTEoyd2h1Wks3RlplNk5UT3M2MU1QN0pWbEljNnRiaHRPQmszTlhVOHBCUE9DWTZRNTVkNjNGdHBVVS9LQ2JRMEsyV1E5bzVqTkd3Tk5KNkN3NUZEQ3QreDVZSmVQMWQyUXpEU2lxK2NTWnF3cUNaVmNOb0FmdEdHemdyUlo0dmtwYlAweFFaSEN0V3NVd0hWVGpyWTROZlJEakhkVlBSbjhKOVh0b29jL2hJV2txekZhY3pZVGxhcHUyUkNKcWU4YUVvM2p6V1pDRUo3QWlCekJZcmFZRGdPZmVpUXllSlVzYmJYSGRHS2lpdzIyQmRwNE0rWXo2NzFNWjBpZTlFV1JrczBRMkRIR1VUbmU5UzR0QUFKUEdiVWY0eHRDTW9KNDBRNFFXMDVRTFNaeTN3TkZBVUZZbVhFNEVDREZCOCtJRWVORkZGL3RnQW5maDhHdkZoLzF4WStMRStvek1taW5qU1J3aHEwOXpRQUVrTkZ5dTBJS2d2RGt5T0kraWNjRjdkb1dMVFVhc1B4TEgxeEdQQlNLa2VXQ25Tb1gvNEMyYzlkdTY2QmJSWGFtSHFhZStWQ2lTNkJOOTUzaVlLT3U4ck1rbmVXK1RGalYwQ3pBZjhQTkc5TEJheVVnWTVVdzl4MFNhcDZ6STRQcGFaS0g0ZDlrdGtGVSs5MXlCYk41U1J0ZGQ4L0VRYXRKWG1CTmZ3N2tScmxsK0xqd1p3ZVdMRWIwalQxUnBHdURxSExkTFRNTWdvd1NjenJwMnpDc0pkUUgvc2VhWXJwd2tDRlI4ZFJqTTdKUlVrVmExQTBTdmRaQVg3NXZ1QnZ2TEJ6TjJjK1JidDVvcXk5M2xxSUJJaktiN3cyVWtSVGI0cEJXY05LTlBVZ3QzYXovUHhReEVhc2FsdGRtdXl2c1hZYnVyM1ZQTm44SnpOLzZKNU4rUUNuejU3MVRjY2FwQ21rUHZGRStnYXlYVStVRjFWS1l5RmNVdmxldjdsc1FsbWpxeksyUUE4WDdRM1R6RmFwcWh2SktsdDdkekJIcmFxWkJnUGdiOU40b0tOYXlpOEZaVDcxK2thWWhJaEZyRVNLWVJIZkhrajVuM2tmaGdMeSt4K2pMRGJLeE1nZ2tnSG5KejlSdUVIWlhCV3RrWUFRPT0iLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsiaWQiOiJ0b29sdV8wMUg2ZlY4SnZSSk52WHk0N2VSbmQzakgiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6e30sIm5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHdBTVJkUzIxRmVKZnpRaE0zcyIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo3Mzk2LCJvdXRwdXRfdG9rZW5zIjo3NzAsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjczOX0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoic2tlcHRpYyIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy00YWU5LTc2YzAtODlhNi00ZjBiMTE0ZTMwMzAtMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzM5Niwib3V0cHV0X3Rva2VucyI6NzcwLCJ0b3RhbF90b2tlbnMiOjgxNjYsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjczOX19fSx7ImNvbnRlbnQiOiJTdWNjZXNzZnVsbHkgdHJhbnNmZXJyZWQgdG8gdmVyaWZpZXIiLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7fSwidHlwZSI6InRvb2wiLCJuYW1lIjoidHJhbnNmZXJfdG9fdmVyaWZpZXIiLCJpZCI6ImMwNjQ1NjNhLTk3NGYtNGYwZi1hYjRhLTA4MGU2M2MwMWNmNSIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsInN0YXR1cyI6InN1Y2Nlc3MifV19" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJvdXRwdXQiOlt7ImdyYXBoIjpudWxsLCJ1cGRhdGUiOnsibWVzc2FnZXMiOlt7ImNvbnRlbnQiOlt7InNpZ25hdHVyZSI6IkNBUVM5Z1VLRUFnU0dBSTRBVUlJZEdocGJtdHBibWNTREdGeUoxTVhvWStBaTZOa2J4b01SZDJUclVmVVU3V3JQcEEySWpEZ3k4ZFVwbGNaS2M3RG5Kd05MVEJmSDVVTlIrbkduYWtTUFFpYThVVkl6cmprT25KMmZMcldVc0Eyb0VPOHlSc3Frd1hra0RDc3loQjk3ZHdqNTdSTXQ5MVh6Z1doTDlsdU5FR085MnlXMHJORkdvTVNVVk94UElyRmM1Ump5cmhLRWMzKzFSbnB4Q2t5bHErTE5sUTVORnhMbjZ2MmNGNUR6cXB2QkhOUjdYQWlnK3VySklVM2dYeWRDa2FHWlpEZXRKdFFPVU9YZ1ppOXo5MTAwTnBkQUVrWERvMklDSENHVEdwVmMwSi9wYlh4V1JCeGI2QjEvR2wwWWpuNjhWNkhsZXNIN0lvQzdHUi8zSHdSdTlOU0RCRzR0dGZRakVXY2crUGVBZWE0Um43anp4K1RtOUg3TDFqb1RLVmdEYlFnNWlLTUxmanZQcWxua3NDTHRYT3Q4alNJUzVPWUxON2EvN1RYNi93NWQzNHZudzh2UERtVTFVWWkwUTcycVVXaFVhMnZ3OE9YTnBjS0w3QnAwbE5mQlM4cnZIWSt2Qlk4L2hhQW9UTDN3dkxGVTFmVCs0ei92QTNCY3NpZ1pEWWxmSmVEWWN4WG1SdXF4enhFR3BYQ1JlOXMxSFd0SUQrNDFwSHU2bHBaSGtYYWdocGM2VFpITHhOd2NWaFRiNmZNUTdRbysyTDZQdmtsaDFtMzJwZW9keGVnMllpUGhkcGwzdy94a09FdTUvSkVGN1pCeHBkVUwrdTN3a081cFVsc3FmZ1g3dWh6djdIQzJXcFNXVmltcndpUXdRT1V4MCtKUHJMdHcrbFlmcTdJM2pydW8wR1cwZ3VXdm51ZjdKTTNZTGo4NHErSmdIb21uU1Nabm5abXJMMFVhUnZyVnlMbzg3N0YzOWNjTFM3WG9abm51aThKNFA1OFpUbWlMa0VKZUdjeDFJdWFnMmUzMzMzOU83WWV3LzU5dExiUXRJR2g5VldNd0hCbkZ4MzlTOC80TEVGcks1SGxkUFFoT3Jsb3lUdGtSczY2aVFSS3R4TUtTMFZJNlFUS1pEQWJzN2E3SDF5QnYrbUppOVhnNHVLbXlVNnFubjd6RUtqalkyYklsQ3dleng0TExHUDVIRDNacjZmWnNtZEcxVUh6RHNCdllHMm9DSTBvcUVLTSt2YWxzVDZoVThNUmpiUUJEL0pKOEpKTi9pQkdsaEJBVUhvMFVtaGh0UjJldVRWcFpXbE1uaG1wWEFkS0RYU2ZQZGFQb2hnQiIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJpZCI6InRvb2x1XzAxWEVBdWhRSENCZHlDd3R5bVp3RmZEaSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3JlZF90ZWFtIiwidHlwZSI6InRvb2xfdXNlIiwidG9vbHNldF9uYW1lIjpudWxsfV0sImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2Zkd0FzYWJEcXVlZ0hpNllVcENuIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjc1MDksIm91dHB1dF90b2tlbnMiOjg4LCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjo1NX0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoidmVyaWZpZXIiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtNjY4Mi03ZDIxLThjMTUtZmZiY2U5NDIyYzlmLTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoidHJhbnNmZXJfdG9fcmVkX3RlYW0iLCJhcmdzIjp7fSwiaWQiOiJ0b29sdV8wMVhFQXVoUUhDQmR5Q3d0eW1ad0ZmRGkiLCJ0eXBlIjoidG9vbF9jYWxsIn1dLCJpbnZhbGlkX3Rvb2xfY2FsbHMiOltdLCJ1c2FnZV9tZXRhZGF0YSI6eyJpbnB1dF90b2tlbnMiOjc1MDksIm91dHB1dF90b2tlbnMiOjg4LCJ0b3RhbF90b2tlbnMiOjc1OTcsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjU1fX19XX0sInJlc3VtZSI6bnVsbCwiZ290byI6W119XX0=" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "0676be444516237c", + "parentSpanId": "38a909e757fa3574", + "name": "FilesystemMiddleware.wrap_model_call", + "kind": 1, + "startTimeUnixNano": "1790968030846874880", + "endTimeUnixNano": "1790968032470281984", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "e30=" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "FilesystemMiddleware.wrap_model_call" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "1" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "verifier:544dea40-e16b-458c-1464-128e1b9b10a9|model:6b48cacc-ef10-0459-49d7-052bc435753c" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "verifier" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "verifier:544dea40-e16b-458c-1464-128e1b9b10a9" + } + }, + { + "key": "langsmith.metadata.ls_method", + "value": { + "stringValue": "traceable" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJvdXRwdXQiOnsicmVzdWx0IjpbeyJjb250ZW50IjpbeyJzaWduYXR1cmUiOiJDQVFTOWdVS0VBZ1NHQUk0QVVJSWRHaHBibXRwYm1jU0RHRnlKMU1Yb1krQWk2TmtieG9NUmQyVHJVZlVVN1dyUHBBMklqRGd5OGRVcGxjWktjN0RuSndOTFRCZkg1VU5SK25HbmFrU1BRaWE4VVZJenJqa09uSjJmTHJXVXNBMm9FTzh5UnNxa3dYa2tEQ3N5aEI5N2R3ajU3Uk10OTFYemdXaEw5bHVORUdPOTJ5VzByTkZHb01TVVZPeFBJckZjNVJqeXJoS0VjMysxUm5weENreWxxK0xObFE1TkZ4TG42djJjRjVEenFwdkJITlI3WEFpZyt1ckpJVTNnWHlkQ2thR1paRGV0SnRRT1VPWGdaaTl6OTEwME5wZEFFa1hEbzJJQ0hDR1RHcFZjMEovcGJYeFdSQnhiNkIxL0dsMFlqbjY4VjZIbGVzSDdJb0M3R1IvM0h3UnU5TlNEQkc0dHRmUWpFV2NnK1BlQWVhNFJuN2p6eCtUbTlIN0wxam9US1ZnRGJRZzVpS01MZmp2UHFsbmtzQ0x0WE90OGpTSVM1T1lMTjdhLzdUWDYvdzVkMzR2bnc4dlBEbVUxVVlpMFE3MnFVV2hVYTJ2dzhPWE5wY0tMN0JwMGxOZkJTOHJ2SFkrdkJZOC9oYUFvVEwzd3ZMRlUxZlQrNHovdkEzQmNzaWdaRFlsZkplRFljeFhtUnVxeHp4RUdwWENSZTlzMUhXdElEKzQxcEh1NmxwWkhrWGFnaHBjNlRaSEx4TndjVmhUYjZmTVE3UW8rMkw2UHZrbGgxbTMycGVvZHhlZzJZaVBoZHBsM3cveGtPRXU1L0pFRjdaQnhwZFVMK3Uzd2tPNXBVbHNxZmdYN3VoenY3SEMyV3BTV1ZpbXJ3aVF3UU9VeDArSlByTHR3K2xZZnE3STNqcnVvMEdXMGd1V3ZudWY3Sk0zWUxqODRxK0pnSG9tblNTWm5uWm1yTDBVYVJ2clZ5TG84NzdGMzljY0xTN1hvWm5udWk4SjRQNThaVG1pTGtFSmVHY3gxSXVhZzJlMzMzMzlPN1lldy81OXRMYlF0SUdoOVZXTXdIQm5GeDM5UzgvNExFRnJLNUhsZFBRaE9ybG95VHRrUnM2NmlRUkt0eE1LUzBWSTZRVEtaREFiczdhN0gxeUJ2K21KaTlYZzR1S215VTZxbm43ekVLampZMmJJbEN3ZXp4NExMR1A1SEQzWnI2ZlpzbWRHMVVIekRzQnZZRzJvQ0kwb3FFS00rdmFsc1Q2aFU4TVJqYlFCRC9KSjhKSk4vaUJHbGhCQVVIbzBVbWhodFIyZXVUVnBaV2xNbmhtcFhBZEtEWFNmUGRhUG9oZ0IiLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsiaWQiOiJ0b29sdV8wMVhFQXVoUUhDQmR5Q3d0eW1ad0ZmRGkiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6e30sIm5hbWUiOiJ0cmFuc2Zlcl90b19yZWRfdGVhbSIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHdBc2FiRHF1ZWdIaTZZVXBDbiIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo3NTA5LCJvdXRwdXRfdG9rZW5zIjo4OCwib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6NTV9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InZlcmlmaWVyIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTY2ODItN2QyMS04YzE1LWZmYmNlOTQyMmM5Zi0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6InRyYW5zZmVyX3RvX3JlZF90ZWFtIiwiYXJncyI6e30sImlkIjoidG9vbHVfMDFYRUF1aFFIQ0JkeUN3dHltWndGZkRpIiwidHlwZSI6InRvb2xfY2FsbCJ9XSwiaW52YWxpZF90b29sX2NhbGxzIjpbXSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjo3NTA5LCJvdXRwdXRfdG9rZW5zIjo4OCwidG90YWxfdG9rZW5zIjo3NTk3LCJpbnB1dF90b2tlbl9kZXRhaWxzIjp7ImNhY2hlX3JlYWQiOjAsImNhY2hlX2NyZWF0aW9uIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VuX2RldGFpbHMiOnsicmVhc29uaW5nIjo1NX19fV0sInN0cnVjdHVyZWRfcmVzcG9uc2UiOm51bGx9fQ==" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "5846c65e1a0fba02", + "parentSpanId": "0676be444516237c", + "name": "UnsupportedContentMiddleware.wrap_model_call", + "kind": 1, + "startTimeUnixNano": "1790968030847229952", + "endTimeUnixNano": "1790968032470125056", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "e30=" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "UnsupportedContentMiddleware.wrap_model_call" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "1" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "verifier:544dea40-e16b-458c-1464-128e1b9b10a9|model:6b48cacc-ef10-0459-49d7-052bc435753c" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "verifier" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "verifier:544dea40-e16b-458c-1464-128e1b9b10a9" + } + }, + { + "key": "langsmith.metadata.ls_method", + "value": { + "stringValue": "traceable" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJvdXRwdXQiOnsicmVzdWx0IjpbeyJjb250ZW50IjpbeyJzaWduYXR1cmUiOiJDQVFTOWdVS0VBZ1NHQUk0QVVJSWRHaHBibXRwYm1jU0RHRnlKMU1Yb1krQWk2TmtieG9NUmQyVHJVZlVVN1dyUHBBMklqRGd5OGRVcGxjWktjN0RuSndOTFRCZkg1VU5SK25HbmFrU1BRaWE4VVZJenJqa09uSjJmTHJXVXNBMm9FTzh5UnNxa3dYa2tEQ3N5aEI5N2R3ajU3Uk10OTFYemdXaEw5bHVORUdPOTJ5VzByTkZHb01TVVZPeFBJckZjNVJqeXJoS0VjMysxUm5weENreWxxK0xObFE1TkZ4TG42djJjRjVEenFwdkJITlI3WEFpZyt1ckpJVTNnWHlkQ2thR1paRGV0SnRRT1VPWGdaaTl6OTEwME5wZEFFa1hEbzJJQ0hDR1RHcFZjMEovcGJYeFdSQnhiNkIxL0dsMFlqbjY4VjZIbGVzSDdJb0M3R1IvM0h3UnU5TlNEQkc0dHRmUWpFV2NnK1BlQWVhNFJuN2p6eCtUbTlIN0wxam9US1ZnRGJRZzVpS01MZmp2UHFsbmtzQ0x0WE90OGpTSVM1T1lMTjdhLzdUWDYvdzVkMzR2bnc4dlBEbVUxVVlpMFE3MnFVV2hVYTJ2dzhPWE5wY0tMN0JwMGxOZkJTOHJ2SFkrdkJZOC9oYUFvVEwzd3ZMRlUxZlQrNHovdkEzQmNzaWdaRFlsZkplRFljeFhtUnVxeHp4RUdwWENSZTlzMUhXdElEKzQxcEh1NmxwWkhrWGFnaHBjNlRaSEx4TndjVmhUYjZmTVE3UW8rMkw2UHZrbGgxbTMycGVvZHhlZzJZaVBoZHBsM3cveGtPRXU1L0pFRjdaQnhwZFVMK3Uzd2tPNXBVbHNxZmdYN3VoenY3SEMyV3BTV1ZpbXJ3aVF3UU9VeDArSlByTHR3K2xZZnE3STNqcnVvMEdXMGd1V3ZudWY3Sk0zWUxqODRxK0pnSG9tblNTWm5uWm1yTDBVYVJ2clZ5TG84NzdGMzljY0xTN1hvWm5udWk4SjRQNThaVG1pTGtFSmVHY3gxSXVhZzJlMzMzMzlPN1lldy81OXRMYlF0SUdoOVZXTXdIQm5GeDM5UzgvNExFRnJLNUhsZFBRaE9ybG95VHRrUnM2NmlRUkt0eE1LUzBWSTZRVEtaREFiczdhN0gxeUJ2K21KaTlYZzR1S215VTZxbm43ekVLampZMmJJbEN3ZXp4NExMR1A1SEQzWnI2ZlpzbWRHMVVIekRzQnZZRzJvQ0kwb3FFS00rdmFsc1Q2aFU4TVJqYlFCRC9KSjhKSk4vaUJHbGhCQVVIbzBVbWhodFIyZXVUVnBaV2xNbmhtcFhBZEtEWFNmUGRhUG9oZ0IiLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsiaWQiOiJ0b29sdV8wMVhFQXVoUUhDQmR5Q3d0eW1ad0ZmRGkiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6e30sIm5hbWUiOiJ0cmFuc2Zlcl90b19yZWRfdGVhbSIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHdBc2FiRHF1ZWdIaTZZVXBDbiIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo3NTA5LCJvdXRwdXRfdG9rZW5zIjo4OCwib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6NTV9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InZlcmlmaWVyIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTY2ODItN2QyMS04YzE1LWZmYmNlOTQyMmM5Zi0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6InRyYW5zZmVyX3RvX3JlZF90ZWFtIiwiYXJncyI6e30sImlkIjoidG9vbHVfMDFYRUF1aFFIQ0JkeUN3dHltWndGZkRpIiwidHlwZSI6InRvb2xfY2FsbCJ9XSwiaW52YWxpZF90b29sX2NhbGxzIjpbXSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjo3NTA5LCJvdXRwdXRfdG9rZW5zIjo4OCwidG90YWxfdG9rZW5zIjo3NTk3LCJpbnB1dF90b2tlbl9kZXRhaWxzIjp7ImNhY2hlX3JlYWQiOjAsImNhY2hlX2NyZWF0aW9uIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VuX2RldGFpbHMiOnsicmVhc29uaW5nIjo1NX19fV0sInN0cnVjdHVyZWRfcmVzcG9uc2UiOm51bGx9fQ==" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "7d9bd24669f22c02", + "parentSpanId": "5846c65e1a0fba02", + "name": "ChatAnthropic", + "kind": 1, + "startTimeUnixNano": "1790968030850436096", + "endTimeUnixNano": "1790968032469747968", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chat" + } + }, + { + "key": "gen_ai.serialized.name", + "value": { + "stringValue": "ChatAnthropic" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "llm" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "ChatAnthropic" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "anthropic" + } + }, + { + "key": "gen_ai.request.model", + "value": { + "stringValue": "claude-sonnet" + } + }, + { + "key": "gen_ai.tool.definitions", + "value": { + "stringValue": "[{\"name\":\"ls\",\"input_schema\":{\"properties\":{\"path\":{\"description\":\"Absolute path to the directory to list. Must be absolute, not relative.\",\"type\":\"string\"}},\"required\":[\"path\"],\"type\":\"object\"},\"description\":\"Lists all files in a directory.\\n\\nThis is useful for exploring the filesystem and finding the right file to read or edit.\\nYou should almost ALWAYS use this tool before using the read_file or edit_file tools.\"},{\"name\":\"read_file\",\"input_schema\":{\"properties\":{\"file_path\":{\"description\":\"Absolute path to the file to read. Must be absolute, not relative.\",\"type\":\"string\"},\"offset\":{\"default\":0,\"description\":\"Line number to start reading from (0-indexed). Use for pagination of large files.\",\"type\":\"integer\"},\"limit\":{\"default\":100,\"description\":\"Maximum number of lines to read. Use for pagination of large files.\",\"type\":\"integer\"}},\"required\":[\"file_path\"],\"type\":\"object\"},\"description\":\"Reads a file from the filesystem. Assume any path the user provides is valid; reading a missing file returns an error.\\n\\nUsage:\\n- By default, it reads up to 100 lines starting from the beginning of the file. Use `offset`/`limit` to page through large files instead of reading them whole.\\n- A status header, `@@ field | field | ... @@`, sits above the file content, and every line after it is verbatim file content. When content is truncated, there may be an explanation before the header. Never include the header when editing.\\n- Speculatively batch multiple `read_file` calls in one response when several files may be useful.\\n- An empty file returns a system-reminder warning in place of contents.\\n- Large tool results may be offloaded to a file; the tool message gives the path. Read that path here, paging with `offset`/`limit`.\\n- Images (`.png`, `.jpg`, etc.), audio, video, and PDFs return multimodal content blocks (https://docs.langchain.com/oss/python/langchain/messages#multimodal).\\n- For images and PDFs, pagination via `offset`/`limit` is text-only - supply `file_path` only\\n- Always read a file before editing it.\"},{\"name\":\"glob\",\"input_schema\":{\"properties\":{\"pattern\":{\"description\":\"Glob pattern to match files (e.g., '*.py', '**/*.py', '/subdir/**/*.md'). A pattern without '/' matches the file name at any depth; a pattern containing '/' matches the search-root-relative path; a leading '/' anchors to the search root ('/*.py' matches only top-level files). Leading-dot names are excluded unless the pattern segment starts with '.', so prefer the bare form '*.py' over '**/*.py' -- '**' will not descend into dot-directories like '.github'.\",\"type\":\"string\"},\"path\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"null\"}],\"default\":null,\"description\":\"Base directory to search from. Defaults to the backend's default root.\"}},\"required\":[\"pattern\"],\"type\":\"object\"},\"description\":\"Find files matching a glob pattern, returning absolute paths.\\n\\nSupports `*` (any characters within a path segment), `**` (any directories), `?` (single character), `[abc]` (one character from a set), and `{a,b}` (alternatives), e.g. `*.py`, `src/**/*.py`, `*.{yml,yaml}`.\\n\\nA pattern without `/` matches the file name at any depth under the search root (`*.py` matches `src/app/main.py`). A pattern containing `/` matches the search-root-relative path (`src/**/*.py`). A leading `/` anchors to the search root (`/*.py` matches only top-level Python files).\\n\\nLeading-dot names are only matched when the pattern segment itself starts with `.` (use `.env`, or `.github/**/*.yml`). Because `**` will not descend into dot-directories, the bare form `*.yml` is *broader* than `**/*.yml` and is usually what you want.\"},{\"name\":\"grep\",\"input_schema\":{\"properties\":{\"pattern\":{\"description\":\"Text pattern to search for (literal string, not regex).\",\"type\":\"string\"},\"path\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"null\"}],\"default\":null,\"description\":\"Directory to search in. Defaults to current working directory.\"},\"glob\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"null\"}],\"default\":null,\"description\":\"Glob pattern (NOT regex) limiting which files are searched (e.g. '*.py', '*.ts'). A pattern without '/' matches the file name at any depth; a pattern containing '/' matches the search-root-relative path (e.g. 'src/**/*.py'). This is an in-tool file filter, not a call to the separate glob tool. Brace expansion (e.g. '*.{ts,tsx}') is not supported on all backends; run a separate search per extension for reliable results.\"},\"output_mode\":{\"default\":\"files_with_matches\",\"description\":\"Shape of the returned text. 'files_with_matches' (default): newline-separated matching file paths. 'content': matching lines grouped by file under a ':' header, each line indented and formatted ': ' (only the matched line, no surrounding context). 'count': one ': ' line per file.\",\"enum\":[\"files_with_matches\",\"content\",\"count\"],\"type\":\"string\"},\"max_count\":{\"anyOf\":[{\"exclusiveMinimum\":0,\"type\":\"integer\"},{\"type\":\"null\"}],\"default\":null,\"description\":\"Optional cap on the total number of matches returned across all files. Leave unset to use the configured default. When the cap is hit, results are truncated and a note says so; narrow the pattern or path to see the rest.\"}},\"required\":[\"pattern\"],\"type\":\"object\"},\"description\":\"Search for a LITERAL text pattern across files (NOT regex).\\n\\nThe pattern is matched verbatim: regex metacharacters are ordinary characters, not operators. To match any of several strings, run a separate grep for each; `grep(pattern=\\\"foo|bar\\\")` searches for the literal text \\\"foo|bar\\\", and `.*` or `\\\\.` match those characters literally.\\n\\nReturns matching files or content per `output_mode`. Offloaded large tool results live under the artifacts root (`/large_tool_results/` by default); grep that directory to search them when you do not know the exact path.\"},{\"name\":\"web_search\",\"input_schema\":{\"properties\":{\"query\":{\"type\":\"string\"}},\"required\":[\"query\"],\"type\":\"object\"},\"description\":\"Search the web with Exa for current facts and return excerpts with source URLs\"},{\"name\":\"transfer_to_researcher\",\"input_schema\":{\"properties\":{},\"type\":\"object\"},\"description\":\"Request a correction from the researcher\"},{\"name\":\"transfer_to_red_team\",\"input_schema\":{\"properties\":{},\"type\":\"object\"},\"description\":\"Send verified evidence for adversarial review\"},{\"name\":\"transfer_to_editor\",\"input_schema\":{\"properties\":{},\"type\":\"object\"},\"description\":\"Send verified findings to the editor\"}]" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_chat_model" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "1" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "verifier:544dea40-e16b-458c-1464-128e1b9b10a9|model:6b48cacc-ef10-0459-49d7-052bc435753c" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "verifier" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "verifier:544dea40-e16b-458c-1464-128e1b9b10a9" + } + }, + { + "key": "langsmith.metadata.ls_provider", + "value": { + "stringValue": "anthropic" + } + }, + { + "key": "langsmith.metadata.ls_model_name", + "value": { + "stringValue": "claude-sonnet" + } + }, + { + "key": "langsmith.metadata.ls_model_type", + "value": { + "stringValue": "chat" + } + }, + { + "key": "langsmith.metadata.ls_max_tokens", + "value": { + "intValue": "4096" + } + }, + { + "key": "langsmith.metadata.lc_versions", + "value": { + "stringValue": "{\"langchain-core\":\"1.6.6\",\"langchain\":\"1.4.3\",\"langchain-anthropic\":\"1.7.5\"}" + } + }, + { + "key": "langsmith.metadata.ls_method", + "value": { + "stringValue": "traceable" + } + }, + { + "key": "langsmith.metadata.model", + "value": { + "stringValue": "claude-sonnet" + } + }, + { + "key": "langsmith.metadata.max_tokens", + "value": { + "intValue": "4096" + } + }, + { + "key": "langsmith.metadata.model_kwargs", + "value": { + "stringValue": "{\"extra_body\":{\"extra_headers\":{\"anthropic-workspace-id\":\"[redacted workspace]\"}}}" + } + }, + { + "key": "langsmith.metadata.streaming", + "value": { + "boolValue": false + } + }, + { + "key": "langsmith.metadata.max_retries", + "value": { + "intValue": "2" + } + }, + { + "key": "langsmith.metadata._type", + "value": { + "stringValue": "anthropic-chat" + } + }, + { + "key": "langsmith.metadata.usage_metadata", + "value": { + "stringValue": "{\"input_tokens\":7509,\"output_tokens\":88,\"total_tokens\":7597,\"input_token_details\":{\"cache_read\":0,\"cache_creation\":0,\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_token_details\":{\"reasoning\":55}}" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "seq:step:1" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W1t7ImxjIjoxLCJ0eXBlIjoiY29uc3RydWN0b3IiLCJpZCI6WyJsYW5nY2hhaW4iLCJzY2hlbWEiLCJtZXNzYWdlcyIsIlN5c3RlbU1lc3NhZ2UiXSwia3dhcmdzIjp7ImNvbnRlbnQiOiJZb3UgYXJlIHRoZSB2ZXJpZmllci4gSW5kZXBlbmRlbnRseSBzZWFyY2ggdG8gY2hlY2sgY2xhaW1zIGFuZCBzb3VyY2UgVVJMcy4gSWYgZXZpZGVuY2UgaXMgd2Vhaywgc2VuZCB0aGUgaXNzdWUgdG8gdGhlIHJlc2VhcmNoZXIuIE90aGVyd2lzZSBzZW5kIHlvdXIgdmVyZGljdCB0byByZWRfdGVhbS4gRG8gbm90IGdpdmUgdGhlIGZpbmFsIGFuc3dlci4iLCJ0eXBlIjoic3lzdGVtIn19LHsibGMiOjEsInR5cGUiOiJjb25zdHJ1Y3RvciIsImlkIjpbImxhbmdjaGFpbiIsInNjaGVtYSIsIm1lc3NhZ2VzIiwiSHVtYW5NZXNzYWdlIl0sImt3YXJncyI6eyJjb250ZW50IjoiRm9yIGEgc3ludGhldGljIHRyYWNpbmcgZml4dHVyZSwgZXhwbGFpbiBQeXRob24gdHVwbGUgdmVyc3VzIGxpc3QgaW1tdXRhYmlsaXR5IGluIGF0IG1vc3QgMTIwIHdvcmRzLiBVc2Ugb25seSBvZmZpY2lhbCBQeXRob24gZG9jdW1lbnRhdGlvbiBpZiB5b3Ugc2VhcmNoLiBIYXZlIHRoZSBza2VwdGljIGFuZCB2ZXJpZmllciBjaGVjayBpdCwgdGhlbiBoYXZlIHRoZSBlZGl0b3Igd3JpdGUgL2Fuc3dlci5tZC4gS2VlcCB0aGUgcnVuIHNob3J0IGFuZCBkbyBub3QgYXNrIGZvbGxvdy11cCBxdWVzdGlvbnMuIiwidHlwZSI6Imh1bWFuIiwiaWQiOiI0NjIwNDMwNS1mOGVlLTQzODQtOWNlOC1mY2Q4MGJmY2RmNzUifX0seyJsYyI6MSwidHlwZSI6ImNvbnN0cnVjdG9yIiwiaWQiOlsibGFuZ2NoYWluIiwic2NoZW1hIiwibWVzc2FnZXMiLCJBSU1lc3NhZ2UiXSwia3dhcmdzIjp7ImNvbnRlbnQiOlt7ImlkIjoidG9vbHVfMDFCMkxmSEdEallTRkpobWlYTmo0Q3VRIiwiY2FsbGVyIjp7InR5cGUiOiJkaXJlY3QifSwiaW5wdXQiOnsicXVlcnkiOiJkb2NzLnB5dGhvbi5vcmcgdHVwbGVzIGltbXV0YWJsZSBzZXF1ZW5jZXMgbGlzdHMgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgdHV0b3JpYWwifSwibmFtZSI6IndlYl9zZWFyY2giLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2ZkdzlhVzlicmVuUHFneWJmTndQIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjI4NDUsIm91dHB1dF90b2tlbnMiOjczLCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjowfSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCIsIm1vZGVsX3Byb3ZpZGVyIjoiYW50aHJvcGljIn0sInR5cGUiOiJhaSIsIm5hbWUiOiJyZXNlYXJjaGVyIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTIxYTMtNzAwMC1hMzgxLTI4NGE2YTBjMDNmNy0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6IndlYl9zZWFyY2giLCJhcmdzIjp7InF1ZXJ5IjoiZG9jcy5weXRob24ub3JnIHR1cGxlcyBpbW11dGFibGUgc2VxdWVuY2VzIGxpc3RzIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIHR1dG9yaWFsIn0sImlkIjoidG9vbHVfMDFCMkxmSEdEallTRkpobWlYTmo0Q3VRIiwidHlwZSI6InRvb2xfY2FsbCJ9XSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjoyODQ1LCJvdXRwdXRfdG9rZW5zIjo3MywidG90YWxfdG9rZW5zIjoyOTE4LCJpbnB1dF90b2tlbl9kZXRhaWxzIjp7ImNhY2hlX3JlYWQiOjAsImNhY2hlX2NyZWF0aW9uIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VuX2RldGFpbHMiOnsicmVhc29uaW5nIjowfX0sImludmFsaWRfdG9vbF9jYWxscyI6W119fSx7ImxjIjoxLCJ0eXBlIjoiY29uc3RydWN0b3IiLCJpZCI6WyJsYW5nY2hhaW4iLCJzY2hlbWEiLCJtZXNzYWdlcyIsIlRvb2xNZXNzYWdlIl0sImt3YXJncyI6eyJjb250ZW50IjoiW3tcInRpdGxlXCI6IFwiNS4gRGF0YSBTdHJ1Y3R1cmVzIFxcdTIwMTQgUHl0aG9uIDMuMTQuNSBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy90dXRvcmlhbC9kYXRhc3RydWN0dXJlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJZb3UgbWlnaHQgaGF2ZSBub3RpY2VkIHRoYXQgbWV0aG9kcyBsaWtlYGluc2VydGAsYHJlbW92ZWAgb3Jgc29ydGAgdGhhdCBvbmx5IG1vZGlmeSB0aGUgbGlzdCBoYXZlIG5vIHJldHVybiB2YWx1ZSBwcmludGVkIFxcdTIwMTMgdGhleSByZXR1cm4gdGhlIGRlZmF1bHRgTm9uZWAuIFsxXSBUaGlzIGlzIGEgZGVzaWduIHByaW5jaXBsZSBmb3IgYWxsIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIGluIFB5dGhvbi5cXG4uLi5cXG4jIyA1LjMuIFR1cGxlcyBhbmQgU2VxdWVuY2VzXFx1MDBiNlxcblxcbldlIHNhdyB0aGF0IGxpc3RzIGFuZCBzdHJpbmdzIGhhdmUgbWFueSBjb21tb24gcHJvcGVydGllcywgc3VjaCBhcyBpbmRleGluZyBhbmQgc2xpY2luZyBvcGVyYXRpb25zLiBUaGV5IGFyZSB0d28gZXhhbXBsZXMgb2Ygc2VxdWVuY2UgZGF0YSB0eXBlcyAoc2VlIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgbGlzdCwgdHVwbGUsIHJhbmdlKS4gU2luY2UgUHl0aG9uIGlzIGFuIGV2b2x2aW5nIGxhbmd1YWdlLCBvdGhlciBzZXF1ZW5jZSBkYXRhIHR5cGVzIG1heSBiZSBhZGRlZC4gVGhlcmUgaXMgYWxzbyBhbm90aGVyIHN0YW5kYXJkIHNlcXVlbmNlIGRhdGEgdHlwZTogdGhlIHR1cGxlLlxcblxcbkEgdHVwbGUgY29uc2lzdHMgb2YgYSBudW1iZXIgb2YgdmFsdWVzIHNlcGFyYXRlZCBieSBjb21tYXMsIGZvciBpbnN0YW5jZTpcXG4uLi5cXG4+Pj4gIyBUdXBsZXMgYXJlIGltbXV0YWJsZTpcXG4+Pj4gdFswXSA9IDg4ODg4XFxuVHJhY2ViYWNrIChtb3N0IHJlY2VudCBjYWxsIGxhc3QpOlxcbiAgRmlsZSBcXFwiPHN0ZGluPlxcXCIsIGxpbmUgMSwgaW4gPG1vZHVsZT5cXG5UeXBlRXJyb3I6ICd0dXBsZScgb2JqZWN0IGRvZXMgbm90IHN1cHBvcnQgaXRlbSBhc3NpZ25tZW50XFxuPj4+ICMgYnV0IHRoZXkgY2FuIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzOlxcbj4+PiB2ID0gKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbj4+PiB2XFxuKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbi4uLlxcbkFzIHlvdSBzZWUsIG9uIG91dHB1dCB0dXBsZXMgYXJlIGFsd2F5cyBlbmNsb3NlZCBpbiBwYXJlbnRoZXNlcywgc28gdGhhdCBuZXN0ZWQgdHVwbGVzIGFyZSBpbnRlcnByZXRlZCBjb3JyZWN0bHk7IHRoZXkgbWF5IGJlIGlucHV0IHdpdGggb3Igd2l0aG91dCBzdXJyb3VuZGluZyBwYXJlbnRoZXNlcywgYWx0aG91Z2ggb2Z0ZW4gcGFyZW50aGVzZXMgYXJlIG5lY2Vzc2FyeSBhbnl3YXkgKGlmIHRoZSB0dXBsZSBpcyBwYXJ0IG9mIGEgbGFyZ2VyIGV4cHJlc3Npb24pLiBJdCBpcyBub3QgcG9zc2libGUgdG8gYXNzaWduIHRvIHRoZSBpbmRpdmlkdWFsIGl0ZW1zIG9mIGEgdHVwbGUsIGhvd2V2ZXIgaXQgaXMgcG9zc2libGUgdG8gY3JlYXRlIHR1cGxlcyB3aGljaCBjb250YWluIG11dGFibGUgb2JqZWN0cywgc3VjaCBhcyBsaXN0cy5cXG5cXG5UaG91Z2ggdHVwbGVzIG1heSBzZWVtIHNpbWlsYXIgdG8gbGlzdHMsIHRoZXkgYXJlIG9mdGVuIHVzZWQgaW4gZGlmZmVyZW50IHNpdHVhdGlvbnMgYW5kIGZvciBkaWZmZXJlbnQgcHVycG9zZXMuIFR1cGxlcyBhcmUgaW1tdXRhYmxlLCBhbmQgdXN1YWxseSBjb250YWluIGEgaGV0ZXJvZ2VuZW91cyBzZXF1ZW5jZSBvZiBlbGVtZW50cyB0aGF0IGFyZSBhY2Nlc3NlZCB2aWEgdW5wYWNraW5nIChzZWUgbGF0ZXIgaW4gdGhpcyBzZWN0aW9uKSBvciBpbmRleGluZyAob3IgZXZlbiBieSBhdHRyaWJ1dGUgaW4gdGhlIGNhc2Ugb2YgbmFtZWR0dXBsZXMpLiBMaXN0cyBhcmUgbXV0YWJsZSwgYW5kIHRoZWlyIGVsZW1lbnRzIGFyZSB1c3VhbGx5IGhvbW9nZW5lb3VzIGFuZCBhcmUgYWNjZXNzZWQgYnkgaXRlcmF0aW5nIG92ZXIgdGhlIGxpc3QuXFxuLi4uXFxuQW5vdGhlciB1c2VmdWwgZGF0YSB0eXBlIGJ1aWx0IGludG8gUHl0aG9uIGlzIHRoZSBkaWN0aW9uYXJ5IChzZWUgTWFwcGluZyBUeXBlcyBcXHUyMDE0IGRpY3QpLiBEaWN0aW9uYXJpZXMgYXJlIHNvbWV0aW1lcyBmb3VuZCBpbiBvdGhlciBsYW5ndWFnZXMgYXMgXFx1MjAxY2Fzc29jaWF0aXZlIG1lbW9yaWVzXFx1MjAxZCBvciBcXHUyMDFjYXNzb2NpYXRpdmUgYXJyYXlzXFx1MjAxZC4gVW5saWtlIHNlcXVlbmNlcywgd2hpY2ggYXJlIGluZGV4ZWQgYnkgYSByYW5nZSBvZiBudW1iZXJzLCBkaWN0aW9uYXJpZXMgYXJlIGluZGV4ZWQgYnkga2V5cywgd2hpY2ggY2FuIGJlIGFueSBpbW11dGFibGUgdHlwZTsgc3RyaW5ncyBhbmQgbnVtYmVycyBjYW4gYWx3YXlzIGJlIGtleXMuIFR1cGxlcyBjYW4gYmUgdXNlZCBhcyBrZXlzIGlmIHRoZXkgY29udGFpbiBvbmx5IHN0cmluZ3MsIG51bWJlcnMsIG9yIHR1cGxlczsgaWYgYSB0dXBsZSBjb250YWlucyBhbnkgbXV0YWJsZSBvYmplY3QgZWl0aGVyIGRpcmVjdGx5IG9yIGluZGlyZWN0bHksIGl0IGNhbm5vdCBiZSB1c2VkIGFzIGEga2V5LiBZb3UgY2FuXFx1MjAxOXQgdXNlIGxpc3RzIGFzIGtleXMsIHNpbmNlIGxpc3RzIGNhbiBiZSBtb2RpZmllZCBpbiBwbGFjZSB1c2luZyBpbmRleCBhc3NpZ25tZW50cywgc2xpY2UgYXNzaWdubWVudHMsIG9yIG1ldGhvZHMgbGlrZSBhcHBlbmQoKSBhbmQgZXh0ZW5kKCkuXCJdfSwge1widGl0bGVcIjogXCIzLiBEYXRhIG1vZGVsIFxcdTIwMTQgUHl0aG9uIDMuMTQuNSBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9yZWZlcmVuY2UvZGF0YW1vZGVsLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcIlRoZSB2YWx1ZSBvZiBzb21lIG9iamVjdHMgY2FuIGNoYW5nZS4gT2JqZWN0cyB3aG9zZSB2YWx1ZSBjYW4gY2hhbmdlIGFyZSBzYWlkIHRvIGJlIG11dGFibGU7IG9iamVjdHMgd2hvc2UgdmFsdWUgaXMgdW5jaGFuZ2VhYmxlIG9uY2UgdGhleSBhcmUgY3JlYXRlZCBhcmUgY2FsbGVkIGltbXV0YWJsZS4gKFRoZSB2YWx1ZSBvZiBhbiBpbW11dGFibGUgY29udGFpbmVyIG9iamVjdCB0aGF0IGNvbnRhaW5zIGEgcmVmZXJlbmNlIHRvIGEgbXV0YWJsZSBvYmplY3QgY2FuIGNoYW5nZSB3aGVuIHRoZSBsYXR0ZXJcXHUyMDE5cyB2YWx1ZSBpcyBjaGFuZ2VkOyBob3dldmVyIHRoZSBjb250YWluZXIgaXMgc3RpbGwgY29uc2lkZXJlZCBpbW11dGFibGUsIGJlY2F1c2UgdGhlIGNvbGxlY3Rpb24gb2Ygb2JqZWN0cyBpdCBjb250YWlucyBjYW5ub3QgYmUgY2hhbmdlZC4gU28sIGltbXV0YWJpbGl0eSBpcyBub3Qgc3RyaWN0bHkgdGhlIHNhbWUgYXMgaGF2aW5nIGFuIHVuY2hhbmdlYWJsZSB2YWx1ZSwgaXQgaXMgbW9yZSBzdWJ0bGUuKSBBbiBvYmplY3RcXHUyMDE5cyBtdXRhYmlsaXR5IGlzIGRldGVybWluZWQgYnkgaXRzIHR5cGU7IGZvciBpbnN0YW5jZSwgbnVtYmVycywgc3RyaW5ncyBhbmQgdHVwbGVzIGFyZSBpbW11dGFibGUsIHdoaWxlIGRpY3Rpb25hcmllcyBhbmQgbGlzdHMgYXJlIG11dGFibGUuXFxuLi4uXFxuU29tZSBvYmplY3RzIGNvbnRhaW4gcmVmZXJlbmNlcyB0byBvdGhlciBvYmplY3RzOyB0aGVzZSBhcmUgY2FsbGVkIGNvbnRhaW5lcnMuIEV4YW1wbGVzIG9mIGNvbnRhaW5lcnMgYXJlIHR1cGxlcywgbGlzdHMgYW5kIGRpY3Rpb25hcmllcy4gVGhlIHJlZmVyZW5jZXMgYXJlIHBhcnQgb2YgYSBjb250YWluZXJcXHUyMDE5cyB2YWx1ZS4gSW4gbW9zdCBjYXNlcywgd2hlbiB3ZSB0YWxrIGFib3V0IHRoZSB2YWx1ZSBvZiBhIGNvbnRhaW5lciwgd2UgaW1wbHkgdGhlIHZhbHVlcywgbm90IHRoZSBpZGVudGl0aWVzIG9mIHRoZSBjb250YWluZWQgb2JqZWN0czsgaG93ZXZlciwgd2hlbiB3ZSB0YWxrIGFib3V0IHRoZSBtdXRhYmlsaXR5IG9mIGEgY29udGFpbmVyLCBvbmx5IHRoZSBpZGVudGl0aWVzIG9mIHRoZSBpbW1lZGlhdGVseSBjb250YWluZWQgb2JqZWN0cyBhcmUgaW1wbGllZC4gU28sIGlmIGFuIGltbXV0YWJsZSBjb250YWluZXIgKGxpa2UgYSB0dXBsZSkgY29udGFpbnMgYSByZWZlcmVuY2UgdG8gYSBtdXRhYmxlIG9iamVjdCwgaXRzIHZhbHVlIGNoYW5nZXMgaWYgdGhhdCBtdXRhYmxlIG9iamVjdCBpcyBjaGFuZ2VkLlxcbi4uLlxcbiMjIyAzLjIuNS4gU2VxdWVuY2VzXFx1MDBiNlxcbi4uLlxcbiMjIyMgMy4yLjUuMS4gSW1tdXRhYmxlIHNlcXVlbmNlc1xcdTAwYjZcXG5cXG5BbiBvYmplY3Qgb2YgYW4gaW1tdXRhYmxlIHNlcXVlbmNlIHR5cGUgY2Fubm90IGNoYW5nZSBvbmNlIGl0IGlzIGNyZWF0ZWQuIChJZiB0aGUgb2JqZWN0IGNvbnRhaW5zIHJlZmVyZW5jZXMgdG8gb3RoZXIgb2JqZWN0cywgdGhlc2Ugb3RoZXIgb2JqZWN0cyBtYXkgYmUgbXV0YWJsZSBhbmQgbWF5IGJlIGNoYW5nZWQ7IGhvd2V2ZXIsIHRoZSBjb2xsZWN0aW9uIG9mIG9iamVjdHMgZGlyZWN0bHkgcmVmZXJlbmNlZCBieSBhbiBpbW11dGFibGUgb2JqZWN0IGNhbm5vdCBjaGFuZ2UuKVxcblxcblRoZSBmb2xsb3dpbmcgdHlwZXMgYXJlIGltbXV0YWJsZSBzZXF1ZW5jZXM6XFxuLi4uXFxuVHVwbGVzXFxuOiBUaGUgaXRlbXMgb2YgYSBgdHVwbGVgIGFyZSBhcmJpdHJhcnkgUHl0aG9uIG9iamVjdHMuIFR1cGxlcyBvZiB0d28gb3IgbW9yZSBpdGVtcyBhcmUgZm9ybWVkIGJ5IGNvbW1hLXNlcGFyYXRlZCBsaXN0cyBvZiBleHByZXNzaW9ucy4gQSB0dXBsZSBvZiBvbmUgaXRlbSAoYSBcXHUyMDE4c2luZ2xldG9uXFx1MjAxOSkgY2FuIGJlIGZvcm1lZCBieSBhZmZpeGluZyBhIGNvbW1hIHRvIGFuIGV4cHJlc3Npb24gKGFuIGV4cHJlc3Npb24gYnkgaXRzZWxmIGRvZXMgbm90IGNyZWF0ZSBhIHR1cGxlLCBzaW5jZSBwYXJlbnRoZXNlcyBtdXN0IGJlIHVzYWJsZSBmb3IgZ3JvdXBpbmcgb2YgZXhwcmVzc2lvbnMpLiBBbiBlbXB0eSB0dXBsZSBjYW4gYmUgZm9ybWVkIGJ5IGFuIGVtcHR5IHBhaXIgb2YgcGFyZW50aGVzZXMuXFxuLi4uXFxuIyMjIyAzLjIuNS4yLiBNdXRhYmxlIHNlcXVlbmNlc1xcdTAwYjZcXG5cXG5NdXRhYmxlIHNlcXVlbmNlcyBjYW4gYmUgY2hhbmdlZCBhZnRlciB0aGV5IGFyZSBjcmVhdGVkLiBUaGUgc3Vic2NyaXB0aW9uIGFuZCBzbGljaW5nIG5vdGF0aW9ucyBjYW4gYmUgdXNlZCBhcyB0aGUgdGFyZ2V0IG9mIGFzc2lnbm1lbnQgYW5kIGBkZWxgIChkZWxldGUpIHN0YXRlbWVudHMuXFxuLi4uXFxuVGhlcmUgYXJlIGN1cnJlbnRseSB0d28gaW50cmluc2ljIG11dGFibGUgc2VxdWVuY2UgdHlwZXM6XFxuXFxuTGlzdHNcXG46IFRoZSBpdGVtcyBvZiBhIGxpc3QgYXJlIGFyYml0cmFyeSBQeXRob24gb2JqZWN0cy4gTGlzdHMgYXJlIGZvcm1lZCBieSBwbGFjaW5nIGEgY29tbWEtc2VwYXJhdGVkIGxpc3Qgb2YgZXhwcmVzc2lvbnMgaW4gc3F1YXJlIGJyYWNrZXRzLiAoTm90ZSB0aGF0IHRoZXJlIGFyZSBubyBzcGVjaWFsIGNhc2VzIG5lZWRlZCB0byBmb3JtIGxpc3RzIG9mIGxlbmd0aCAwIG9yIDEuKVwiXX0sIHtcInRpdGxlXCI6IFwiQnVpbHQtaW4gVHlwZXMgXFx1MjAxNCBQeXRob24gMy4xNC43IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL2J1aWx0aW5zL3N0ZHR5cGVzLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcImNvbGxlY3Rpb24gY2xhc3NlcyBhcmUgbXV0YWJsZS4gVGhlIG1ldGhvZHMgdGhhdCBhZGQsIHN1YnRyYWN0LCBvciByZWFycmFuZ2UgdGhlaXIgLi4uIGluIHBsYWNlLCBhbmQgZG9uXFx1MjAxOXQgcmV0dXJuIGEgLi4uICwgbmV2ZXIgcmV0dXJuIHRoZSBjb2xsZWN0aW9uIGluc3RhbmNlIGl0c2VsZiBidXQgYE5vbmVgLlxcbi4uLlxcbiMjIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgYGxpc3RgLCBgdHVwbGVgLCBgcmFuZ2VgXFx1MDBiNlxcblxcblRoZXJlIGFyZSB0aHJlZSBiYXNpYyBzZXF1ZW5jZSB0eXBlczogbGlzdHMsIHR1cGxlcywgYW5kIHJhbmdlIG9iamVjdHMuIEFkZGl0aW9uYWwgc2VxdWVuY2UgdHlwZXMgdGFpbG9yZWQgZm9yIHByb2Nlc3Npbmcgb2YgYmluYXJ5IGRhdGEgYW5kIHRleHQgc3RyaW5ncyBhcmUgZGVzY3JpYmVkIGluIGRlZGljYXRlZCBzZWN0aW9ucy5cXG4uLi5cXG5UaGUgb3BlcmF0aW9ucyBpbiB0aGUgZm9sbG93aW5nIHRhYmxlIC4uLiBtdXRhYmxlIGFuZCBpbW11dGFibGUuIFRoZSBgY29sbGVjdGlvbnMuIC4uLiBpcyBwcm92aWRlZCB0byBtYWtlIC4uLiBlYXNpZXIgdG8gY29ycmVjdGx5IGltcGxlbWVudCB0aGVzZSBvcGVyYXRpb25zIG9uIGN1c3RvbSBzZXF1ZW5jZSB0eXBlc1xcbi4uLlxcbiMjIyBJbW11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxubXV0YWJsZSBzZXF1ZW5jZSB0eXBlcyBpc1xcbi4uLlxcbnN1cHBvcnQgYWxsb3dzIGltbXV0YWJsZSBzZXF1ZW5jZXMsIC4uLiAsIHRvIGJlIHVzZWQgYXMgYGRpY3RgIGtleXMgYW5kIHN0b3JlZCBpbiAuLi4gZW5zZXRgIGluc3RhbmNlc1xcbi4uLlxcbiMjIyBNdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbiMjIyBMaXN0c1xcdTAwYjZcXG5cXG5MaXN0cyBhcmUgbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhvbW9nZW5lb3VzIGl0ZW1zICh3aGVyZSB0aGUgcHJlY2lzZSBkZWdyZWUgb2Ygc2ltaWxhcml0eSB3aWxsIHZhcnkgYnkgYXBwbGljYXRpb24pLlxcbi4uLlxcbiMjIyBUdXBsZXNcXHUwMGI2XFxuXFxuVHVwbGVzIGFyZSBpbW11dGFibGUgc2VxdWVuY2VzLCB0eXBpY2FsbHkgdXNlZCB0byBzdG9yZSBjb2xsZWN0aW9ucyBvZiBoZXRlcm9nZW5lb3VzIGRhdGEgKHN1Y2ggYXMgdGhlIDItdHVwbGVzIHByb2R1Y2VkIGJ5IHRoZSBgZW51bWVyYXRlKClgIGJ1aWx0LWluKS4gVHVwbGVzIGFyZSBhbHNvIHVzZWQgZm9yIGNhc2VzIHdoZXJlIGFuIGltbXV0YWJsZSBzZXF1ZW5jZSBvZiBob21vZ2VuZW91cyBkYXRhIGlzIG5lZWRlZCAoc3VjaCBhcyBhbGxvd2luZyBzdG9yYWdlIGluIGEgYHNldGAgb3IgYGRpY3RgIGluc3RhbmNlKS5cXG4uLi5cXG50dXBsZShpdGVyYWJsZT1cXG4uLi5cXG5UaGUgY29uc3RydWN0b3IgYnVpbGRzIGEgdHVwbGUgd2hvc2UgaXRlbXMgYXJlIHRoZSBzYW1lIGFuZCBpbiB0aGUgc2FtZSBvcmRlciBhcyBpdGVyYWJsZVxcdTIwMTlzIGl0ZW1zLiBpdGVyYWJsZSBtYXkgYmUgZWl0aGVyIGEgc2VxdWVuY2UsIGEgY29udGFpbmVyIHRoYXQgc3VwcG9ydHMgaXRlcmF0aW9uLCBvciBhbiBpdGVyYXRvciBvYmplY3QuIElmIGl0ZXJhYmxlIGlzIGFscmVhZHkgYSB0dXBsZSwgaXQgaXMgcmV0dXJuZWQgdW5jaGFuZ2VkLiBGb3IgZXhhbXBsZSwgYHR1cGxlKCdhYmMnKWAgcmV0dXJucyBgKCdhJywgJ2InLCAnYycpYCBhbmQgYHR1cGxlKCBbMSwgMiwgM10gKWAgcmV0dXJucyBgKDEsIDIsIDMpYC4gSWYgbm8gYXJndW1lbnQgaXMgZ2l2ZW4sIHRoZSBjb25zdHJ1Y3RvciBjcmVhdGVzIGEgbmV3IGVtcHR5IHR1cGxlLCBgKClgLlxcbi4uLlxcblR1cGxlcyBpbXBsZW1lbnQgYWxsIG9mIHRoZSBjb21tb24gc2VxdWVuY2Ugb3BlcmF0aW9ucy5cXG4uLi5cXG5Gb3IgaGV0ZXJvZ2VuZW91cyBjb2xsZWN0aW9ucyBvZiBkYXRhIHdoZXJlIGFjY2VzcyBieSBuYW1lIGlzIGNsZWFyZXIgdGhhbiBhY2Nlc3MgYnkgaW5kZXgsIGBjb2xsZWN0aW9ucy5uYW1lZHR1cGxlKClgIG1heSBiZSBhIG1vcmUgYXBwcm9wcmlhdGUgY2hvaWNlIHRoYW4gYSBzaW1wbGUgdHVwbGUgb2JqZWN0LlwiXX0sIHtcInRpdGxlXCI6IFwiQnVpbHQtaW4gVHlwZXMgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL2xpYnJhcnkvc3RkdHlwZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiU29tZSBjb2xsZWN0aW9uIGNsYXNzZXMgYXJlIG11dGFibGUuIFRoZSBtZXRob2RzIHRoYXQgYWRkLCBzdWJ0cmFjdCwgb3IgcmVhcnJhbmdlIHRoZWlyIC4uLiBpbiBwbGFjZSwgYW5kIGRvblxcdTIwMTl0IHJldHVybiBhIC4uLiAsIG5ldmVyIHJldHVybiB0aGUgY29sbGVjdGlvbiBpbnN0YW5jZSBpdHNlbGYgYnV0IGBOb25lYC5cXG4uLi5cXG4jIyBTZXF1ZW5jZSBUeXBlcyBcXHUyMDE0IGBsaXN0YCwgYHR1cGxlYCwgYHJhbmdlYFxcdTAwYjZcXG5cXG5UaGVyZSBhcmUgdGhyZWUgYmFzaWMgc2VxdWVuY2UgdHlwZXM6IGxpc3RzLCB0dXBsZXMsIGFuZCAuLi4gb2JqZWN0cy4gQWRkaXRpb25hbCBzZXF1ZW5jZSB0eXBlcyB0YWlsb3JlZCBmb3IgcHJvY2Vzc2luZyBvZiBiaW5hcnkgZGF0YSBhbmQgdGV4dCBzdHJpbmdzIGFyZSBkZXNjcmliZWQgaW4gZGVkaWNhdGVkIHNlY3Rpb25zLlxcbi4uLlxcblRoZSBvcGVyYXRpb25zIGluIHRoZSBmb2xsb3dpbmcgdGFibGUgLi4uIGltbXV0YWJsZS4gVGhlIGAgLi4uIGlzIHByb3ZpZGVkIHRvIG1ha2UgLi4uIGVhc2llciB0byBjb3JyZWN0bHkgaW1wbGVtZW50IHRoZXNlIG9wZXJhdGlvbnMgb24gY3VzdG9tIHNlcXVlbmNlIHR5cGVzXFxuLi4uXFxuIyMjIEltbXV0YWJsZSBTZXF1ZW5jZSBUeXBlc1xcdTAwYjZcXG4uLi5cXG5vcGVyYXRpb24gdGhhdCBpbW11dGFibGUgc2VxdWVuY2UgLi4uIGJ5IG11dGFibGUgc2VxdWVuY2UgdHlwZXMgaXMgLi4uIGBoYXNoKClgXFxuLi4uXFxuVGhpcyBzdXBwb3J0IGFsbG93cyBpbW11dGFibGUgc2VxdWVuY2VzLCBzdWNoIGFzIGB0dXBsZWAgaW5zdGFuY2VzLCB0byBiZSB1c2VkIGFzIGBkaWN0YCBrZXlzIGFuZCBzdG9yZWQgaW4gYHNldGAgYW5kIGBmcm96ZW5zZXRgIGluc3RhbmNlcy5cXG4uLi5cXG4jIyMgTXV0YWJsZSBTZXF1ZW5jZSBUeXBlc1xcdTAwYjZcXG4uLi5cXG4jIyMgTGlzdHNcXHUwMGI2XFxuXFxuTGlzdHMgYXJlIG11dGFibGUgc2VxdWVuY2VzLCB0eXBpY2FsbHkgdXNlZCB0byBzdG9yZSBjb2xsZWN0aW9ucyBvZiBob21vZ2VuZW91cyBpdGVtcyAod2hlcmUgdGhlIHByZWNpc2UgZGVncmVlIG9mIHNpbWlsYXJpdHkgd2lsbCB2YXJ5IGJ5IGFwcGxpY2F0aW9uKS5cXG4uLi5cXG4jIyMgVHVwbGVzXFx1MDBiNlxcblxcblR1cGxlcyBhcmUgaW1tdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaGV0ZXJvZ2VuZW91cyBkYXRhIChzdWNoIGFzIHRoZSAyLXR1cGxlcyBwcm9kdWNlZCBieSB0aGUgYGVudW1lcmF0ZSgpYCBidWlsdC1pbikuIFR1cGxlcyBhcmUgYWxzbyB1c2VkIGZvciBjYXNlcyB3aGVyZSBhbiBpbW11dGFibGUgc2VxdWVuY2Ugb2YgaG9tb2dlbmVvdXMgZGF0YSBpcyBuZWVkZWQgKHN1Y2ggYXMgYWxsb3dpbmcgc3RvcmFnZSBpbiBhIGBzZXRgIG9yIGBkaWN0YCBpbnN0YW5jZSkuXFxuLi4uXFxuVGhlIGNvbnN0cnVjdG9yIGJ1aWxkcyBhIHR1cGxlIHdob3NlIGl0ZW1zIGFyZSB0aGUgc2FtZSBhbmQgaW4gdGhlIHNhbWUgb3JkZXIgYXMgaXRlcmFibGVcXHUyMDE5cyBpdGVtcy4gaXRlcmFibGUgbWF5IGJlIGVpdGhlciBhIHNlcXVlbmNlLCBhIGNvbnRhaW5lciB0aGF0IHN1cHBvcnRzIGl0ZXJhdGlvbiwgb3IgYW4gaXRlcmF0b3Igb2JqZWN0LiBJZiBpdGVyYWJsZSBpcyBhbHJlYWR5IGEgdHVwbGUsIGl0IGlzIHJldHVybmVkIHVuY2hhbmdlZC4gRm9yIGV4YW1wbGUsIGB0dXBsZSgnYWJjJylgIHJldHVybnMgYCgnYScsICdiJywgJ2MnKWAgYW5kIGB0dXBsZSggWzEsIDIsIDNdIClgIHJldHVybnMgYCgxLCAyLCAzKWAuIElmIG5vIGFyZ3VtZW50IGlzIGdpdmVuLCB0aGUgY29uc3RydWN0b3IgY3JlYXRlcyBhIG5ldyBlbXB0eSB0dXBsZSwgYCgpYC5cXG4uLi5cXG5UdXBsZXMgaW1wbGVtZW50IGFsbCBvZiB0aGUgY29tbW9uIHNlcXVlbmNlIG9wZXJhdGlvbnMuXFxuLi4uXFxuRm9yIGhldGVyb2dlbmVvdXMgY29sbGVjdGlvbnMgb2YgZGF0YSB3aGVyZSBhY2Nlc3MgYnkgbmFtZSBpcyBjbGVhcmVyIHRoYW4gYWNjZXNzIGJ5IGluZGV4LCBgY29sbGVjdGlvbnMubmFtZWR0dXBsZSgpYCBtYXkgYmUgYSBtb3JlIGFwcHJvcHJpYXRlIGNob2ljZSB0aGFuIGEgc2ltcGxlIHR1cGxlIG9iamVjdC5cIl19LCB7XCJ0aXRsZVwiOiBcIjUuIERhdGEgU3RydWN0dXJlcyBcXHUyMDE0IFB5dGhvbiAzLjEwLjIwIGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zLjEwL3R1dG9yaWFsL2RhdGFzdHJ1Y3R1cmVzLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcIllvdSBtaWdodCBoYXZlIG5vdGljZWQgdGhhdCBtZXRob2RzIGxpa2UgYGluc2VydGAsIGByZW1vdmVgIG9yIGBzb3J0YCB0aGF0IG9ubHkgbW9kaWZ5IHRoZSBsaXN0IGhhdmUgbm8gcmV0dXJuIHZhbHVlIHByaW50ZWQgXFx1MjAxMyB0aGV5IHJldHVybiB0aGUgZGVmYXVsdCBgTm9uZWAuIDEgVGhpcyBpcyBhIGRlc2lnbiBwcmluY2lwbGUgZm9yIGFsbCBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyBpbiBQeXRob24uXFxuLi4uXFxuIyMgNS4zLiBUdXBsZXMgYW5kIFNlcXVlbmNlc1xcdTAwYjZcXG5cXG5XZSBzYXcgdGhhdCBsaXN0cyBhbmQgc3RyaW5ncyBoYXZlIG1hbnkgY29tbW9uIHByb3BlcnRpZXMsIHN1Y2ggYXMgaW5kZXhpbmcgYW5kIHNsaWNpbmcgb3BlcmF0aW9ucy4gVGhleSBhcmUgdHdvIGV4YW1wbGVzIG9mIHNlcXVlbmNlIGRhdGEgdHlwZXMgKHNlZSBTZXF1ZW5jZSBUeXBlcyBcXHUyMDE0IGxpc3QsIHR1cGxlLCByYW5nZSkuIFNpbmNlIFB5dGhvbiBpcyBhbiBldm9sdmluZyBsYW5ndWFnZSwgb3RoZXIgc2VxdWVuY2UgZGF0YSB0eXBlcyBtYXkgYmUgYWRkZWQuIFRoZXJlIGlzIGFsc28gYW5vdGhlciBzdGFuZGFyZCBzZXF1ZW5jZSBkYXRhIHR5cGU6IHRoZSB0dXBsZS5cXG5cXG5BIHR1cGxlIGNvbnNpc3RzIG9mIGEgbnVtYmVyIG9mIHZhbHVlcyBzZXBhcmF0ZWQgYnkgY29tbWFzLCBmb3IgaW5zdGFuY2U6XFxuLi4uXFxuPj4+ICMgVHVwbGVzIGFyZSBpbW11dGFibGU6XFxuLi4uIHRbMF0gPSA4ODg4OFxcblRyYWNlYmFjayAobW9zdCByZWNlbnQgY2FsbCBsYXN0KTpcXG4gIEZpbGUgXFxcIjxzdGRpbj5cXFwiLCBsaW5lIDEsIGluIDxtb2R1bGU+XFxuVHlwZUVycm9yOiAndHVwbGUnIG9iamVjdCBkb2VzIG5vdCBzdXBwb3J0IGl0ZW0gYXNzaWdubWVudFxcbj4+PiAjIGJ1dCB0aGV5IGNhbiBjb250YWluIG11dGFibGUgb2JqZWN0czpcXG4uLi4gdiA9IChbMSwgMiwgM10sIFszLCAyLCAxXSlcXG4+Pj4gdlxcbihbMSwgMiwgM10sIFszLCAyLCAxXSlcXG4uLi5cXG5BcyB5b3Ugc2VlLCBvbiBvdXRwdXQgdHVwbGVzIGFyZSBhbHdheXMgZW5jbG9zZWQgaW4gcGFyZW50aGVzZXMsIHNvIHRoYXQgbmVzdGVkIHR1cGxlcyBhcmUgaW50ZXJwcmV0ZWQgY29ycmVjdGx5OyB0aGV5IG1heSBiZSBpbnB1dCB3aXRoIG9yIHdpdGhvdXQgc3Vycm91bmRpbmcgcGFyZW50aGVzZXMsIGFsdGhvdWdoIG9mdGVuIHBhcmVudGhlc2VzIGFyZSBuZWNlc3NhcnkgYW55d2F5IChpZiB0aGUgdHVwbGUgaXMgcGFydCBvZiBhIGxhcmdlciBleHByZXNzaW9uKS4gSXQgaXMgbm90IHBvc3NpYmxlIHRvIGFzc2lnbiB0byB0aGUgaW5kaXZpZHVhbCBpdGVtcyBvZiBhIHR1cGxlLCBob3dldmVyIGl0IGlzIHBvc3NpYmxlIHRvIGNyZWF0ZSB0dXBsZXMgd2hpY2ggY29udGFpbiBtdXRhYmxlIG9iamVjdHMsIHN1Y2ggYXMgbGlzdHMuXFxuXFxuVGhvdWdoIHR1cGxlcyBtYXkgc2VlbSBzaW1pbGFyIHRvIGxpc3RzLCB0aGV5IGFyZSBvZnRlbiB1c2VkIGluIGRpZmZlcmVudCBzaXR1YXRpb25zIGFuZCBmb3IgZGlmZmVyZW50IHB1cnBvc2VzLiBUdXBsZXMgYXJlIGltbXV0YWJsZSwgYW5kIHVzdWFsbHkgY29udGFpbiBhIGhldGVyb2dlbmVvdXMgc2VxdWVuY2Ugb2YgZWxlbWVudHMgdGhhdCBhcmUgYWNjZXNzZWQgdmlhIHVucGFja2luZyAoc2VlIGxhdGVyIGluIHRoaXMgc2VjdGlvbikgb3IgaW5kZXhpbmcgKG9yIGV2ZW4gYnkgYXR0cmlidXRlIGluIHRoZSBjYXNlIG9mIGBuYW1lZHR1cGxlc2ApLiBMaXN0cyBhcmUgbXV0YWJsZSwgYW5kIHRoZWlyIGVsZW1lbnRzIGFyZSB1c3VhbGx5IGhvbW9nZW5lb3VzIGFuZCBhcmUgYWNjZXNzZWQgYnkgaXRlcmF0aW5nIG92ZXIgdGhlIGxpc3QuXFxuLi4uXFxuIyMgNS40Llxcbi4uLlxcbi4gU2V0IG9iamVjdHNcXG4uLi5cXG4jIyA1LjUuIERpY3Rpb25hcmllc1xcdTAwYjZcXG5cXG5Bbm90aGVyIHVzZWZ1bCBkYXRhIHR5cGUgYnVpbHQgaW50byBQeXRob24gaXMgdGhlIGRpY3Rpb25hcnkgKHNlZSBNYXBwaW5nIFR5cGVzIFxcdTIwMTQgZGljdCkuIERpY3Rpb25hcmllcyBhcmUgc29tZXRpbWVzIGZvdW5kIGluIG90aGVyIGxhbmd1YWdlcyBhcyBcXHUyMDFjYXNzb2NpYXRpdmUgbWVtb3JpZXNcXHUyMDFkIG9yIFxcdTIwMWNhc3NvY2lhdGl2ZSBhcnJheXNcXHUyMDFkLiBVbmxpa2Ugc2VxdWVuY2VzLCB3aGljaCBhcmUgaW5kZXhlZCBieSBhIHJhbmdlIG9mIG51bWJlcnMsIGRpY3Rpb25hcmllcyBhcmUgaW5kZXhlZCBieSBrZXlzLCB3aGljaCBjYW4gYmUgYW55IGltbXV0YWJsZSB0eXBlOyBzdHJpbmdzIGFuZCBudW1iZXJzIGNhbiBhbHdheXMgYmUga2V5cy4gVHVwbGVzIGNhbiBiZSB1c2VkIGFzIGtleXMgaWYgdGhleSBjb250YWluIG9ubHkgc3RyaW5ncywgbnVtYmVycywgb3IgdHVwbGVzOyBpZiBhIHR1cGxlIGNvbnRhaW5zIGFueSBtdXRhYmxlIG9iamVjdCBlaXRoZXIgZGlyZWN0bHkgb3IgaW5kaXJlY3RseSwgaXQgY2Fubm90IGJlIHVzZWQgYXMgYSBrZXkuIFlvdSBjYW5cXHUyMDE5dCB1c2UgbGlzdHMgYXMga2V5cywgc2luY2UgbGlzdHMgY2FuIGJlIG1vZGlmaWVkIGluIHBsYWNlIHVzaW5nIGluZGV4IGFzc2lnbm1lbnRzLCBzbGljZSBhc3NpZ25tZW50cywgb3IgbWV0aG9kcyBsaWtlIGBhcHBlbmQoKWAgYW5kIGBleHRlbmQoKWAuXCJdfV0iLCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ3ZWJfc2VhcmNoIiwiaWQiOiI2ZTUxMjVhYy0zMTY0LTRhNzYtOWM1Mi1hYzM0NjBjZTM2ZTQiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJzdGF0dXMiOiJzdWNjZXNzIn19LHsibGMiOjEsInR5cGUiOiJjb25zdHJ1Y3RvciIsImlkIjpbImxhbmdjaGFpbiIsInNjaGVtYSIsIm1lc3NhZ2VzIiwiQUlNZXNzYWdlIl0sImt3YXJncyI6eyJjb250ZW50IjpbeyJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3NrZXB0aWMiLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2Zkd0E2RjRjQ041eWhENENyczlNIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjczNzgsIm91dHB1dF90b2tlbnMiOjMzLCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjowfSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCIsIm1vZGVsX3Byb3ZpZGVyIjoiYW50aHJvcGljIn0sInR5cGUiOiJhaSIsIm5hbWUiOiJyZXNlYXJjaGVyIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTNjZDgtNzlmMS04OGExLWNlNmNlZTI5NGU3OS0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6InRyYW5zZmVyX3RvX3NrZXB0aWMiLCJhcmdzIjp7fSwiaWQiOiJ0b29sdV8wMVhqajNhUmRXeWhCSmZQbVRKQnhRYTkiLCJ0eXBlIjoidG9vbF9jYWxsIn1dLCJ1c2FnZV9tZXRhZGF0YSI6eyJpbnB1dF90b2tlbnMiOjczNzgsIm91dHB1dF90b2tlbnMiOjMzLCJ0b3RhbF90b2tlbnMiOjc0MTEsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjB9fSwiaW52YWxpZF90b29sX2NhbGxzIjpbXX19LHsibGMiOjEsInR5cGUiOiJjb25zdHJ1Y3RvciIsImlkIjpbImxhbmdjaGFpbiIsInNjaGVtYSIsIm1lc3NhZ2VzIiwiVG9vbE1lc3NhZ2UiXSwia3dhcmdzIjp7ImNvbnRlbnQiOiJTdWNjZXNzZnVsbHkgdHJhbnNmZXJyZWQgdG8gc2tlcHRpYyIsInR5cGUiOiJ0b29sIiwibmFtZSI6InRyYW5zZmVyX3RvX3NrZXB0aWMiLCJpZCI6IjM4OTA1OGQxLTA4M2YtNDYzMi1iYmM1LTA5YmZkM2EwZmZkMiIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsInN0YXR1cyI6InN1Y2Nlc3MifX0seyJsYyI6MSwidHlwZSI6ImNvbnN0cnVjdG9yIiwiaWQiOlsibGFuZ2NoYWluIiwic2NoZW1hIiwibWVzc2FnZXMiLCJBSU1lc3NhZ2UiXSwia3dhcmdzIjp7ImNvbnRlbnQiOlt7InNpZ25hdHVyZSI6IkNBUVMxUWdLRUFnU0dBSTRBVUlJZEdocGJtdHBibWNTRE42VGZVNk5ETTk3MWxFYnp4b012UlVBMUo5b3gzZkpRWVQ5SWpBckNRUUdIQlA0cFJIUGF2WHpSbGJNYmFZSEM1N1lqK0dieGdyMGhlbHZkMk9JTC9vNlJidDRQRkEzRmFJRlVsZ3E4Z2M3S1FJOUZ0R252U0FrTEJSeklzVXAvMmQ1UmQyemdFc2ZrRlZlWHNuOFdZWVpraW5BUVZWeTZBN0IzTGFFSlcweGIveDg4RGk2N2ZlZHd4MFErRmxadHdjbVQzRGNhYTExSnZleUhScjNkbSt6R1NaUDZDWGVYTko4bnJUWjdXMVlSVHlzRDZ3N283THA4aDFSd3E0ZjRvOGorUVVmWTB2amUrekpac3hIUG1RTnQzeWtlS2g5OWhnbjZkYml6Ni9FSHFtYkhqU0F1MU00U3FFRGZxSUtUaEhZZW5hbUFabWNRNFhBT282bWRRNis3N1ZncFYxbEMxL2s1MklpVW9KamtHbFFybkxDdnV0aUk3S04rc2dLOGMyKzJLQ0pyUFJDdlZWK0JrU3R2UDVqV1dxenl0cEtRK25IemhMNDQ4T3JJQTlnZEpDMzlDOGVEclhlUm5JN3Q0RnFsa1hHb01sWkFWOVVNY3JRSFJ4T01pOHRPeVR6aTlveUJGRFRTc1ErTEpxZXIwdHoxRnQ5Z2VrNnZ6ZzUwU3BWQUZRZExFUnY4UjFZcU1hZUkreHN2VEFFVzlNUmd2Uk1rekx1elJUMmd1dzNqRCtGamlwMzd4Z0s1UUkwUitDc09BbVFQb1RaaXp6Y0l5QVVBdUNKRkNncS9nRC9DaWJMeG95bWlWVUkxYW45U250M0twTlJvV2R2V0luZTMzVjBwdjhQSGc5eVcySnVMT3FRR2lsTENzSy9RM29RcjN1cEVNWHhYR2pDTDdXVWl6dlJaR0ZFRVYrVnBsWXQ3djR0S1pNajVwazN2ZGVNME1KNVhaZzdQdlVzUnp5Y3hjbnJUM0tub2MzcU0rdHRxWmZUc2I4ZDZVL3BRaWZxN0d1YWhGT0hydnJvcWdKTGorLzgxWXkxcFpGa2pmSmdZMFhrbHpBbkE2Q0ZxL1lUMGd4YlYrVEg1SVMrV2FZT1V6SzM2UzhzNXFxTXdobmd2NEl6RTNjdFhYcW5oSVpYTnpueVZCcWJZYS9HeDJiQ2pjRWoybjVSeUdZUlMvK01jdWxnTEJ6blE5TWtHMkQ3UVAzTXEvbndIWEtNSWFMUzhWSzhoRzNkMHJBamx2bmF1WFNoTnJKeHRrajRxa3dueEQ0THJFamsxdWUrUExjYkxxWVd3QXI4bjBzKzQxanJzSVBWeXlLOXZGYUVackE5S2FCeGtKeWlxeDUwR1pObjdja3grbWtJMThnSDUyaXlWUHRVOEZLTVhqdWdDbDZtUExpSFlpRFFkK0c2M0x1ZG1PS3lzOWl0Vm5CREJOTVAyYi9JNytiSVZSbGhlR2FaV3I2TUFUS2w4NkIzeSswa3VPckxSTXlXY2RpYmMyaVFrWm0rOGY3ZFRVd2JLYkJKc0tPRlk3My84bmxML0ppWXBqejZuVUlrK0lxU2phRnlLNlhGbENjdWtXcFdIcERKblN2YkV6ZXk3cUh5Q3FKK0krb244aVA4R090ZW1wUWFTMC80a1pHU3dsckU2eE9qNkV3R3BmamJEWktIa1AvMlJBZE5ZUFFxM3o3YmRWSk1KTm5GTXo2ZTM3bXpNcENCaWh3UGRTRlRSRTNTQ1k2Nnd2VGdKekdDVXloSGJTaTVpMWF6ek9YZDh1SENTcFQvNkwxbzZWVTBwam9SOEthYTR3Q1dIR2Y3UC9XRWQrYXZ5cDAySm51MWNtcFcwK0dxK3ZLOWgwdGRiMklDbE0vZU5mZG91UVArZXA4TUpVanJhRFJJQ21BSlFZNW9iUzcxdm5pT28rcmVhRnkwelJnQiIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJzaWduYXR1cmUiOiJDQVFTL1JFS0VRZ1NHQUk0QVVJSmJtRnljbUYwYVc5dUVnd0ZoM0FWSlpQM3M4eTFIcTBhREc3NnVoSE9OYVNjWEFHU2hpSXdHZ2pMWEhVeDF5WnJrTFBsamp6VE9ReWNwTEppS1Z3WGN4bS9FektKbTFzOE01V3h1UkVXRWxkWGNvYU4wL21zS3BrUk1UK2VMYjVpaDlyQUhVTFRvbVVOc1V3SmV1MGpySmU0R0xhenk3c1A1S3RqRU9FQktRc1hVZm9RN2ZEa1pPUVVIWmk5enpRTjRoRk5tWUpVZjNPVnFmOVRRTmRDMSttLy9IQVpQZldtQXk2SHREdm03a2dCSmtDRW9pRm5tN3RuL1BYMWhYR1BSZ1djNkJUWFVNcjJXQThhS3lxZ2dSVWJNSDIrSll2NWlXWmRkQ2MxdXAxTmJyM3NGN1dQY290OVpCaHlIUVhyNjkyemVKazZTSmd5Z0lWckFTYk1seTJiQnhUV0pCNjIzZ1ZnenBDbVZTTFpGcGtLWGdQY3A4aEd5REtPdkY2U2lRU0hxR1J4UW9mcjdWczFhUGhmWml4Uk9KR0QrTWxya2dnVkFuR015WUlEVVpwYXZ0Q2VpVTFKbUJTejAwODRiWFB0VC90bnBzS09uU1BZWlFvM0IwQlhLYnFBL1dzS25nOC9WM1o4VXVham82SVZaMmhnQTFXRGR1cVFPUWtFeEdVNnBGdUxoZWJJanVXeFNOSS9EZmZ2Ui8vTE5xQ2Z2ZG5mMGh2b1B0RHhmeC9TYXdJeDQvc3BxOXhid0Z6SHRMZ29IZzFGMndGdzdjbFBEbGcxUHlEL3YyeUpHM1Y5VDhHTDZqR1BFNnJoZmlsL3U0SE52ZjdCVzBkUXpDVFRYY3FaeTRuQnl5MCtwK1M3WnhmTWw0S3B4YTJnT2hoU05qZnBmdVgwUXBMNGtNRThKMW5saUZOMWQ4S3FFUHVESTE4QlFIVWhEL1VmWTI5b0EweDUxMEZqTS9Cc0xaNG41c3Bwa29JaTdDRm9RcTJhQUtWTmZDMUtpbGhJcFJEKzFkeUR0WlFrbE44Z1pwT0VTSHc2TUwxVHRVMHlmQWNibXFveEo2aGhqendINkNNc1RRRUV5OE1iUkJNK1FLS2tJUmRSQ1BIR0ZXMkxrdVI1MCtKMHBGL3RyNnBxVnZKZW96ZmlNbnJFaWxCMjdUWlA0bktUSkc4cmQvL3ZDV2VuMHk3Q0EvOGRCSjlnZHA1dHg3TlZBNDVrMWdkY3hFUjFnWW91TlgzbjF2RzMvQjFGNCs5M3dYVzlNcW82VXM1OVRRbXJiOU5xYTdwU1FOU3JIMDZ3MUd4eUJBbGtGZnFBbGFmazE2YU1wcTQxNlRKWXBHeTQzV0NVaUNjTFdsaGlXZ3oyc2dPTEFYUDlhNXdlV0FLUkM4SjZqa2NVa0RlWG5XK0hGU2lQTTYrOUJvNmtWaExVaE41cmM0bUhaT1pwRjdwdFBwc1FONFhDcUxQc3FwUkgvOG81WHB5QzZabmVUZUYyRXh0amh3WlZqWER3TWJJelZZSWJ3MXkyUFBHdzhkQzdwYzRzSVZTbkk0b1FBSDB2NjUwZ2hmaG1NZXZXVzRoNFB5bGJ3L0RHNGZkZVQzKzh6MGY4TU9TVHRmaXB4MDUvUXR0SGxBdk9EUnhhNXlEQ3ZPejZuM2YxQk8wTi80TWJHeU9NMjdxVzR4OG1CVm9yeVVaV2ZzbWhmaTAvR2ZOdkI4K3lsVEdiWU5YMjdPY0VGNVdadE5xYldFeTdWNUdibVpVU3hzcGNnN0ZCWjNqMy8zTDdtdXZaWG9aaTFmNWVycTR5M2xTSCtvRjdxUEZudmlleUxPQ3d1YmNReStveW93ajRjRDBpZFJlY25wK3MrNTJENyttekYxZ3BXK2YrY0phc294aWdieGtPbG9TNHllaUt2Z000bldTcEdFajEyUlB4NW84MEVtemlYTjl0NHpKelcxaHRBNWhYSkplRnltcE1ZdHlDVUNldlJXVm1yUllrdFE3VW5DYTZ0TC9BVlBpMW5pNVhDWEhmTE91cWZLNUtMWXhqd1Z0ZGhoZm1DTHZwYUtWTkNFT0NmNG5OR0hxRDk4d1ViMGVqV053RFlCbkpHa0JSdzlSVnNwZndOZG9Xb1ZjbjJxdjh5d0dVcDd5Yy9zazJMcEk2VnppUnl2V3FiR05HMVpKcVZWdXRSYzQ2K3NPRTRWU2VTMi9DVE5LTmE2Tk1DZi9oeHhhVVZzWWZLZFArUzFMYzRpYW5LcDBzVXVnUHBtdG95ZEMwQ1AyUndqbkhMY256eENudnF4bExRV1hNTlBUc3UyeUJjSGxXSkx0Y21kZy9NY0xhZ3k3bDVrRzZzTjdaeEpMRC9rQXg4RTZ1cW9oUCtLL2pNU3JZdVFKbmhyNHRmNGJpV0ZZM0p6Zi9LcEFZeVZTcStwYmZGS1hNYkw5bkhFb1pRVDE1V3lhYmQ2RE1BcTNRWk90aE01cFV6ZE5xVVhrY3JJMVB3akFTQWc0ZFZVLzJvKzk4bVRYbnloUDdWbEVrVG5kZHB3eUpGK1lDQ2MwMXBxYTZaWDk5cG5SK1lPcElyYjVEdFBjS3EwdDVjRkNEeVF4TjFsVXVxWmNZdWp2WC9HQzcrNklwUlR6L05rRVkvNWNudE9mWkp5ZDZTa1V4L0pSNUFKSVJQQ05laWIwcmNXbm5GbjBYSzhzMjJ4NDB3TGpFdHVmWERPOSt5cmFmNGtIeGxCSTNTQjBtZU1qZXBYNTBDSmhBeDdOZWtrUkZud29lY0xmcUl5cmpYcmdBWnNUTVBpWDk0c25qZHJ6aXNBWHRCS2FudWxpbFN4T2ZXMkhTbWZuazBMMkdYSEpKM1MrdkNiZ1JFbWw5ZGVrZm1IaG13RVl0djNrZmh0aDdFUkl3VlQ4ajY5T3RJQkNtZVRJSnVBN283M1BYYzFMSjJ3aHVaSzdGWmU2TlRPczYxTVA3SlZsSWM2dGJodE9CazNOWFU4cEJQT0NZNlE1NWQ2M0Z0cFVVL0tDYlEwSzJXUTlvNWpOR3dOTko2Q3c1RkRDdCt4NVlKZVAxZDJRekRTaXErY1NacXdxQ1pWY05vQWZ0R0d6Z3JSWjR2a3BiUDB4UVpIQ3RXc1V3SFZUanJZNE5mUkRqSGRWUFJuOEo5WHRvb2MvaElXa3F6RmFjellUbGFwdTJSQ0pxZThhRW8zanpXWkNFSjdBaUJ6QllyYVlEZ09mZWlReWVKVXNiYlhIZEdLaWl3MjJCZHA0TStZejY3MU1aMGllOUVXUmtzMFEyREhHVVRuZTlTNHRBQUpQR2JVZjR4dENNb0o0MFE0UVcwNVFMU1p5M3dORkFVRlltWEU0RUNERkI4K0lFZU5GRkYvdGdBbmZoOEd2RmgvMXhZK0xFK296TW1pbmpTUndocTA5elFBRWtORnl1MElLZ3ZEa3lPSStpY2NGN2RvV0xUVWFzUHhMSDF4R1BCU0trZVdDblNvWC80QzJjOWR1NjZCYlJYYW1IcWFlK1ZDaVM2Qk45NTNpWUtPdThyTWtuZVcrVEZqVjBDekFmOFBORzlMQmF5VWdZNVV3OXgwU2FwNnpJNFBwYVpLSDRkOWt0a0ZVKzkxeUJiTjVTUnRkZDgvRVFhdEpYbUJOZnc3a1JybGwrTGp3WndlV0xFYjBqVDFScEd1RHFITGRMVE1NZ293U2N6cnAyekNzSmRRSC9zZWFZcnB3a0NGUjhkUmpNN0pSVWtWYTFBMFN2ZFpBWDc1dnVCdnZMQnpOMmMrUmJ0NW9xeTkzbHFJQklqS2I3dzJVa1JUYjRwQldjTktOUFVndDNhei9QeFF4RWFzYWx0ZG11eXZzWFlidXIzVlBObjhKek4vNko1TitRQ256NTcxVGNjYXBDbWtQdkZFK2dheVhVK1VGMVZLWXlGY1V2bGV2N2xzUWxtanF6SzJRQThYN1EzVHpGYXBxaHZKS2x0N2R6QkhyYXFaQmdQZ2I5TjRvS05heWk4RlpUNzEra2FZaEloRnJFU0tZUkhmSGtqNW4za2ZoZ0x5K3grakxEYkt4TWdna2dIbkp6OVJ1RUhaWEJXdGtZQVE9PSIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJpZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3ZlcmlmaWVyIiwidHlwZSI6InRvb2xfdXNlIiwidG9vbHNldF9uYW1lIjpudWxsfV0sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHdBTVJkUzIxRmVKZnpRaE0zcyIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo3Mzk2LCJvdXRwdXRfdG9rZW5zIjo3NzAsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjczOX0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoic2tlcHRpYyIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy00YWU5LTc2YzAtODlhNi00ZjBiMTE0ZTMwMzAtMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsInR5cGUiOiJ0b29sX2NhbGwifV0sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzM5Niwib3V0cHV0X3Rva2VucyI6NzcwLCJ0b3RhbF90b2tlbnMiOjgxNjYsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjczOX19LCJpbnZhbGlkX3Rvb2xfY2FsbHMiOltdfX0seyJsYyI6MSwidHlwZSI6ImNvbnN0cnVjdG9yIiwiaWQiOlsibGFuZ2NoYWluIiwic2NoZW1hIiwibWVzc2FnZXMiLCJUb29sTWVzc2FnZSJdLCJrd2FyZ3MiOnsiY29udGVudCI6IlN1Y2Nlc3NmdWxseSB0cmFuc2ZlcnJlZCB0byB2ZXJpZmllciIsInR5cGUiOiJ0b29sIiwibmFtZSI6InRyYW5zZmVyX3RvX3ZlcmlmaWVyIiwiaWQiOiJjMDY0NTYzYS05NzRmLTRmMGYtYWI0YS0wODBlNjNjMDFjZjUiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMUg2ZlY4SnZSSk52WHk0N2VSbmQzakgiLCJzdGF0dXMiOiJzdWNjZXNzIn19XV19" + } + }, + { + "key": "gen_ai.usage.input_tokens", + "value": { + "intValue": "7509" + } + }, + { + "key": "gen_ai.usage.output_tokens", + "value": { + "intValue": "88" + } + }, + { + "key": "gen_ai.usage.total_tokens", + "value": { + "intValue": "7597" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJnZW5lcmF0aW9ucyI6W1t7InRleHQiOiIiLCJnZW5lcmF0aW9uX2luZm8iOm51bGwsInR5cGUiOiJDaGF0R2VuZXJhdGlvbiIsIm1lc3NhZ2UiOnsibGMiOjEsInR5cGUiOiJjb25zdHJ1Y3RvciIsImlkIjpbImxhbmdjaGFpbiIsInNjaGVtYSIsIm1lc3NhZ2VzIiwiQUlNZXNzYWdlIl0sImt3YXJncyI6eyJjb250ZW50IjpbeyJzaWduYXR1cmUiOiJDQVFTOWdVS0VBZ1NHQUk0QVVJSWRHaHBibXRwYm1jU0RHRnlKMU1Yb1krQWk2TmtieG9NUmQyVHJVZlVVN1dyUHBBMklqRGd5OGRVcGxjWktjN0RuSndOTFRCZkg1VU5SK25HbmFrU1BRaWE4VVZJenJqa09uSjJmTHJXVXNBMm9FTzh5UnNxa3dYa2tEQ3N5aEI5N2R3ajU3Uk10OTFYemdXaEw5bHVORUdPOTJ5VzByTkZHb01TVVZPeFBJckZjNVJqeXJoS0VjMysxUm5weENreWxxK0xObFE1TkZ4TG42djJjRjVEenFwdkJITlI3WEFpZyt1ckpJVTNnWHlkQ2thR1paRGV0SnRRT1VPWGdaaTl6OTEwME5wZEFFa1hEbzJJQ0hDR1RHcFZjMEovcGJYeFdSQnhiNkIxL0dsMFlqbjY4VjZIbGVzSDdJb0M3R1IvM0h3UnU5TlNEQkc0dHRmUWpFV2NnK1BlQWVhNFJuN2p6eCtUbTlIN0wxam9US1ZnRGJRZzVpS01MZmp2UHFsbmtzQ0x0WE90OGpTSVM1T1lMTjdhLzdUWDYvdzVkMzR2bnc4dlBEbVUxVVlpMFE3MnFVV2hVYTJ2dzhPWE5wY0tMN0JwMGxOZkJTOHJ2SFkrdkJZOC9oYUFvVEwzd3ZMRlUxZlQrNHovdkEzQmNzaWdaRFlsZkplRFljeFhtUnVxeHp4RUdwWENSZTlzMUhXdElEKzQxcEh1NmxwWkhrWGFnaHBjNlRaSEx4TndjVmhUYjZmTVE3UW8rMkw2UHZrbGgxbTMycGVvZHhlZzJZaVBoZHBsM3cveGtPRXU1L0pFRjdaQnhwZFVMK3Uzd2tPNXBVbHNxZmdYN3VoenY3SEMyV3BTV1ZpbXJ3aVF3UU9VeDArSlByTHR3K2xZZnE3STNqcnVvMEdXMGd1V3ZudWY3Sk0zWUxqODRxK0pnSG9tblNTWm5uWm1yTDBVYVJ2clZ5TG84NzdGMzljY0xTN1hvWm5udWk4SjRQNThaVG1pTGtFSmVHY3gxSXVhZzJlMzMzMzlPN1lldy81OXRMYlF0SUdoOVZXTXdIQm5GeDM5UzgvNExFRnJLNUhsZFBRaE9ybG95VHRrUnM2NmlRUkt0eE1LUzBWSTZRVEtaREFiczdhN0gxeUJ2K21KaTlYZzR1S215VTZxbm43ekVLampZMmJJbEN3ZXp4NExMR1A1SEQzWnI2ZlpzbWRHMVVIekRzQnZZRzJvQ0kwb3FFS00rdmFsc1Q2aFU4TVJqYlFCRC9KSjhKSk4vaUJHbGhCQVVIbzBVbWhodFIyZXVUVnBaV2xNbmhtcFhBZEtEWFNmUGRhUG9oZ0IiLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsiaWQiOiJ0b29sdV8wMVhFQXVoUUhDQmR5Q3d0eW1ad0ZmRGkiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6e30sIm5hbWUiOiJ0cmFuc2Zlcl90b19yZWRfdGVhbSIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QXNhYkRxdWVnSGk2WVVwQ24iLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzUwOSwib3V0cHV0X3Rva2VucyI6ODgsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjU1fSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCIsIm1vZGVsX3Byb3ZpZGVyIjoiYW50aHJvcGljIn0sInR5cGUiOiJhaSIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy02NjgyLTdkMjEtOGMxNS1mZmJjZTk0MjJjOWYtMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ0cmFuc2Zlcl90b19yZWRfdGVhbSIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxWEVBdWhRSENCZHlDd3R5bVp3RmZEaSIsInR5cGUiOiJ0b29sX2NhbGwifV0sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzUwOSwib3V0cHV0X3Rva2VucyI6ODgsInRvdGFsX3Rva2VucyI6NzU5NywiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6NTV9fSwiaW52YWxpZF90b29sX2NhbGxzIjpbXX19fV1dLCJsbG1fb3V0cHV0Ijp7ImlkIjoibXNnXzAxMUNmZHdBc2FiRHF1ZWdIaTZZVXBDbiIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo3NTA5LCJvdXRwdXRfdG9rZW5zIjo4OCwib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6NTV9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0In0sInJ1biI6bnVsbCwidHlwZSI6IkxMTVJlc3VsdCJ9" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "4862a8dc9e239ae2", + "parentSpanId": "0b1303d7a7aa229b", + "name": "verifier", + "kind": 1, + "startTimeUnixNano": "1790968030846055936", + "endTimeUnixNano": "1790968032478449152", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "e30=" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "verifier" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "3" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "verifier" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:verifier\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"verifier\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "verifier:544dea40-e16b-458c-1464-128e1b9b10a9" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "verifier" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "verifier:544dea40-e16b-458c-1464-128e1b9b10a9" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "seq:step:1" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W3siY29udGVudCI6IkZvciBhIHN5bnRoZXRpYyB0cmFjaW5nIGZpeHR1cmUsIGV4cGxhaW4gUHl0aG9uIHR1cGxlIHZlcnN1cyBsaXN0IGltbXV0YWJpbGl0eSBpbiBhdCBtb3N0IDEyMCB3b3Jkcy4gVXNlIG9ubHkgb2ZmaWNpYWwgUHl0aG9uIGRvY3VtZW50YXRpb24gaWYgeW91IHNlYXJjaC4gSGF2ZSB0aGUgc2tlcHRpYyBhbmQgdmVyaWZpZXIgY2hlY2sgaXQsIHRoZW4gaGF2ZSB0aGUgZWRpdG9yIHdyaXRlIC9hbnN3ZXIubWQuIEtlZXAgdGhlIHJ1biBzaG9ydCBhbmQgZG8gbm90IGFzayBmb2xsb3ctdXAgcXVlc3Rpb25zLiIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoiaHVtYW4iLCJpZCI6IjQ2MjA0MzA1LWY4ZWUtNDM4NC05Y2U4LWZjZDgwYmZjZGY3NSJ9LHsiY29udGVudCI6W3siaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9LCJuYW1lIjoid2ViX3NlYXJjaCIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHc5YVc5YnJlblBxZ3liZk53UCIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjoyODQ1LCJvdXRwdXRfdG9rZW5zIjo3Mywib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6MH0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoicmVzZWFyY2hlciIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy0yMWEzLTcwMDAtYTM4MS0yODRhNmEwYzAzZjctMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ3ZWJfc2VhcmNoIiwiYXJncyI6eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9LCJpZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6Mjg0NSwib3V0cHV0X3Rva2VucyI6NzMsInRvdGFsX3Rva2VucyI6MjkxOCwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fSx7ImNvbnRlbnQiOiJbe1widGl0bGVcIjogXCI1LiBEYXRhIFN0cnVjdHVyZXMgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3R1dG9yaWFsL2RhdGFzdHJ1Y3R1cmVzLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcIllvdSBtaWdodCBoYXZlIG5vdGljZWQgdGhhdCBtZXRob2RzIGxpa2VgaW5zZXJ0YCxgcmVtb3ZlYCBvcmBzb3J0YCB0aGF0IG9ubHkgbW9kaWZ5IHRoZSBsaXN0IGhhdmUgbm8gcmV0dXJuIHZhbHVlIHByaW50ZWQgXFx1MjAxMyB0aGV5IHJldHVybiB0aGUgZGVmYXVsdGBOb25lYC4gWzFdIFRoaXMgaXMgYSBkZXNpZ24gcHJpbmNpcGxlIGZvciBhbGwgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgaW4gUHl0aG9uLlxcbi4uLlxcbiMjIDUuMy4gVHVwbGVzIGFuZCBTZXF1ZW5jZXNcXHUwMGI2XFxuXFxuV2Ugc2F3IHRoYXQgbGlzdHMgYW5kIHN0cmluZ3MgaGF2ZSBtYW55IGNvbW1vbiBwcm9wZXJ0aWVzLCBzdWNoIGFzIGluZGV4aW5nIGFuZCBzbGljaW5nIG9wZXJhdGlvbnMuIFRoZXkgYXJlIHR3byBleGFtcGxlcyBvZiBzZXF1ZW5jZSBkYXRhIHR5cGVzIChzZWUgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBsaXN0LCB0dXBsZSwgcmFuZ2UpLiBTaW5jZSBQeXRob24gaXMgYW4gZXZvbHZpbmcgbGFuZ3VhZ2UsIG90aGVyIHNlcXVlbmNlIGRhdGEgdHlwZXMgbWF5IGJlIGFkZGVkLiBUaGVyZSBpcyBhbHNvIGFub3RoZXIgc3RhbmRhcmQgc2VxdWVuY2UgZGF0YSB0eXBlOiB0aGUgdHVwbGUuXFxuXFxuQSB0dXBsZSBjb25zaXN0cyBvZiBhIG51bWJlciBvZiB2YWx1ZXMgc2VwYXJhdGVkIGJ5IGNvbW1hcywgZm9yIGluc3RhbmNlOlxcbi4uLlxcbj4+PiAjIFR1cGxlcyBhcmUgaW1tdXRhYmxlOlxcbj4+PiB0WzBdID0gODg4ODhcXG5UcmFjZWJhY2sgKG1vc3QgcmVjZW50IGNhbGwgbGFzdCk6XFxuICBGaWxlIFxcXCI8c3RkaW4+XFxcIiwgbGluZSAxLCBpbiA8bW9kdWxlPlxcblR5cGVFcnJvcjogJ3R1cGxlJyBvYmplY3QgZG9lcyBub3Qgc3VwcG9ydCBpdGVtIGFzc2lnbm1lbnRcXG4+Pj4gIyBidXQgdGhleSBjYW4gY29udGFpbiBtdXRhYmxlIG9iamVjdHM6XFxuPj4+IHYgPSAoWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuPj4+IHZcXG4oWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuLi4uXFxuQXMgeW91IHNlZSwgb24gb3V0cHV0IHR1cGxlcyBhcmUgYWx3YXlzIGVuY2xvc2VkIGluIHBhcmVudGhlc2VzLCBzbyB0aGF0IG5lc3RlZCB0dXBsZXMgYXJlIGludGVycHJldGVkIGNvcnJlY3RseTsgdGhleSBtYXkgYmUgaW5wdXQgd2l0aCBvciB3aXRob3V0IHN1cnJvdW5kaW5nIHBhcmVudGhlc2VzLCBhbHRob3VnaCBvZnRlbiBwYXJlbnRoZXNlcyBhcmUgbmVjZXNzYXJ5IGFueXdheSAoaWYgdGhlIHR1cGxlIGlzIHBhcnQgb2YgYSBsYXJnZXIgZXhwcmVzc2lvbikuIEl0IGlzIG5vdCBwb3NzaWJsZSB0byBhc3NpZ24gdG8gdGhlIGluZGl2aWR1YWwgaXRlbXMgb2YgYSB0dXBsZSwgaG93ZXZlciBpdCBpcyBwb3NzaWJsZSB0byBjcmVhdGUgdHVwbGVzIHdoaWNoIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzLCBzdWNoIGFzIGxpc3RzLlxcblxcblRob3VnaCB0dXBsZXMgbWF5IHNlZW0gc2ltaWxhciB0byBsaXN0cywgdGhleSBhcmUgb2Z0ZW4gdXNlZCBpbiBkaWZmZXJlbnQgc2l0dWF0aW9ucyBhbmQgZm9yIGRpZmZlcmVudCBwdXJwb3Nlcy4gVHVwbGVzIGFyZSBpbW11dGFibGUsIGFuZCB1c3VhbGx5IGNvbnRhaW4gYSBoZXRlcm9nZW5lb3VzIHNlcXVlbmNlIG9mIGVsZW1lbnRzIHRoYXQgYXJlIGFjY2Vzc2VkIHZpYSB1bnBhY2tpbmcgKHNlZSBsYXRlciBpbiB0aGlzIHNlY3Rpb24pIG9yIGluZGV4aW5nIChvciBldmVuIGJ5IGF0dHJpYnV0ZSBpbiB0aGUgY2FzZSBvZiBuYW1lZHR1cGxlcykuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG5Bbm90aGVyIHVzZWZ1bCBkYXRhIHR5cGUgYnVpbHQgaW50byBQeXRob24gaXMgdGhlIGRpY3Rpb25hcnkgKHNlZSBNYXBwaW5nIFR5cGVzIFxcdTIwMTQgZGljdCkuIERpY3Rpb25hcmllcyBhcmUgc29tZXRpbWVzIGZvdW5kIGluIG90aGVyIGxhbmd1YWdlcyBhcyBcXHUyMDFjYXNzb2NpYXRpdmUgbWVtb3JpZXNcXHUyMDFkIG9yIFxcdTIwMWNhc3NvY2lhdGl2ZSBhcnJheXNcXHUyMDFkLiBVbmxpa2Ugc2VxdWVuY2VzLCB3aGljaCBhcmUgaW5kZXhlZCBieSBhIHJhbmdlIG9mIG51bWJlcnMsIGRpY3Rpb25hcmllcyBhcmUgaW5kZXhlZCBieSBrZXlzLCB3aGljaCBjYW4gYmUgYW55IGltbXV0YWJsZSB0eXBlOyBzdHJpbmdzIGFuZCBudW1iZXJzIGNhbiBhbHdheXMgYmUga2V5cy4gVHVwbGVzIGNhbiBiZSB1c2VkIGFzIGtleXMgaWYgdGhleSBjb250YWluIG9ubHkgc3RyaW5ncywgbnVtYmVycywgb3IgdHVwbGVzOyBpZiBhIHR1cGxlIGNvbnRhaW5zIGFueSBtdXRhYmxlIG9iamVjdCBlaXRoZXIgZGlyZWN0bHkgb3IgaW5kaXJlY3RseSwgaXQgY2Fubm90IGJlIHVzZWQgYXMgYSBrZXkuIFlvdSBjYW5cXHUyMDE5dCB1c2UgbGlzdHMgYXMga2V5cywgc2luY2UgbGlzdHMgY2FuIGJlIG1vZGlmaWVkIGluIHBsYWNlIHVzaW5nIGluZGV4IGFzc2lnbm1lbnRzLCBzbGljZSBhc3NpZ25tZW50cywgb3IgbWV0aG9kcyBsaWtlIGFwcGVuZCgpIGFuZCBleHRlbmQoKS5cIl19LCB7XCJ0aXRsZVwiOiBcIjMuIERhdGEgbW9kZWwgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3JlZmVyZW5jZS9kYXRhbW9kZWwuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiVGhlIHZhbHVlIG9mIHNvbWUgb2JqZWN0cyBjYW4gY2hhbmdlLiBPYmplY3RzIHdob3NlIHZhbHVlIGNhbiBjaGFuZ2UgYXJlIHNhaWQgdG8gYmUgbXV0YWJsZTsgb2JqZWN0cyB3aG9zZSB2YWx1ZSBpcyB1bmNoYW5nZWFibGUgb25jZSB0aGV5IGFyZSBjcmVhdGVkIGFyZSBjYWxsZWQgaW1tdXRhYmxlLiAoVGhlIHZhbHVlIG9mIGFuIGltbXV0YWJsZSBjb250YWluZXIgb2JqZWN0IHRoYXQgY29udGFpbnMgYSByZWZlcmVuY2UgdG8gYSBtdXRhYmxlIG9iamVjdCBjYW4gY2hhbmdlIHdoZW4gdGhlIGxhdHRlclxcdTIwMTlzIHZhbHVlIGlzIGNoYW5nZWQ7IGhvd2V2ZXIgdGhlIGNvbnRhaW5lciBpcyBzdGlsbCBjb25zaWRlcmVkIGltbXV0YWJsZSwgYmVjYXVzZSB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGl0IGNvbnRhaW5zIGNhbm5vdCBiZSBjaGFuZ2VkLiBTbywgaW1tdXRhYmlsaXR5IGlzIG5vdCBzdHJpY3RseSB0aGUgc2FtZSBhcyBoYXZpbmcgYW4gdW5jaGFuZ2VhYmxlIHZhbHVlLCBpdCBpcyBtb3JlIHN1YnRsZS4pIEFuIG9iamVjdFxcdTIwMTlzIG11dGFiaWxpdHkgaXMgZGV0ZXJtaW5lZCBieSBpdHMgdHlwZTsgZm9yIGluc3RhbmNlLCBudW1iZXJzLCBzdHJpbmdzIGFuZCB0dXBsZXMgYXJlIGltbXV0YWJsZSwgd2hpbGUgZGljdGlvbmFyaWVzIGFuZCBsaXN0cyBhcmUgbXV0YWJsZS5cXG4uLi5cXG5Tb21lIG9iamVjdHMgY29udGFpbiByZWZlcmVuY2VzIHRvIG90aGVyIG9iamVjdHM7IHRoZXNlIGFyZSBjYWxsZWQgY29udGFpbmVycy4gRXhhbXBsZXMgb2YgY29udGFpbmVycyBhcmUgdHVwbGVzLCBsaXN0cyBhbmQgZGljdGlvbmFyaWVzLiBUaGUgcmVmZXJlbmNlcyBhcmUgcGFydCBvZiBhIGNvbnRhaW5lclxcdTIwMTlzIHZhbHVlLiBJbiBtb3N0IGNhc2VzLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIHZhbHVlIG9mIGEgY29udGFpbmVyLCB3ZSBpbXBseSB0aGUgdmFsdWVzLCBub3QgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGNvbnRhaW5lZCBvYmplY3RzOyBob3dldmVyLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIG11dGFiaWxpdHkgb2YgYSBjb250YWluZXIsIG9ubHkgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGltbWVkaWF0ZWx5IGNvbnRhaW5lZCBvYmplY3RzIGFyZSBpbXBsaWVkLiBTbywgaWYgYW4gaW1tdXRhYmxlIGNvbnRhaW5lciAobGlrZSBhIHR1cGxlKSBjb250YWlucyBhIHJlZmVyZW5jZSB0byBhIG11dGFibGUgb2JqZWN0LCBpdHMgdmFsdWUgY2hhbmdlcyBpZiB0aGF0IG11dGFibGUgb2JqZWN0IGlzIGNoYW5nZWQuXFxuLi4uXFxuIyMjIDMuMi41LiBTZXF1ZW5jZXNcXHUwMGI2XFxuLi4uXFxuIyMjIyAzLjIuNS4xLiBJbW11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbkFuIG9iamVjdCBvZiBhbiBpbW11dGFibGUgc2VxdWVuY2UgdHlwZSBjYW5ub3QgY2hhbmdlIG9uY2UgaXQgaXMgY3JlYXRlZC4gKElmIHRoZSBvYmplY3QgY29udGFpbnMgcmVmZXJlbmNlcyB0byBvdGhlciBvYmplY3RzLCB0aGVzZSBvdGhlciBvYmplY3RzIG1heSBiZSBtdXRhYmxlIGFuZCBtYXkgYmUgY2hhbmdlZDsgaG93ZXZlciwgdGhlIGNvbGxlY3Rpb24gb2Ygb2JqZWN0cyBkaXJlY3RseSByZWZlcmVuY2VkIGJ5IGFuIGltbXV0YWJsZSBvYmplY3QgY2Fubm90IGNoYW5nZS4pXFxuXFxuVGhlIGZvbGxvd2luZyB0eXBlcyBhcmUgaW1tdXRhYmxlIHNlcXVlbmNlczpcXG4uLi5cXG5UdXBsZXNcXG46IFRoZSBpdGVtcyBvZiBhIGB0dXBsZWAgYXJlIGFyYml0cmFyeSBQeXRob24gb2JqZWN0cy4gVHVwbGVzIG9mIHR3byBvciBtb3JlIGl0ZW1zIGFyZSBmb3JtZWQgYnkgY29tbWEtc2VwYXJhdGVkIGxpc3RzIG9mIGV4cHJlc3Npb25zLiBBIHR1cGxlIG9mIG9uZSBpdGVtIChhIFxcdTIwMThzaW5nbGV0b25cXHUyMDE5KSBjYW4gYmUgZm9ybWVkIGJ5IGFmZml4aW5nIGEgY29tbWEgdG8gYW4gZXhwcmVzc2lvbiAoYW4gZXhwcmVzc2lvbiBieSBpdHNlbGYgZG9lcyBub3QgY3JlYXRlIGEgdHVwbGUsIHNpbmNlIHBhcmVudGhlc2VzIG11c3QgYmUgdXNhYmxlIGZvciBncm91cGluZyBvZiBleHByZXNzaW9ucykuIEFuIGVtcHR5IHR1cGxlIGNhbiBiZSBmb3JtZWQgYnkgYW4gZW1wdHkgcGFpciBvZiBwYXJlbnRoZXNlcy5cXG4uLi5cXG4jIyMjIDMuMi41LjIuIE11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbk11dGFibGUgc2VxdWVuY2VzIGNhbiBiZSBjaGFuZ2VkIGFmdGVyIHRoZXkgYXJlIGNyZWF0ZWQuIFRoZSBzdWJzY3JpcHRpb24gYW5kIHNsaWNpbmcgbm90YXRpb25zIGNhbiBiZSB1c2VkIGFzIHRoZSB0YXJnZXQgb2YgYXNzaWdubWVudCBhbmQgYGRlbGAgKGRlbGV0ZSkgc3RhdGVtZW50cy5cXG4uLi5cXG5UaGVyZSBhcmUgY3VycmVudGx5IHR3byBpbnRyaW5zaWMgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlczpcXG5cXG5MaXN0c1xcbjogVGhlIGl0ZW1zIG9mIGEgbGlzdCBhcmUgYXJiaXRyYXJ5IFB5dGhvbiBvYmplY3RzLiBMaXN0cyBhcmUgZm9ybWVkIGJ5IHBsYWNpbmcgYSBjb21tYS1zZXBhcmF0ZWQgbGlzdCBvZiBleHByZXNzaW9ucyBpbiBzcXVhcmUgYnJhY2tldHMuIChOb3RlIHRoYXQgdGhlcmUgYXJlIG5vIHNwZWNpYWwgY2FzZXMgbmVlZGVkIHRvIGZvcm0gbGlzdHMgb2YgbGVuZ3RoIDAgb3IgMS4pXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjcgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvYnVpbHRpbnMvc3RkdHlwZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiY29sbGVjdGlvbiBjbGFzc2VzIGFyZSBtdXRhYmxlLiBUaGUgbWV0aG9kcyB0aGF0IGFkZCwgc3VidHJhY3QsIG9yIHJlYXJyYW5nZSB0aGVpciAuLi4gaW4gcGxhY2UsIGFuZCBkb25cXHUyMDE5dCByZXR1cm4gYSAuLi4gLCBuZXZlciByZXR1cm4gdGhlIGNvbGxlY3Rpb24gaW5zdGFuY2UgaXRzZWxmIGJ1dCBgTm9uZWAuXFxuLi4uXFxuIyMgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBgbGlzdGAsIGB0dXBsZWAsIGByYW5nZWBcXHUwMGI2XFxuXFxuVGhlcmUgYXJlIHRocmVlIGJhc2ljIHNlcXVlbmNlIHR5cGVzOiBsaXN0cywgdHVwbGVzLCBhbmQgcmFuZ2Ugb2JqZWN0cy4gQWRkaXRpb25hbCBzZXF1ZW5jZSB0eXBlcyB0YWlsb3JlZCBmb3IgcHJvY2Vzc2luZyBvZiBiaW5hcnkgZGF0YSBhbmQgdGV4dCBzdHJpbmdzIGFyZSBkZXNjcmliZWQgaW4gZGVkaWNhdGVkIHNlY3Rpb25zLlxcbi4uLlxcblRoZSBvcGVyYXRpb25zIGluIHRoZSBmb2xsb3dpbmcgdGFibGUgLi4uIG11dGFibGUgYW5kIGltbXV0YWJsZS4gVGhlIGBjb2xsZWN0aW9ucy4gLi4uIGlzIHByb3ZpZGVkIHRvIG1ha2UgLi4uIGVhc2llciB0byBjb3JyZWN0bHkgaW1wbGVtZW50IHRoZXNlIG9wZXJhdGlvbnMgb24gY3VzdG9tIHNlcXVlbmNlIHR5cGVzXFxuLi4uXFxuIyMjIEltbXV0YWJsZSBTZXF1ZW5jZSBUeXBlc1xcdTAwYjZcXG4uLi5cXG5tdXRhYmxlIHNlcXVlbmNlIHR5cGVzIGlzXFxuLi4uXFxuc3VwcG9ydCBhbGxvd3MgaW1tdXRhYmxlIHNlcXVlbmNlcywgLi4uICwgdG8gYmUgdXNlZCBhcyBgZGljdGAga2V5cyBhbmQgc3RvcmVkIGluIC4uLiBlbnNldGAgaW5zdGFuY2VzXFxuLi4uXFxuIyMjIE11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxuIyMjIExpc3RzXFx1MDBiNlxcblxcbkxpc3RzIGFyZSBtdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaG9tb2dlbmVvdXMgaXRlbXMgKHdoZXJlIHRoZSBwcmVjaXNlIGRlZ3JlZSBvZiBzaW1pbGFyaXR5IHdpbGwgdmFyeSBieSBhcHBsaWNhdGlvbikuXFxuLi4uXFxuIyMjIFR1cGxlc1xcdTAwYjZcXG5cXG5UdXBsZXMgYXJlIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhldGVyb2dlbmVvdXMgZGF0YSAoc3VjaCBhcyB0aGUgMi10dXBsZXMgcHJvZHVjZWQgYnkgdGhlIGBlbnVtZXJhdGUoKWAgYnVpbHQtaW4pLiBUdXBsZXMgYXJlIGFsc28gdXNlZCBmb3IgY2FzZXMgd2hlcmUgYW4gaW1tdXRhYmxlIHNlcXVlbmNlIG9mIGhvbW9nZW5lb3VzIGRhdGEgaXMgbmVlZGVkIChzdWNoIGFzIGFsbG93aW5nIHN0b3JhZ2UgaW4gYSBgc2V0YCBvciBgZGljdGAgaW5zdGFuY2UpLlxcbi4uLlxcbnR1cGxlKGl0ZXJhYmxlPVxcbi4uLlxcblRoZSBjb25zdHJ1Y3RvciBidWlsZHMgYSB0dXBsZSB3aG9zZSBpdGVtcyBhcmUgdGhlIHNhbWUgYW5kIGluIHRoZSBzYW1lIG9yZGVyIGFzIGl0ZXJhYmxlXFx1MjAxOXMgaXRlbXMuIGl0ZXJhYmxlIG1heSBiZSBlaXRoZXIgYSBzZXF1ZW5jZSwgYSBjb250YWluZXIgdGhhdCBzdXBwb3J0cyBpdGVyYXRpb24sIG9yIGFuIGl0ZXJhdG9yIG9iamVjdC4gSWYgaXRlcmFibGUgaXMgYWxyZWFkeSBhIHR1cGxlLCBpdCBpcyByZXR1cm5lZCB1bmNoYW5nZWQuIEZvciBleGFtcGxlLCBgdHVwbGUoJ2FiYycpYCByZXR1cm5zIGAoJ2EnLCAnYicsICdjJylgIGFuZCBgdHVwbGUoIFsxLCAyLCAzXSApYCByZXR1cm5zIGAoMSwgMiwgMylgLiBJZiBubyBhcmd1bWVudCBpcyBnaXZlbiwgdGhlIGNvbnN0cnVjdG9yIGNyZWF0ZXMgYSBuZXcgZW1wdHkgdHVwbGUsIGAoKWAuXFxuLi4uXFxuVHVwbGVzIGltcGxlbWVudCBhbGwgb2YgdGhlIGNvbW1vbiBzZXF1ZW5jZSBvcGVyYXRpb25zLlxcbi4uLlxcbkZvciBoZXRlcm9nZW5lb3VzIGNvbGxlY3Rpb25zIG9mIGRhdGEgd2hlcmUgYWNjZXNzIGJ5IG5hbWUgaXMgY2xlYXJlciB0aGFuIGFjY2VzcyBieSBpbmRleCwgYGNvbGxlY3Rpb25zLm5hbWVkdHVwbGUoKWAgbWF5IGJlIGEgbW9yZSBhcHByb3ByaWF0ZSBjaG9pY2UgdGhhbiBhIHNpbXBsZSB0dXBsZSBvYmplY3QuXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvbGlicmFyeS9zdGR0eXBlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJTb21lIGNvbGxlY3Rpb24gY2xhc3NlcyBhcmUgbXV0YWJsZS4gVGhlIG1ldGhvZHMgdGhhdCBhZGQsIHN1YnRyYWN0LCBvciByZWFycmFuZ2UgdGhlaXIgLi4uIGluIHBsYWNlLCBhbmQgZG9uXFx1MjAxOXQgcmV0dXJuIGEgLi4uICwgbmV2ZXIgcmV0dXJuIHRoZSBjb2xsZWN0aW9uIGluc3RhbmNlIGl0c2VsZiBidXQgYE5vbmVgLlxcbi4uLlxcbiMjIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgYGxpc3RgLCBgdHVwbGVgLCBgcmFuZ2VgXFx1MDBiNlxcblxcblRoZXJlIGFyZSB0aHJlZSBiYXNpYyBzZXF1ZW5jZSB0eXBlczogbGlzdHMsIHR1cGxlcywgYW5kIC4uLiBvYmplY3RzLiBBZGRpdGlvbmFsIHNlcXVlbmNlIHR5cGVzIHRhaWxvcmVkIGZvciBwcm9jZXNzaW5nIG9mIGJpbmFyeSBkYXRhIGFuZCB0ZXh0IHN0cmluZ3MgYXJlIGRlc2NyaWJlZCBpbiBkZWRpY2F0ZWQgc2VjdGlvbnMuXFxuLi4uXFxuVGhlIG9wZXJhdGlvbnMgaW4gdGhlIGZvbGxvd2luZyB0YWJsZSAuLi4gaW1tdXRhYmxlLiBUaGUgYCAuLi4gaXMgcHJvdmlkZWQgdG8gbWFrZSAuLi4gZWFzaWVyIHRvIGNvcnJlY3RseSBpbXBsZW1lbnQgdGhlc2Ugb3BlcmF0aW9ucyBvbiBjdXN0b20gc2VxdWVuY2UgdHlwZXNcXG4uLi5cXG4jIyMgSW1tdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbm9wZXJhdGlvbiB0aGF0IGltbXV0YWJsZSBzZXF1ZW5jZSAuLi4gYnkgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlcyBpcyAuLi4gYGhhc2goKWBcXG4uLi5cXG5UaGlzIHN1cHBvcnQgYWxsb3dzIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHN1Y2ggYXMgYHR1cGxlYCBpbnN0YW5jZXMsIHRvIGJlIHVzZWQgYXMgYGRpY3RgIGtleXMgYW5kIHN0b3JlZCBpbiBgc2V0YCBhbmQgYGZyb3plbnNldGAgaW5zdGFuY2VzLlxcbi4uLlxcbiMjIyBNdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbiMjIyBMaXN0c1xcdTAwYjZcXG5cXG5MaXN0cyBhcmUgbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhvbW9nZW5lb3VzIGl0ZW1zICh3aGVyZSB0aGUgcHJlY2lzZSBkZWdyZWUgb2Ygc2ltaWxhcml0eSB3aWxsIHZhcnkgYnkgYXBwbGljYXRpb24pLlxcbi4uLlxcbiMjIyBUdXBsZXNcXHUwMGI2XFxuXFxuVHVwbGVzIGFyZSBpbW11dGFibGUgc2VxdWVuY2VzLCB0eXBpY2FsbHkgdXNlZCB0byBzdG9yZSBjb2xsZWN0aW9ucyBvZiBoZXRlcm9nZW5lb3VzIGRhdGEgKHN1Y2ggYXMgdGhlIDItdHVwbGVzIHByb2R1Y2VkIGJ5IHRoZSBgZW51bWVyYXRlKClgIGJ1aWx0LWluKS4gVHVwbGVzIGFyZSBhbHNvIHVzZWQgZm9yIGNhc2VzIHdoZXJlIGFuIGltbXV0YWJsZSBzZXF1ZW5jZSBvZiBob21vZ2VuZW91cyBkYXRhIGlzIG5lZWRlZCAoc3VjaCBhcyBhbGxvd2luZyBzdG9yYWdlIGluIGEgYHNldGAgb3IgYGRpY3RgIGluc3RhbmNlKS5cXG4uLi5cXG5UaGUgY29uc3RydWN0b3IgYnVpbGRzIGEgdHVwbGUgd2hvc2UgaXRlbXMgYXJlIHRoZSBzYW1lIGFuZCBpbiB0aGUgc2FtZSBvcmRlciBhcyBpdGVyYWJsZVxcdTIwMTlzIGl0ZW1zLiBpdGVyYWJsZSBtYXkgYmUgZWl0aGVyIGEgc2VxdWVuY2UsIGEgY29udGFpbmVyIHRoYXQgc3VwcG9ydHMgaXRlcmF0aW9uLCBvciBhbiBpdGVyYXRvciBvYmplY3QuIElmIGl0ZXJhYmxlIGlzIGFscmVhZHkgYSB0dXBsZSwgaXQgaXMgcmV0dXJuZWQgdW5jaGFuZ2VkLiBGb3IgZXhhbXBsZSwgYHR1cGxlKCdhYmMnKWAgcmV0dXJucyBgKCdhJywgJ2InLCAnYycpYCBhbmQgYHR1cGxlKCBbMSwgMiwgM10gKWAgcmV0dXJucyBgKDEsIDIsIDMpYC4gSWYgbm8gYXJndW1lbnQgaXMgZ2l2ZW4sIHRoZSBjb25zdHJ1Y3RvciBjcmVhdGVzIGEgbmV3IGVtcHR5IHR1cGxlLCBgKClgLlxcbi4uLlxcblR1cGxlcyBpbXBsZW1lbnQgYWxsIG9mIHRoZSBjb21tb24gc2VxdWVuY2Ugb3BlcmF0aW9ucy5cXG4uLi5cXG5Gb3IgaGV0ZXJvZ2VuZW91cyBjb2xsZWN0aW9ucyBvZiBkYXRhIHdoZXJlIGFjY2VzcyBieSBuYW1lIGlzIGNsZWFyZXIgdGhhbiBhY2Nlc3MgYnkgaW5kZXgsIGBjb2xsZWN0aW9ucy5uYW1lZHR1cGxlKClgIG1heSBiZSBhIG1vcmUgYXBwcm9wcmlhdGUgY2hvaWNlIHRoYW4gYSBzaW1wbGUgdHVwbGUgb2JqZWN0LlwiXX0sIHtcInRpdGxlXCI6IFwiNS4gRGF0YSBTdHJ1Y3R1cmVzIFxcdTIwMTQgUHl0aG9uIDMuMTAuMjAgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMuMTAvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiWW91IG1pZ2h0IGhhdmUgbm90aWNlZCB0aGF0IG1ldGhvZHMgbGlrZSBgaW5zZXJ0YCwgYHJlbW92ZWAgb3IgYHNvcnRgIHRoYXQgb25seSBtb2RpZnkgdGhlIGxpc3QgaGF2ZSBubyByZXR1cm4gdmFsdWUgcHJpbnRlZCBcXHUyMDEzIHRoZXkgcmV0dXJuIHRoZSBkZWZhdWx0IGBOb25lYC4gMSBUaGlzIGlzIGEgZGVzaWduIHByaW5jaXBsZSBmb3IgYWxsIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIGluIFB5dGhvbi5cXG4uLi5cXG4jIyA1LjMuIFR1cGxlcyBhbmQgU2VxdWVuY2VzXFx1MDBiNlxcblxcbldlIHNhdyB0aGF0IGxpc3RzIGFuZCBzdHJpbmdzIGhhdmUgbWFueSBjb21tb24gcHJvcGVydGllcywgc3VjaCBhcyBpbmRleGluZyBhbmQgc2xpY2luZyBvcGVyYXRpb25zLiBUaGV5IGFyZSB0d28gZXhhbXBsZXMgb2Ygc2VxdWVuY2UgZGF0YSB0eXBlcyAoc2VlIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgbGlzdCwgdHVwbGUsIHJhbmdlKS4gU2luY2UgUHl0aG9uIGlzIGFuIGV2b2x2aW5nIGxhbmd1YWdlLCBvdGhlciBzZXF1ZW5jZSBkYXRhIHR5cGVzIG1heSBiZSBhZGRlZC4gVGhlcmUgaXMgYWxzbyBhbm90aGVyIHN0YW5kYXJkIHNlcXVlbmNlIGRhdGEgdHlwZTogdGhlIHR1cGxlLlxcblxcbkEgdHVwbGUgY29uc2lzdHMgb2YgYSBudW1iZXIgb2YgdmFsdWVzIHNlcGFyYXRlZCBieSBjb21tYXMsIGZvciBpbnN0YW5jZTpcXG4uLi5cXG4+Pj4gIyBUdXBsZXMgYXJlIGltbXV0YWJsZTpcXG4uLi4gdFswXSA9IDg4ODg4XFxuVHJhY2ViYWNrIChtb3N0IHJlY2VudCBjYWxsIGxhc3QpOlxcbiAgRmlsZSBcXFwiPHN0ZGluPlxcXCIsIGxpbmUgMSwgaW4gPG1vZHVsZT5cXG5UeXBlRXJyb3I6ICd0dXBsZScgb2JqZWN0IGRvZXMgbm90IHN1cHBvcnQgaXRlbSBhc3NpZ25tZW50XFxuPj4+ICMgYnV0IHRoZXkgY2FuIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzOlxcbi4uLiB2ID0gKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbj4+PiB2XFxuKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbi4uLlxcbkFzIHlvdSBzZWUsIG9uIG91dHB1dCB0dXBsZXMgYXJlIGFsd2F5cyBlbmNsb3NlZCBpbiBwYXJlbnRoZXNlcywgc28gdGhhdCBuZXN0ZWQgdHVwbGVzIGFyZSBpbnRlcnByZXRlZCBjb3JyZWN0bHk7IHRoZXkgbWF5IGJlIGlucHV0IHdpdGggb3Igd2l0aG91dCBzdXJyb3VuZGluZyBwYXJlbnRoZXNlcywgYWx0aG91Z2ggb2Z0ZW4gcGFyZW50aGVzZXMgYXJlIG5lY2Vzc2FyeSBhbnl3YXkgKGlmIHRoZSB0dXBsZSBpcyBwYXJ0IG9mIGEgbGFyZ2VyIGV4cHJlc3Npb24pLiBJdCBpcyBub3QgcG9zc2libGUgdG8gYXNzaWduIHRvIHRoZSBpbmRpdmlkdWFsIGl0ZW1zIG9mIGEgdHVwbGUsIGhvd2V2ZXIgaXQgaXMgcG9zc2libGUgdG8gY3JlYXRlIHR1cGxlcyB3aGljaCBjb250YWluIG11dGFibGUgb2JqZWN0cywgc3VjaCBhcyBsaXN0cy5cXG5cXG5UaG91Z2ggdHVwbGVzIG1heSBzZWVtIHNpbWlsYXIgdG8gbGlzdHMsIHRoZXkgYXJlIG9mdGVuIHVzZWQgaW4gZGlmZmVyZW50IHNpdHVhdGlvbnMgYW5kIGZvciBkaWZmZXJlbnQgcHVycG9zZXMuIFR1cGxlcyBhcmUgaW1tdXRhYmxlLCBhbmQgdXN1YWxseSBjb250YWluIGEgaGV0ZXJvZ2VuZW91cyBzZXF1ZW5jZSBvZiBlbGVtZW50cyB0aGF0IGFyZSBhY2Nlc3NlZCB2aWEgdW5wYWNraW5nIChzZWUgbGF0ZXIgaW4gdGhpcyBzZWN0aW9uKSBvciBpbmRleGluZyAob3IgZXZlbiBieSBhdHRyaWJ1dGUgaW4gdGhlIGNhc2Ugb2YgYG5hbWVkdHVwbGVzYCkuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG4jIyA1LjQuXFxuLi4uXFxuLiBTZXQgb2JqZWN0c1xcbi4uLlxcbiMjIDUuNS4gRGljdGlvbmFyaWVzXFx1MDBiNlxcblxcbkFub3RoZXIgdXNlZnVsIGRhdGEgdHlwZSBidWlsdCBpbnRvIFB5dGhvbiBpcyB0aGUgZGljdGlvbmFyeSAoc2VlIE1hcHBpbmcgVHlwZXMgXFx1MjAxNCBkaWN0KS4gRGljdGlvbmFyaWVzIGFyZSBzb21ldGltZXMgZm91bmQgaW4gb3RoZXIgbGFuZ3VhZ2VzIGFzIFxcdTIwMWNhc3NvY2lhdGl2ZSBtZW1vcmllc1xcdTIwMWQgb3IgXFx1MjAxY2Fzc29jaWF0aXZlIGFycmF5c1xcdTIwMWQuIFVubGlrZSBzZXF1ZW5jZXMsIHdoaWNoIGFyZSBpbmRleGVkIGJ5IGEgcmFuZ2Ugb2YgbnVtYmVycywgZGljdGlvbmFyaWVzIGFyZSBpbmRleGVkIGJ5IGtleXMsIHdoaWNoIGNhbiBiZSBhbnkgaW1tdXRhYmxlIHR5cGU7IHN0cmluZ3MgYW5kIG51bWJlcnMgY2FuIGFsd2F5cyBiZSBrZXlzLiBUdXBsZXMgY2FuIGJlIHVzZWQgYXMga2V5cyBpZiB0aGV5IGNvbnRhaW4gb25seSBzdHJpbmdzLCBudW1iZXJzLCBvciB0dXBsZXM7IGlmIGEgdHVwbGUgY29udGFpbnMgYW55IG11dGFibGUgb2JqZWN0IGVpdGhlciBkaXJlY3RseSBvciBpbmRpcmVjdGx5LCBpdCBjYW5ub3QgYmUgdXNlZCBhcyBhIGtleS4gWW91IGNhblxcdTIwMTl0IHVzZSBsaXN0cyBhcyBrZXlzLCBzaW5jZSBsaXN0cyBjYW4gYmUgbW9kaWZpZWQgaW4gcGxhY2UgdXNpbmcgaW5kZXggYXNzaWdubWVudHMsIHNsaWNlIGFzc2lnbm1lbnRzLCBvciBtZXRob2RzIGxpa2UgYGFwcGVuZCgpYCBhbmQgYGV4dGVuZCgpYC5cIl19XSIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ3ZWJfc2VhcmNoIiwiaWQiOiI2ZTUxMjVhYy0zMTY0LTRhNzYtOWM1Mi1hYzM0NjBjZTM2ZTQiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJzdGF0dXMiOiJzdWNjZXNzIn0seyJjb250ZW50IjpbeyJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3NrZXB0aWMiLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QTZGNGNDTjV5aEQ0Q3JzOU0iLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjB9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InJlc2VhcmNoZXIiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtM2NkOC03OWYxLTg4YTEtY2U2Y2VlMjk0ZTc5LTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoidHJhbnNmZXJfdG9fc2tlcHRpYyIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsInRvdGFsX3Rva2VucyI6NzQxMSwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fSx7ImNvbnRlbnQiOiJTdWNjZXNzZnVsbHkgdHJhbnNmZXJyZWQgdG8gc2tlcHRpYyIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ0cmFuc2Zlcl90b19za2VwdGljIiwiaWQiOiIzODkwNThkMS0wODNmLTQ2MzItYmJjNS0wOWJmZDNhMGZmZDIiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMVhqajNhUmRXeWhCSmZQbVRKQnhRYTkiLCJzdGF0dXMiOiJzdWNjZXNzIn0seyJjb250ZW50IjpbeyJzaWduYXR1cmUiOiJDQVFTMVFnS0VBZ1NHQUk0QVVJSWRHaHBibXRwYm1jU0RONlRmVTZORE05NzFsRWJ6eG9NdlJVQTFKOW94M2ZKUVlUOUlqQXJDUVFHSEJQNHBSSFBhdlh6UmxiTWJhWUhDNTdZaitHYnhncjBoZWx2ZDJPSUwvbzZSYnQ0UEZBM0ZhSUZVbGdxOGdjN0tRSTlGdEdudlNBa0xCUnpJc1VwLzJkNVJkMnpnRXNma0ZWZVhzbjhXWVlaa2luQVFWVnk2QTdCM0xhRUpXMHhiL3g4OERpNjdmZWR3eDBRK0ZsWnR3Y21UM0RjYWExMUp2ZXlIUnIzZG0rekdTWlA2Q1hlWE5KOG5yVFo3VzFZUlR5c0Q2dzdvN0xwOGgxUndxNGY0bzhqK1FVZlkwdmplK3pKWnN4SFBtUU50M3lrZUtoOTloZ242ZGJpejYvRUhxbWJIalNBdTFNNFNxRURmcUlLVGhIWWVuYW1BWm1jUTRYQU9vNm1kUTYrNzdWZ3BWMWxDMS9rNTJJaVVvSmprR2xRcm5MQ3Z1dGlJN0tOK3NnSzhjMisyS0NKclBSQ3ZWVitCa1N0dlA1aldXcXp5dHBLUStuSHpoTDQ0OE9ySUE5Z2RKQzM5QzhlRHJYZVJuSTd0NEZxbGtYR29NbFpBVjlVTWNyUUhSeE9NaTh0T3lUemk5b3lCRkRUU3NRK0xKcWVyMHR6MUZ0OWdlazZ2emc1MFNwVkFGUWRMRVJ2OFIxWXFNYWVJK3hzdlRBRVc5TVJndlJNa3pMdXpSVDJndXczakQrRmppcDM3eGdLNVFJMFIrQ3NPQW1RUG9UWml6emNJeUFVQXVDSkZDZ3EvZ0QvQ2liTHhveW1pVlVJMWFuOVNudDNLcE5Sb1dkdldJbmUzM1YwcHY4UEhnOXlXMkp1TE9xUUdpbExDc0svUTNvUXIzdXBFTVh4WEdqQ0w3V1VpenZSWkdGRUVWK1ZwbFl0N3Y0dEtaTWo1cGszdmRlTTBNSjVYWmc3UHZVc1J6eWN4Y25yVDNLbm9jM3FNK3R0cVpmVHNiOGQ2VS9wUWlmcTdHdWFoRk9IcnZyb3FnSkxqKy84MVl5MXBaRmtqZkpnWTBYa2x6QW5BNkNGcS9ZVDBneGJWK1RINUlTK1dhWU9VekszNlM4czVxcU13aG5ndjRJekUzY3RYWHFuaElaWE56bnlWQnFiWWEvR3gyYkNqY0VqMm41UnlHWVJTLytNY3VsZ0xCem5ROU1rRzJEN1FQM01xL253SFhLTUlhTFM4Vks4aEczZDByQWpsdm5hdVhTaE5ySnh0a2o0cWt3bnhENExyRWprMXVlK1BMY2JMcVlXd0FyOG4wcys0MWpyc0lQVnl5Szl2RmFFWnJBOUthQnhrSnlpcXg1MEdaTm43Y2t4K21rSTE4Z0g1Mml5VlB0VThGS01YanVnQ2w2bVBMaUhZaURRZCtHNjNMdWRtT0t5czlpdFZuQkRCTk1QMmIvSTcrYklWUmxoZUdhWldyNk1BVEtsODZCM3krMGt1T3JMUk15V2NkaWJjMmlRa1ptKzhmN2RUVXdiS2JCSnNLT0ZZNzMvOG5sTC9KaVlwano2blVJaytJcVNqYUZ5SzZYRmxDY3VrV3BXSHBESm5TdmJFemV5N3FIeUNxSitJK29uOGlQOEdPdGVtcFFhUzAvNGtaR1N3bHJFNnhPajZFd0dwZmpiRFpLSGtQLzJSQWROWVBRcTN6N2JkVkpNSk5uRk16NmUzN216TXBDQmlod1BkU0ZUUkUzU0NZNjZ3dlRnSnpHQ1V5aEhiU2k1aTFhenpPWGQ4dUhDU3BULzZMMW82VlUwcGpvUjhLYWE0d0NXSEdmN1AvV0VkK2F2eXAwMkpudTFjbXBXMCtHcSt2SzloMHRkYjJJQ2xNL2VOZmRvdVFQK2VwOE1KVWpyYURSSUNtQUpRWTVvYlM3MXZuaU9vK3JlYUZ5MHpSZ0IiLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsic2lnbmF0dXJlIjoiQ0FRUy9SRUtFUWdTR0FJNEFVSUpibUZ5Y21GMGFXOXVFZ3dGaDNBVkpaUDNzOHkxSHEwYURHNzZ1aEhPTmFTY1hBR1NoaUl3R2dqTFhIVXgxeVpya0xQbGpqelRPUXljcExKaUtWd1hjeG0vRXpLSm0xczhNNVd4dVJFV0VsZFhjb2FOMC9tc0twa1JNVCtlTGI1aWg5ckFIVUxUb21VTnNVd0pldTBqckplNEdMYXp5N3NQNUt0akVPRUJLUXNYVWZvUTdmRGtaT1FVSFppOXp6UU40aEZObVlKVWYzT1ZxZjlUUU5kQzErbS8vSEFaUGZXbUF5Nkh0RHZtN2tnQkprQ0VvaUZubTd0bi9QWDFoWEdQUmdXYzZCVFhVTXIyV0E4YUt5cWdnUlViTUgyK0pZdjVpV1pkZENjMXVwMU5icjNzRjdXUGNvdDlaQmh5SFFYcjY5MnplSms2U0pneWdJVnJBU2JNbHkyYkJ4VFdKQjYyM2dWZ3pwQ21WU0xaRnBrS1hnUGNwOGhHeURLT3ZGNlNpUVNIcUdSeFFvZnI3VnMxYVBoZlppeFJPSkdEK01scmtnZ1ZBbkdNeVlJRFVacGF2dENlaVUxSm1CU3owMDg0YlhQdFQvdG5wc0tPblNQWVpRbzNCMEJYS2JxQS9Xc0tuZzgvVjNaOFV1YWpvNklWWjJoZ0ExV0RkdXFRT1FrRXhHVTZwRnVMaGViSWp1V3hTTkkvRGZmdlIvL0xOcUNmdmRuZjBodm9QdER4ZngvU2F3SXg0L3NwcTl4YndGekh0TGdvSGcxRjJ3Rnc3Y2xQRGxnMVB5RC92MnlKRzNWOVQ4R0w2akdQRTZyaGZpbC91NEhOdmY3QlcwZFF6Q1RUWGNxWnk0bkJ5eTArcCtTN1p4Zk1sNEtweGEyZ09oaFNOamZwZnVYMFFwTDRrTUU4SjFubGlGTjFkOEtxRVB1REkxOEJRSFVoRC9VZlkyOW9BMHg1MTBGak0vQnNMWjRuNXNwcGtvSWk3Q0ZvUXEyYUFLVk5mQzFLaWxoSXBSRCsxZHlEdFpRa2xOOGdacE9FU0h3Nk1MMVR0VTB5ZkFjYm1xb3hKNmhoanp3SDZDTXNUUUVFeThNYlJCTStRS0trSVJkUkNQSEdGVzJMa3VSNTArSjBwRi90cjZwcVZ2SmVvemZpTW5yRWlsQjI3VFpQNG5LVEpHOHJkLy92Q1dlbjB5N0NBLzhkQko5Z2RwNXR4N05WQTQ1azFnZGN4RVIxZ1lvdU5YM24xdkczL0IxRjQrOTN3WFc5TXFvNlVzNTlUUW1yYjlOcWE3cFNRTlNySDA2dzFHeHlCQWxrRmZxQWxhZmsxNmFNcHE0MTZUSllwR3k0M1dDVWlDY0xXbGhpV2d6MnNnT0xBWFA5YTV3ZVdBS1JDOEo2amtjVWtEZVhuVytIRlNpUE02KzlCbzZrVmhMVWhONXJjNG1IWk9acEY3cHRQcHNRTjRYQ3FMUHNxcFJILzhvNVhweUM2Wm5lVGVGMkV4dGpod1pWalhEd01iSXpWWUlidzF5MlBQR3c4ZEM3cGM0c0lWU25JNG9RQUgwdjY1MGdoZmhtTWV2V1c0aDRQeWxidy9ERzRmZGVUMys4ejBmOE1PU1R0ZmlweDA1L1F0dEhsQXZPRFJ4YTV5REN2T3o2bjNmMUJPME4vNE1iR3lPTTI3cVc0eDhtQlZvcnlVWldmc21oZmkwL0dmTnZCOCt5bFRHYllOWDI3T2NFRjVXWnROcWJXRXk3VjVHYm1aVVN4c3BjZzdGQlozajMvM0w3bXV2WlhvWmkxZjVlcnE0eTNsU0grb0Y3cVBGbnZpZXlMT0N3dWJjUXkrb3lvd2o0Y0QwaWRSZWNucCtzKzUyRDcrbXpGMWdwVytmK2NKYXNveGlnYnhrT2xvUzR5ZWlLdmdNNG5XU3BHRWoxMlJQeDVvODBFbXppWE45dDR6SnpXMWh0QTVoWEpKZUZ5bXBNWXR5Q1VDZXZSV1ZtclJZa3RRN1VuQ2E2dEwvQVZQaTFuaTVYQ1hIZkxPdXFmSzVLTFl4andWdGRoaGZtQ0x2cGFLVk5DRU9DZjRuTkdIcUQ5OHdVYjBlaldOd0RZQm5KR2tCUnc5UlZzcGZ3TmRvV29WY24ycXY4eXdHVXA3eWMvc2syTHBJNlZ6aVJ5dldxYkdORzFaSnFWVnV0UmM0NitzT0U0VlNlUzIvQ1ROS05hNk5NQ2YvaHh4YVVWc1lmS2RQK1MxTGM0aWFuS3Awc1V1Z1BwbXRveWRDMENQMlJ3am5ITGNuenhDbnZxeGxMUVdYTU5QVHN1MnlCY0hsV0pMdGNtZGcvTWNMYWd5N2w1a0c2c043WnhKTEQva0F4OEU2dXFvaFArSy9qTVNyWXVRSm5ocjR0ZjRiaVdGWTNKemYvS3BBWXlWU3ErcGJmRktYTWJMOW5IRW9aUVQxNVd5YWJkNkRNQXEzUVpPdGhNNXBVemROcVVYa2NySTFQd2pBU0FnNGRWVS8ybys5OG1UWG55aFA3VmxFa1RuZGRwd3lKRitZQ0NjMDFwcWE2Wlg5OXBuUitZT3BJcmI1RHRQY0txMHQ1Y0ZDRHlReE4xbFV1cVpjWXVqdlgvR0M3KzZJcFJUei9Oa0VZLzVjbnRPZlpKeWQ2U2tVeC9KUjVBSklSUENOZWliMHJjV25uRm4wWEs4czIyeDQwd0xqRXR1ZlhETzkreXJhZjRrSHhsQkkzU0IwbWVNamVwWDUwQ0poQXg3TmVra1JGbndvZWNMZnFJeXJqWHJnQVpzVE1QaVg5NHNuamRyemlzQVh0QkthbnVsaWxTeE9mVzJIU21mbmswTDJHWEhKSjNTK3ZDYmdSRW1sOWRla2ZtSGhtd0VZdHYza2ZodGg3RVJJd1ZUOGo2OU90SUJDbWVUSUp1QTdvNzNQWGMxTEoyd2h1Wks3RlplNk5UT3M2MU1QN0pWbEljNnRiaHRPQmszTlhVOHBCUE9DWTZRNTVkNjNGdHBVVS9LQ2JRMEsyV1E5bzVqTkd3Tk5KNkN3NUZEQ3QreDVZSmVQMWQyUXpEU2lxK2NTWnF3cUNaVmNOb0FmdEdHemdyUlo0dmtwYlAweFFaSEN0V3NVd0hWVGpyWTROZlJEakhkVlBSbjhKOVh0b29jL2hJV2txekZhY3pZVGxhcHUyUkNKcWU4YUVvM2p6V1pDRUo3QWlCekJZcmFZRGdPZmVpUXllSlVzYmJYSGRHS2lpdzIyQmRwNE0rWXo2NzFNWjBpZTlFV1JrczBRMkRIR1VUbmU5UzR0QUFKUEdiVWY0eHRDTW9KNDBRNFFXMDVRTFNaeTN3TkZBVUZZbVhFNEVDREZCOCtJRWVORkZGL3RnQW5maDhHdkZoLzF4WStMRStvek1taW5qU1J3aHEwOXpRQUVrTkZ5dTBJS2d2RGt5T0kraWNjRjdkb1dMVFVhc1B4TEgxeEdQQlNLa2VXQ25Tb1gvNEMyYzlkdTY2QmJSWGFtSHFhZStWQ2lTNkJOOTUzaVlLT3U4ck1rbmVXK1RGalYwQ3pBZjhQTkc5TEJheVVnWTVVdzl4MFNhcDZ6STRQcGFaS0g0ZDlrdGtGVSs5MXlCYk41U1J0ZGQ4L0VRYXRKWG1CTmZ3N2tScmxsK0xqd1p3ZVdMRWIwalQxUnBHdURxSExkTFRNTWdvd1NjenJwMnpDc0pkUUgvc2VhWXJwd2tDRlI4ZFJqTTdKUlVrVmExQTBTdmRaQVg3NXZ1QnZ2TEJ6TjJjK1JidDVvcXk5M2xxSUJJaktiN3cyVWtSVGI0cEJXY05LTlBVZ3QzYXovUHhReEVhc2FsdGRtdXl2c1hZYnVyM1ZQTm44SnpOLzZKNU4rUUNuejU3MVRjY2FwQ21rUHZGRStnYXlYVStVRjFWS1l5RmNVdmxldjdsc1FsbWpxeksyUUE4WDdRM1R6RmFwcWh2SktsdDdkekJIcmFxWkJnUGdiOU40b0tOYXlpOEZaVDcxK2thWWhJaEZyRVNLWVJIZkhrajVuM2tmaGdMeSt4K2pMRGJLeE1nZ2tnSG5KejlSdUVIWlhCV3RrWUFRPT0iLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsiaWQiOiJ0b29sdV8wMUg2ZlY4SnZSSk52WHk0N2VSbmQzakgiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6e30sIm5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHdBTVJkUzIxRmVKZnpRaE0zcyIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo3Mzk2LCJvdXRwdXRfdG9rZW5zIjo3NzAsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjczOX0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoic2tlcHRpYyIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy00YWU5LTc2YzAtODlhNi00ZjBiMTE0ZTMwMzAtMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzM5Niwib3V0cHV0X3Rva2VucyI6NzcwLCJ0b3RhbF90b2tlbnMiOjgxNjYsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjczOX19fSx7ImNvbnRlbnQiOiJTdWNjZXNzZnVsbHkgdHJhbnNmZXJyZWQgdG8gdmVyaWZpZXIiLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7fSwidHlwZSI6InRvb2wiLCJuYW1lIjoidHJhbnNmZXJfdG9fdmVyaWZpZXIiLCJpZCI6ImMwNjQ1NjNhLTk3NGYtNGYwZi1hYjRhLTA4MGU2M2MwMWNmNSIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsInN0YXR1cyI6InN1Y2Nlc3MifV0sImFjdGl2ZV9hZ2VudCI6InZlcmlmaWVyIn0=" + } + } + ], + "events": [ + { + "timeUnixNano": "1790968032543667000", + "name": "exception", + "attributes": [ + { + "key": "exception.type", + "value": { + "stringValue": "Exception" + } + }, + { + "key": "exception.message", + "value": { + "stringValue": "ParentCommand(Command(graph='verifier:544dea40-e16b-458c-1464-128e1b9b10a9', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', id='389058d1-083f-4632-bbc5-09bfd3a0ffd2', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9'), AIMessage(content=[{'signature': 'CAQS1QgKEAgSGAI4AUIIdGhpbmtpbmcSDN6TfU6NDM971lEbzxoMvRUA1J9ox3fJQYT9IjArCQQGHBP4pRHPavXzRlbMbaYHC57Yj+Gbxgr0helvd2OIL/o6Rbt4PFA3FaIFUlgq8gc7KQI9FtGnvSAkLBRzIsUp/2d5Rd2zgEsfkFVeXsn8WYYZkinAQVVy6A7B3LaEJW0xb/x88Di67fedwx0Q+FlZtwcmT3Dcaa11JveyHRr3dm+zGSZP6CXeXNJ8nrTZ7W1YRTysD6w7o7Lp8h1Rwq4f4o8j+QUfY0vje+zJZsxHPmQNt3ykeKh99hgn6dbiz6/EHqmbHjSAu1M4SqEDfqIKThHYenamAZmcQ4XAOo6mdQ6+77VgpV1lC1/k52IiUoJjkGlQrnLCvutiI7KN+sgK8c2+2KCJrPRCvVV+BkStvP5jWWqzytpKQ+nHzhL448OrIA9gdJC39C8eDrXeRnI7t4FqlkXGoMlZAV9UMcrQHRxOMi8tOyTzi9oyBFDTSsQ+LJqer0tz1Ft9gek6vzg50SpVAFQdLERv8R1YqMaeI+xsvTAEW9MRgvRMkzLuzRT2guw3jD+Fjip37xgK5QI0R+CsOAmQPoTZizzcIyAUAuCJFCgq/gD/CibLxoymiVUI1an9Snt3KpNRoWdvWIne33V0pv8PHg9yW2JuLOqQGilLCsK/Q3oQr3upEMXxXGjCL7WUizvRZGFEEV+VplYt7v4tKZMj5pk3vdeM0MJ5XZg7PvUsRzycxcnrT3Knoc3qM+ttqZfTsb8d6U/pQifq7GuahFOHrvroqgJLj+/81Yy1pZFkjfJgY0XklzAnA6CFq/YT0gxbV+TH5IS+WaYOUzK36S8s5qqMwhngv4IzE3ctXXqnhIZXNznyVBqbYa/Gx2bCjcEj2n5RyGYRS/+MculgLBznQ9MkG2D7QP3Mq/nwHXKMIaLS8VK8hG3d0rAjlvnauXShNrJxtkj4qkwnxD4LrEjk1ue+PLcbLqYWwAr8n0s+41jrsIPVyyK9vFaEZrA9KaBxkJyiqx50GZNn7ckx+mkI18gH52iyVPtU8FKMXjugCl6mPLiHYiDQd+G63LudmOKys9itVnBDBNMP2b/I7+bIVRlheGaZWr6MATKl86B3y+0kuOrLRMyWcdibc2iQkZm+8f7dTUwbKbBJsKOFY73/8nlL/JiYpjz6nUIk+IqSjaFyK6XFlCcukWpWHpDJnSvbEzey7qHyCqJ+I+on8iP8GOtempQaS0/4kZGSwlrE6xOj6EwGpfjbDZKHkP/2RAdNYPQq3z7bdVJMJNnFMz6e37mzMpCBihwPdSFTRE3SCY66wvTgJzGCUyhHbSi5i1azzOXd8uHCSpT/6L1o6VU0pjoR8Kaa4wCWHGf7P/WEd+avyp02Jnu1cmpW0+Gq+vK9h0tdb2IClM/eNfdouQP+ep8MJUjraDRICmAJQY5obS71vniOo+reaFy0zRgB', 'thinking': '', 'type': 'thinking'}, {'signature': 'CAQS/REKEQgSGAI4AUIJbmFycmF0aW9uEgwFh3AVJZP3s8y1Hq0aDG76uhHONaScXAGShiIwGgjLXHUx1yZrkLPljjzTOQycpLJiKVwXcxm/EzKJm1s8M5WxuREWEldXcoaN0/msKpkRMT+eLb5ih9rAHULTomUNsUwJeu0jrJe4GLazy7sP5KtjEOEBKQsXUfoQ7fDkZOQUHZi9zzQN4hFNmYJUf3OVqf9TQNdC1+m//HAZPfWmAy6HtDvm7kgBJkCEoiFnm7tn/PX1hXGPRgWc6BTXUMr2WA8aKyqggRUbMH2+JYv5iWZddCc1up1Nbr3sF7WPcot9ZBhyHQXr692zeJk6SJgygIVrASbMly2bBxTWJB623gVgzpCmVSLZFpkKXgPcp8hGyDKOvF6SiQSHqGRxQofr7Vs1aPhfZixROJGD+MlrkggVAnGMyYIDUZpavtCeiU1JmBSz0084bXPtT/tnpsKOnSPYZQo3B0BXKbqA/WsKng8/V3Z8Uuajo6IVZ2hgA1WDduqQOQkExGU6pFuLhebIjuWxSNI/DffvR//LNqCfvdnf0hvoPtDxfx/SawIx4/spq9xbwFzHtLgoHg1F2wFw7clPDlg1PyD/v2yJG3V9T8GL6jGPE6rhfil/u4HNvf7BW0dQzCTTXcqZy4nByy0+p+S7ZxfMl4Kpxa2gOhhSNjfpfuX0QpL4kME8J1nliFN1d8KqEPuDI18BQHUhD/UfY29oA0x510FjM/BsLZ4n5sppkoIi7CFoQq2aAKVNfC1KilhIpRD+1dyDtZQklN8gZpOESHw6ML1TtU0yfAcbmqoxJ6hhjzwH6CMsTQEEy8MbRBM+QKKkIRdRCPHGFW2LkuR50+J0pF/tr6pqVvJeozfiMnrEilB27TZP4nKTJG8rd//vCWen0y7CA/8dBJ9gdp5tx7NVA45k1gdcxER1gYouNX3n1vG3/B1F4+93wXW9Mqo6Us59TQmrb9Nqa7pSQNSrH06w1GxyBAlkFfqAlafk16aMpq416TJYpGy43WCUiCcLWlhiWgz2sgOLAXP9a5weWAKRC8J6jkcUkDeXnW+HFSiPM6+9Bo6kVhLUhN5rc4mHZOZpF7ptPpsQN4XCqLPsqpRH/8o5XpyC6ZneTeF2ExtjhwZVjXDwMbIzVYIbw1y2PPGw8dC7pc4sIVSnI4oQAH0v650ghfhmMevWW4h4Pylbw/DG4fdeT3+8z0f8MOSTtfipx05/QttHlAvODRxa5yDCvOz6n3f1BO0N/4MbGyOM27qW4x8mBVoryUZWfsmhfi0/GfNvB8+ylTGbYNX27OcEF5WZtNqbWEy7V5GbmZUSxspcg7FBZ3j3/3L7muvZXoZi1f5erq4y3lSH+oF7qPFnvieyLOCwubcQy+oyowj4cD0idRecnp+s+52D7+mzF1gpW+f+cJasoxigbxkOloS4yeiKvgM4nWSpGEj12RPx5o80EmziXN9t4zJzW1htA5hXJJeFympMYtyCUCevRWVmrRYktQ7UnCa6tL/AVPi1ni5XCXHfLOuqfK5KLYxjwVtdhhfmCLvpaKVNCEOCf4nNGHqD98wUb0ejWNwDYBnJGkBRw9RVspfwNdoWoVcn2qv8ywGUp7yc/sk2LpI6VziRyvWqbGNG1ZJqVVutRc46+sOE4VSeS2/CTNKNa6NMCf/hxxaUVsYfKdP+S1Lc4ianKp0sUugPpmtoydC0CP2RwjnHLcnzxCnvqxlLQWXMNPTsu2yBcHlWJLtcmdg/McLagy7l5kG6sN7ZxJLD/kAx8E6uqohP+K/jMSrYuQJnhr4tf4biWFY3Jzf/KpAYyVSq+pbfFKXMbL9nHEoZQT15Wyabd6DMAq3QZOthM5pUzdNqUXkcrI1PwjASAg4dVU/2o+98mTXnyhP7VlEkTnddpwyJF+YCCc01pqa6ZX99pnR+YOpIrb5DtPcKq0t5cFCDyQxN1lUuqZcYujvX/GC7+6IpRTz/NkEY/5cntOfZJyd6SkUx/JR5AJIRPCNeib0rcWnnFn0XK8s22x40wLjEtufXDO9+yraf4kHxlBI3SB0meMjepX50CJhAx7NekkRFnwoecLfqIyrjXrgAZsTMPiX94snjdrzisAXtBKanulilSxOfW2HSmfnk0L2GXHJJ3S+vCbgREml9dekfmHhmwEYtv3kfhth7ERIwVT8j69OtIBCmeTIJuA7o73PXc1LJ2whuZK7FZe6NTOs61MP7JVlIc6tbhtOBk3NXU8pBPOCY6Q55d63FtpUU/KCbQ0K2WQ9o5jNGwNNJ6Cw5FDCt+x5YJeP1d2QzDSiq+cSZqwqCZVcNoAftGGzgrRZ4vkpbP0xQZHCtWsUwHVTjrY4NfRDjHdVPRn8J9Xtooc/hIWkqzFaczYTlapu2RCJqe8aEo3jzWZCEJ7AiBzBYraYDgOfeiQyeJUsbbXHdGKiiw22Bdp4M+Yz671MZ0ie9EWRks0Q2DHGUTne9S4tAAJPGbUf4xtCMoJ40Q4QW05QLSZy3wNFAUFYmXE4ECDFB8+IEeNFFF/tgAnfh8GvFh/1xY+LE+ozMminjSRwhq09zQAEkNFyu0IKgvDkyOI+iccF7doWLTUasPxLH1xGPBSKkeWCnSoX/4C2c9du66BbRXamHqae+VCiS6BN953iYKOu8rMkneW+TFjV0CzAf8PNG9LBayUgY5Uw9x0Sap6zI4PpaZKH4d9ktkFU+91yBbN5SRtdd8/EQatJXmBNfw7kRrll+LjwZweWLEb0jT1RpGuDqHLdLTMMgowSczrp2zCsJdQH/seaYrpwkCFR8dRjM7JRUkVa1A0SvdZAX75vuBvvLBzN2c+Rbt5oqy93lqIBIjKb7w2UkRTb4pBWcNKNPUgt3az/PxQxEasaltdmuyvsXYbur3VPNn8JzN/6J5N+QCnz571TccapCmkPvFE+gayXU+UF1VKYyFcUvlev7lsQlmjqzK2QA8X7Q3TzFapqhvJKlt7dzBHraqZBgPgb9N4oKNayi8FZT71+kaYhIhFrESKYRHfHkj5n3kfhgLy+x+jLDbKxMggkgHnJz9RuEHZXBWtkYAQ==', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_verifier', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAMRdS21FeJfzQhM3s', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7396, 'output_tokens': 770, 'output_tokens_details': {'thinking_tokens': 739}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='skeptic', id='lc_run--01a0fe03-4ae9-76c0-89a6-4f0b114e3030-0', tool_calls=[{'name': 'transfer_to_verifier', 'args': {}, 'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7396, 'output_tokens': 770, 'total_tokens': 8166, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 739}}), ToolMessage(content='Successfully transferred to verifier', name='transfer_to_verifier', id='c064563a-974f-4f0f-ab4a-080e63c01cf5', tool_call_id='toolu_01H6fV8JvRJNvXy47eRnd3jH'), AIMessage(content=[{'signature': 'CAQS9gUKEAgSGAI4AUIIdGhpbmtpbmcSDGFyJ1MXoY+Ai6NkbxoMRd2TrUfUU7WrPpA2IjDgy8dUplcZKc7DnJwNLTBfH5UNR+nGnakSPQia8UVIzrjkOnJ2fLrWUsA2oEO8yRsqkwXkkDCsyhB97dwj57RMt91XzgWhL9luNEGO92yW0rNFGoMSUVOxPIrFc5RjyrhKEc3+1RnpxCkylq+LNlQ5NFxLn6v2cF5DzqpvBHNR7XAig+urJIU3gXydCkaGZZDetJtQOUOXgZi9z9100NpdAEkXDo2ICHCGTGpVc0J/pbXxWRBxb6B1/Gl0Yjn68V6HlesH7IoC7GR/3HwRu9NSDBG4ttfQjEWcg+PeAea4Rn7jzx+Tm9H7L1joTKVgDbQg5iKMLfjvPqlnksCLtXOt8jSIS5OYLN7a/7TX6/w5d34vnw8vPDmU1UYi0Q72qUWhUa2vw8OXNpcKL7Bp0lNfBS8rvHY+vBY8/haAoTL3wvLFU1fT+4z/vA3BcsigZDYlfJeDYcxXmRuqxzxEGpXCRe9s1HWtID+41pHu6lpZHkXaghpc6TZHLxNwcVhTb6fMQ7Qo+2L6Pvklh1m32peodxeg2YiPhdpl3w/xkOEu5/JEF7ZBxpdUL+u3wkO5pUlsqfgX7uhzv7HC2WpSWVimrwiQwQOUx0+JPrLtw+lYfq7I3jruo0GW0guWvnuf7JM3YLj84q+JgHomnSSZnnZmrL0UaRvrVyLo877F39ccLS7XoZnnui8J4P58ZTmiLkEJeGcx1Iuag2e33339O7Yew/59tLbQtIGh9VWMwHBnFx39S8/4LEFrK5HldPQhOrloyTtkRs66iQRKtxMKS0VI6QTKZDAbs7a7H1yBv+mJi9Xg4uKmyU6qnn7zEKjjY2bIlCwezx4LLGP5HD3Zr6fZsmdG1UHzDsBvYG2oCI0oqEKM+valsT6hU8MRjbQBD/JJ8JJN/iBGlhBAUHo0UmhhtR2euTVpZWlMnhmpXAdKDXSfPdaPohgB', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_red_team', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAsabDquegHi6YUpCn', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7509, 'output_tokens': 88, 'output_tokens_details': {'thinking_tokens': 55}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='verifier', id='lc_run--01a0fe03-6682-7d21-8c15-ffbce9422c9f-0', tool_calls=[{'name': 'transfer_to_red_team', 'args': {}, 'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7509, 'output_tokens': 88, 'total_tokens': 7597, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 55}}), ToolMessage(content='Successfully transferred to red_team', name='transfer_to_red_team', tool_call_id='toolu_01XEAuhQHCBdyCwtymZwFfDi')], 'active_agent': 'red_team'}, goto='red_team'))Traceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 2967, in stream\n for _ in runner.tick(\n ~~~~~~~~~~~^\n [t for t in loop.tasks.values() if not t.writes],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<2 lines>...\n schedule_task=loop.accept_push,\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ):\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_runner.py\", line 207, in tick\n run_with_retry(\n ~~~~~~~~~~~~~~^\n t,\n ^^\n ...<10 lines>...\n },\n ^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_retry.py\", line 617, in run_with_retry\n return task.proc.invoke(task.input, config)\n ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 709, in invoke\n input = step.invoke(input, config)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 447, in invoke\n ret = self.func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 84, in _write\n self.do_write(\n ~~~~~~~~~~~~~^\n config,\n ^^^^^^^\n writes,\n ^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 126, in do_write\n write(_assemble_writes(writes))\n ~~~~~~~~~~~~~~~~^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 181, in _assemble_writes\n if ww := w.mapper(w.value):\n ~~~~~~~~^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/graph/state.py\", line 1762, in _control_branch\n raise ParentCommand(command)\n\n\nlanggraph.errors.ParentCommand: Command(graph='verifier:544dea40-e16b-458c-1464-128e1b9b10a9', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', id='389058d1-083f-4632-bbc5-09bfd3a0ffd2', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9'), AIMessage(content=[{'signature': 'CAQS1QgKEAgSGAI4AUIIdGhpbmtpbmcSDN6TfU6NDM971lEbzxoMvRUA1J9ox3fJQYT9IjArCQQGHBP4pRHPavXzRlbMbaYHC57Yj+Gbxgr0helvd2OIL/o6Rbt4PFA3FaIFUlgq8gc7KQI9FtGnvSAkLBRzIsUp/2d5Rd2zgEsfkFVeXsn8WYYZkinAQVVy6A7B3LaEJW0xb/x88Di67fedwx0Q+FlZtwcmT3Dcaa11JveyHRr3dm+zGSZP6CXeXNJ8nrTZ7W1YRTysD6w7o7Lp8h1Rwq4f4o8j+QUfY0vje+zJZsxHPmQNt3ykeKh99hgn6dbiz6/EHqmbHjSAu1M4SqEDfqIKThHYenamAZmcQ4XAOo6mdQ6+77VgpV1lC1/k52IiUoJjkGlQrnLCvutiI7KN+sgK8c2+2KCJrPRCvVV+BkStvP5jWWqzytpKQ+nHzhL448OrIA9gdJC39C8eDrXeRnI7t4FqlkXGoMlZAV9UMcrQHRxOMi8tOyTzi9oyBFDTSsQ+LJqer0tz1Ft9gek6vzg50SpVAFQdLERv8R1YqMaeI+xsvTAEW9MRgvRMkzLuzRT2guw3jD+Fjip37xgK5QI0R+CsOAmQPoTZizzcIyAUAuCJFCgq/gD/CibLxoymiVUI1an9Snt3KpNRoWdvWIne33V0pv8PHg9yW2JuLOqQGilLCsK/Q3oQr3upEMXxXGjCL7WUizvRZGFEEV+VplYt7v4tKZMj5pk3vdeM0MJ5XZg7PvUsRzycxcnrT3Knoc3qM+ttqZfTsb8d6U/pQifq7GuahFOHrvroqgJLj+/81Yy1pZFkjfJgY0XklzAnA6CFq/YT0gxbV+TH5IS+WaYOUzK36S8s5qqMwhngv4IzE3ctXXqnhIZXNznyVBqbYa/Gx2bCjcEj2n5RyGYRS/+MculgLBznQ9MkG2D7QP3Mq/nwHXKMIaLS8VK8hG3d0rAjlvnauXShNrJxtkj4qkwnxD4LrEjk1ue+PLcbLqYWwAr8n0s+41jrsIPVyyK9vFaEZrA9KaBxkJyiqx50GZNn7ckx+mkI18gH52iyVPtU8FKMXjugCl6mPLiHYiDQd+G63LudmOKys9itVnBDBNMP2b/I7+bIVRlheGaZWr6MATKl86B3y+0kuOrLRMyWcdibc2iQkZm+8f7dTUwbKbBJsKOFY73/8nlL/JiYpjz6nUIk+IqSjaFyK6XFlCcukWpWHpDJnSvbEzey7qHyCqJ+I+on8iP8GOtempQaS0/4kZGSwlrE6xOj6EwGpfjbDZKHkP/2RAdNYPQq3z7bdVJMJNnFMz6e37mzMpCBihwPdSFTRE3SCY66wvTgJzGCUyhHbSi5i1azzOXd8uHCSpT/6L1o6VU0pjoR8Kaa4wCWHGf7P/WEd+avyp02Jnu1cmpW0+Gq+vK9h0tdb2IClM/eNfdouQP+ep8MJUjraDRICmAJQY5obS71vniOo+reaFy0zRgB', 'thinking': '', 'type': 'thinking'}, {'signature': 'CAQS/REKEQgSGAI4AUIJbmFycmF0aW9uEgwFh3AVJZP3s8y1Hq0aDG76uhHONaScXAGShiIwGgjLXHUx1yZrkLPljjzTOQycpLJiKVwXcxm/EzKJm1s8M5WxuREWEldXcoaN0/msKpkRMT+eLb5ih9rAHULTomUNsUwJeu0jrJe4GLazy7sP5KtjEOEBKQsXUfoQ7fDkZOQUHZi9zzQN4hFNmYJUf3OVqf9TQNdC1+m//HAZPfWmAy6HtDvm7kgBJkCEoiFnm7tn/PX1hXGPRgWc6BTXUMr2WA8aKyqggRUbMH2+JYv5iWZddCc1up1Nbr3sF7WPcot9ZBhyHQXr692zeJk6SJgygIVrASbMly2bBxTWJB623gVgzpCmVSLZFpkKXgPcp8hGyDKOvF6SiQSHqGRxQofr7Vs1aPhfZixROJGD+MlrkggVAnGMyYIDUZpavtCeiU1JmBSz0084bXPtT/tnpsKOnSPYZQo3B0BXKbqA/WsKng8/V3Z8Uuajo6IVZ2hgA1WDduqQOQkExGU6pFuLhebIjuWxSNI/DffvR//LNqCfvdnf0hvoPtDxfx/SawIx4/spq9xbwFzHtLgoHg1F2wFw7clPDlg1PyD/v2yJG3V9T8GL6jGPE6rhfil/u4HNvf7BW0dQzCTTXcqZy4nByy0+p+S7ZxfMl4Kpxa2gOhhSNjfpfuX0QpL4kME8J1nliFN1d8KqEPuDI18BQHUhD/UfY29oA0x510FjM/BsLZ4n5sppkoIi7CFoQq2aAKVNfC1KilhIpRD+1dyDtZQklN8gZpOESHw6ML1TtU0yfAcbmqoxJ6hhjzwH6CMsTQEEy8MbRBM+QKKkIRdRCPHGFW2LkuR50+J0pF/tr6pqVvJeozfiMnrEilB27TZP4nKTJG8rd//vCWen0y7CA/8dBJ9gdp5tx7NVA45k1gdcxER1gYouNX3n1vG3/B1F4+93wXW9Mqo6Us59TQmrb9Nqa7pSQNSrH06w1GxyBAlkFfqAlafk16aMpq416TJYpGy43WCUiCcLWlhiWgz2sgOLAXP9a5weWAKRC8J6jkcUkDeXnW+HFSiPM6+9Bo6kVhLUhN5rc4mHZOZpF7ptPpsQN4XCqLPsqpRH/8o5XpyC6ZneTeF2ExtjhwZVjXDwMbIzVYIbw1y2PPGw8dC7pc4sIVSnI4oQAH0v650ghfhmMevWW4h4Pylbw/DG4fdeT3+8z0f8MOSTtfipx05/QttHlAvODRxa5yDCvOz6n3f1BO0N/4MbGyOM27qW4x8mBVoryUZWfsmhfi0/GfNvB8+ylTGbYNX27OcEF5WZtNqbWEy7V5GbmZUSxspcg7FBZ3j3/3L7muvZXoZi1f5erq4y3lSH+oF7qPFnvieyLOCwubcQy+oyowj4cD0idRecnp+s+52D7+mzF1gpW+f+cJasoxigbxkOloS4yeiKvgM4nWSpGEj12RPx5o80EmziXN9t4zJzW1htA5hXJJeFympMYtyCUCevRWVmrRYktQ7UnCa6tL/AVPi1ni5XCXHfLOuqfK5KLYxjwVtdhhfmCLvpaKVNCEOCf4nNGHqD98wUb0ejWNwDYBnJGkBRw9RVspfwNdoWoVcn2qv8ywGUp7yc/sk2LpI6VziRyvWqbGNG1ZJqVVutRc46+sOE4VSeS2/CTNKNa6NMCf/hxxaUVsYfKdP+S1Lc4ianKp0sUugPpmtoydC0CP2RwjnHLcnzxCnvqxlLQWXMNPTsu2yBcHlWJLtcmdg/McLagy7l5kG6sN7ZxJLD/kAx8E6uqohP+K/jMSrYuQJnhr4tf4biWFY3Jzf/KpAYyVSq+pbfFKXMbL9nHEoZQT15Wyabd6DMAq3QZOthM5pUzdNqUXkcrI1PwjASAg4dVU/2o+98mTXnyhP7VlEkTnddpwyJF+YCCc01pqa6ZX99pnR+YOpIrb5DtPcKq0t5cFCDyQxN1lUuqZcYujvX/GC7+6IpRTz/NkEY/5cntOfZJyd6SkUx/JR5AJIRPCNeib0rcWnnFn0XK8s22x40wLjEtufXDO9+yraf4kHxlBI3SB0meMjepX50CJhAx7NekkRFnwoecLfqIyrjXrgAZsTMPiX94snjdrzisAXtBKanulilSxOfW2HSmfnk0L2GXHJJ3S+vCbgREml9dekfmHhmwEYtv3kfhth7ERIwVT8j69OtIBCmeTIJuA7o73PXc1LJ2whuZK7FZe6NTOs61MP7JVlIc6tbhtOBk3NXU8pBPOCY6Q55d63FtpUU/KCbQ0K2WQ9o5jNGwNNJ6Cw5FDCt+x5YJeP1d2QzDSiq+cSZqwqCZVcNoAftGGzgrRZ4vkpbP0xQZHCtWsUwHVTjrY4NfRDjHdVPRn8J9Xtooc/hIWkqzFaczYTlapu2RCJqe8aEo3jzWZCEJ7AiBzBYraYDgOfeiQyeJUsbbXHdGKiiw22Bdp4M+Yz671MZ0ie9EWRks0Q2DHGUTne9S4tAAJPGbUf4xtCMoJ40Q4QW05QLSZy3wNFAUFYmXE4ECDFB8+IEeNFFF/tgAnfh8GvFh/1xY+LE+ozMminjSRwhq09zQAEkNFyu0IKgvDkyOI+iccF7doWLTUasPxLH1xGPBSKkeWCnSoX/4C2c9du66BbRXamHqae+VCiS6BN953iYKOu8rMkneW+TFjV0CzAf8PNG9LBayUgY5Uw9x0Sap6zI4PpaZKH4d9ktkFU+91yBbN5SRtdd8/EQatJXmBNfw7kRrll+LjwZweWLEb0jT1RpGuDqHLdLTMMgowSczrp2zCsJdQH/seaYrpwkCFR8dRjM7JRUkVa1A0SvdZAX75vuBvvLBzN2c+Rbt5oqy93lqIBIjKb7w2UkRTb4pBWcNKNPUgt3az/PxQxEasaltdmuyvsXYbur3VPNn8JzN/6J5N+QCnz571TccapCmkPvFE+gayXU+UF1VKYyFcUvlev7lsQlmjqzK2QA8X7Q3TzFapqhvJKlt7dzBHraqZBgPgb9N4oKNayi8FZT71+kaYhIhFrESKYRHfHkj5n3kfhgLy+x+jLDbKxMggkgHnJz9RuEHZXBWtkYAQ==', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_verifier', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAMRdS21FeJfzQhM3s', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7396, 'output_tokens': 770, 'output_tokens_details': {'thinking_tokens': 739}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='skeptic', id='lc_run--01a0fe03-4ae9-76c0-89a6-4f0b114e3030-0', tool_calls=[{'name': 'transfer_to_verifier', 'args': {}, 'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7396, 'output_tokens': 770, 'total_tokens': 8166, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 739}}), ToolMessage(content='Successfully transferred to verifier', name='transfer_to_verifier', id='c064563a-974f-4f0f-ab4a-080e63c01cf5', tool_call_id='toolu_01H6fV8JvRJNvXy47eRnd3jH'), AIMessage(content=[{'signature': 'CAQS9gUKEAgSGAI4AUIIdGhpbmtpbmcSDGFyJ1MXoY+Ai6NkbxoMRd2TrUfUU7WrPpA2IjDgy8dUplcZKc7DnJwNLTBfH5UNR+nGnakSPQia8UVIzrjkOnJ2fLrWUsA2oEO8yRsqkwXkkDCsyhB97dwj57RMt91XzgWhL9luNEGO92yW0rNFGoMSUVOxPIrFc5RjyrhKEc3+1RnpxCkylq+LNlQ5NFxLn6v2cF5DzqpvBHNR7XAig+urJIU3gXydCkaGZZDetJtQOUOXgZi9z9100NpdAEkXDo2ICHCGTGpVc0J/pbXxWRBxb6B1/Gl0Yjn68V6HlesH7IoC7GR/3HwRu9NSDBG4ttfQjEWcg+PeAea4Rn7jzx+Tm9H7L1joTKVgDbQg5iKMLfjvPqlnksCLtXOt8jSIS5OYLN7a/7TX6/w5d34vnw8vPDmU1UYi0Q72qUWhUa2vw8OXNpcKL7Bp0lNfBS8rvHY+vBY8/haAoTL3wvLFU1fT+4z/vA3BcsigZDYlfJeDYcxXmRuqxzxEGpXCRe9s1HWtID+41pHu6lpZHkXaghpc6TZHLxNwcVhTb6fMQ7Qo+2L6Pvklh1m32peodxeg2YiPhdpl3w/xkOEu5/JEF7ZBxpdUL+u3wkO5pUlsqfgX7uhzv7HC2WpSWVimrwiQwQOUx0+JPrLtw+lYfq7I3jruo0GW0guWvnuf7JM3YLj84q+JgHomnSSZnnZmrL0UaRvrVyLo877F39ccLS7XoZnnui8J4P58ZTmiLkEJeGcx1Iuag2e33339O7Yew/59tLbQtIGh9VWMwHBnFx39S8/4LEFrK5HldPQhOrloyTtkRs66iQRKtxMKS0VI6QTKZDAbs7a7H1yBv+mJi9Xg4uKmyU6qnn7zEKjjY2bIlCwezx4LLGP5HD3Zr6fZsmdG1UHzDsBvYG2oCI0oqEKM+valsT6hU8MRjbQBD/JJ8JJN/iBGlhBAUHo0UmhhtR2euTVpZWlMnhmpXAdKDXSfPdaPohgB', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_red_team', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAsabDquegHi6YUpCn', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7509, 'output_tokens': 88, 'output_tokens_details': {'thinking_tokens': 55}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='verifier', id='lc_run--01a0fe03-6682-7d21-8c15-ffbce9422c9f-0', tool_calls=[{'name': 'transfer_to_red_team', 'args': {}, 'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7509, 'output_tokens': 88, 'total_tokens': 7597, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 55}}), ToolMessage(content='Successfully transferred to red_team', name='transfer_to_red_team', tool_call_id='toolu_01XEAuhQHCBdyCwtymZwFfDi')], 'active_agent': 'red_team'}, goto='red_team')" + } + }, + { + "key": "exception.stacktrace", + "value": { + "stringValue": "Exception: ParentCommand(Command(graph='verifier:544dea40-e16b-458c-1464-128e1b9b10a9', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', id='389058d1-083f-4632-bbc5-09bfd3a0ffd2', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9'), AIMessage(content=[{'signature': 'CAQS1QgKEAgSGAI4AUIIdGhpbmtpbmcSDN6TfU6NDM971lEbzxoMvRUA1J9ox3fJQYT9IjArCQQGHBP4pRHPavXzRlbMbaYHC57Yj+Gbxgr0helvd2OIL/o6Rbt4PFA3FaIFUlgq8gc7KQI9FtGnvSAkLBRzIsUp/2d5Rd2zgEsfkFVeXsn8WYYZkinAQVVy6A7B3LaEJW0xb/x88Di67fedwx0Q+FlZtwcmT3Dcaa11JveyHRr3dm+zGSZP6CXeXNJ8nrTZ7W1YRTysD6w7o7Lp8h1Rwq4f4o8j+QUfY0vje+zJZsxHPmQNt3ykeKh99hgn6dbiz6/EHqmbHjSAu1M4SqEDfqIKThHYenamAZmcQ4XAOo6mdQ6+77VgpV1lC1/k52IiUoJjkGlQrnLCvutiI7KN+sgK8c2+2KCJrPRCvVV+BkStvP5jWWqzytpKQ+nHzhL448OrIA9gdJC39C8eDrXeRnI7t4FqlkXGoMlZAV9UMcrQHRxOMi8tOyTzi9oyBFDTSsQ+LJqer0tz1Ft9gek6vzg50SpVAFQdLERv8R1YqMaeI+xsvTAEW9MRgvRMkzLuzRT2guw3jD+Fjip37xgK5QI0R+CsOAmQPoTZizzcIyAUAuCJFCgq/gD/CibLxoymiVUI1an9Snt3KpNRoWdvWIne33V0pv8PHg9yW2JuLOqQGilLCsK/Q3oQr3upEMXxXGjCL7WUizvRZGFEEV+VplYt7v4tKZMj5pk3vdeM0MJ5XZg7PvUsRzycxcnrT3Knoc3qM+ttqZfTsb8d6U/pQifq7GuahFOHrvroqgJLj+/81Yy1pZFkjfJgY0XklzAnA6CFq/YT0gxbV+TH5IS+WaYOUzK36S8s5qqMwhngv4IzE3ctXXqnhIZXNznyVBqbYa/Gx2bCjcEj2n5RyGYRS/+MculgLBznQ9MkG2D7QP3Mq/nwHXKMIaLS8VK8hG3d0rAjlvnauXShNrJxtkj4qkwnxD4LrEjk1ue+PLcbLqYWwAr8n0s+41jrsIPVyyK9vFaEZrA9KaBxkJyiqx50GZNn7ckx+mkI18gH52iyVPtU8FKMXjugCl6mPLiHYiDQd+G63LudmOKys9itVnBDBNMP2b/I7+bIVRlheGaZWr6MATKl86B3y+0kuOrLRMyWcdibc2iQkZm+8f7dTUwbKbBJsKOFY73/8nlL/JiYpjz6nUIk+IqSjaFyK6XFlCcukWpWHpDJnSvbEzey7qHyCqJ+I+on8iP8GOtempQaS0/4kZGSwlrE6xOj6EwGpfjbDZKHkP/2RAdNYPQq3z7bdVJMJNnFMz6e37mzMpCBihwPdSFTRE3SCY66wvTgJzGCUyhHbSi5i1azzOXd8uHCSpT/6L1o6VU0pjoR8Kaa4wCWHGf7P/WEd+avyp02Jnu1cmpW0+Gq+vK9h0tdb2IClM/eNfdouQP+ep8MJUjraDRICmAJQY5obS71vniOo+reaFy0zRgB', 'thinking': '', 'type': 'thinking'}, {'signature': 'CAQS/REKEQgSGAI4AUIJbmFycmF0aW9uEgwFh3AVJZP3s8y1Hq0aDG76uhHONaScXAGShiIwGgjLXHUx1yZrkLPljjzTOQycpLJiKVwXcxm/EzKJm1s8M5WxuREWEldXcoaN0/msKpkRMT+eLb5ih9rAHULTomUNsUwJeu0jrJe4GLazy7sP5KtjEOEBKQsXUfoQ7fDkZOQUHZi9zzQN4hFNmYJUf3OVqf9TQNdC1+m//HAZPfWmAy6HtDvm7kgBJkCEoiFnm7tn/PX1hXGPRgWc6BTXUMr2WA8aKyqggRUbMH2+JYv5iWZddCc1up1Nbr3sF7WPcot9ZBhyHQXr692zeJk6SJgygIVrASbMly2bBxTWJB623gVgzpCmVSLZFpkKXgPcp8hGyDKOvF6SiQSHqGRxQofr7Vs1aPhfZixROJGD+MlrkggVAnGMyYIDUZpavtCeiU1JmBSz0084bXPtT/tnpsKOnSPYZQo3B0BXKbqA/WsKng8/V3Z8Uuajo6IVZ2hgA1WDduqQOQkExGU6pFuLhebIjuWxSNI/DffvR//LNqCfvdnf0hvoPtDxfx/SawIx4/spq9xbwFzHtLgoHg1F2wFw7clPDlg1PyD/v2yJG3V9T8GL6jGPE6rhfil/u4HNvf7BW0dQzCTTXcqZy4nByy0+p+S7ZxfMl4Kpxa2gOhhSNjfpfuX0QpL4kME8J1nliFN1d8KqEPuDI18BQHUhD/UfY29oA0x510FjM/BsLZ4n5sppkoIi7CFoQq2aAKVNfC1KilhIpRD+1dyDtZQklN8gZpOESHw6ML1TtU0yfAcbmqoxJ6hhjzwH6CMsTQEEy8MbRBM+QKKkIRdRCPHGFW2LkuR50+J0pF/tr6pqVvJeozfiMnrEilB27TZP4nKTJG8rd//vCWen0y7CA/8dBJ9gdp5tx7NVA45k1gdcxER1gYouNX3n1vG3/B1F4+93wXW9Mqo6Us59TQmrb9Nqa7pSQNSrH06w1GxyBAlkFfqAlafk16aMpq416TJYpGy43WCUiCcLWlhiWgz2sgOLAXP9a5weWAKRC8J6jkcUkDeXnW+HFSiPM6+9Bo6kVhLUhN5rc4mHZOZpF7ptPpsQN4XCqLPsqpRH/8o5XpyC6ZneTeF2ExtjhwZVjXDwMbIzVYIbw1y2PPGw8dC7pc4sIVSnI4oQAH0v650ghfhmMevWW4h4Pylbw/DG4fdeT3+8z0f8MOSTtfipx05/QttHlAvODRxa5yDCvOz6n3f1BO0N/4MbGyOM27qW4x8mBVoryUZWfsmhfi0/GfNvB8+ylTGbYNX27OcEF5WZtNqbWEy7V5GbmZUSxspcg7FBZ3j3/3L7muvZXoZi1f5erq4y3lSH+oF7qPFnvieyLOCwubcQy+oyowj4cD0idRecnp+s+52D7+mzF1gpW+f+cJasoxigbxkOloS4yeiKvgM4nWSpGEj12RPx5o80EmziXN9t4zJzW1htA5hXJJeFympMYtyCUCevRWVmrRYktQ7UnCa6tL/AVPi1ni5XCXHfLOuqfK5KLYxjwVtdhhfmCLvpaKVNCEOCf4nNGHqD98wUb0ejWNwDYBnJGkBRw9RVspfwNdoWoVcn2qv8ywGUp7yc/sk2LpI6VziRyvWqbGNG1ZJqVVutRc46+sOE4VSeS2/CTNKNa6NMCf/hxxaUVsYfKdP+S1Lc4ianKp0sUugPpmtoydC0CP2RwjnHLcnzxCnvqxlLQWXMNPTsu2yBcHlWJLtcmdg/McLagy7l5kG6sN7ZxJLD/kAx8E6uqohP+K/jMSrYuQJnhr4tf4biWFY3Jzf/KpAYyVSq+pbfFKXMbL9nHEoZQT15Wyabd6DMAq3QZOthM5pUzdNqUXkcrI1PwjASAg4dVU/2o+98mTXnyhP7VlEkTnddpwyJF+YCCc01pqa6ZX99pnR+YOpIrb5DtPcKq0t5cFCDyQxN1lUuqZcYujvX/GC7+6IpRTz/NkEY/5cntOfZJyd6SkUx/JR5AJIRPCNeib0rcWnnFn0XK8s22x40wLjEtufXDO9+yraf4kHxlBI3SB0meMjepX50CJhAx7NekkRFnwoecLfqIyrjXrgAZsTMPiX94snjdrzisAXtBKanulilSxOfW2HSmfnk0L2GXHJJ3S+vCbgREml9dekfmHhmwEYtv3kfhth7ERIwVT8j69OtIBCmeTIJuA7o73PXc1LJ2whuZK7FZe6NTOs61MP7JVlIc6tbhtOBk3NXU8pBPOCY6Q55d63FtpUU/KCbQ0K2WQ9o5jNGwNNJ6Cw5FDCt+x5YJeP1d2QzDSiq+cSZqwqCZVcNoAftGGzgrRZ4vkpbP0xQZHCtWsUwHVTjrY4NfRDjHdVPRn8J9Xtooc/hIWkqzFaczYTlapu2RCJqe8aEo3jzWZCEJ7AiBzBYraYDgOfeiQyeJUsbbXHdGKiiw22Bdp4M+Yz671MZ0ie9EWRks0Q2DHGUTne9S4tAAJPGbUf4xtCMoJ40Q4QW05QLSZy3wNFAUFYmXE4ECDFB8+IEeNFFF/tgAnfh8GvFh/1xY+LE+ozMminjSRwhq09zQAEkNFyu0IKgvDkyOI+iccF7doWLTUasPxLH1xGPBSKkeWCnSoX/4C2c9du66BbRXamHqae+VCiS6BN953iYKOu8rMkneW+TFjV0CzAf8PNG9LBayUgY5Uw9x0Sap6zI4PpaZKH4d9ktkFU+91yBbN5SRtdd8/EQatJXmBNfw7kRrll+LjwZweWLEb0jT1RpGuDqHLdLTMMgowSczrp2zCsJdQH/seaYrpwkCFR8dRjM7JRUkVa1A0SvdZAX75vuBvvLBzN2c+Rbt5oqy93lqIBIjKb7w2UkRTb4pBWcNKNPUgt3az/PxQxEasaltdmuyvsXYbur3VPNn8JzN/6J5N+QCnz571TccapCmkPvFE+gayXU+UF1VKYyFcUvlev7lsQlmjqzK2QA8X7Q3TzFapqhvJKlt7dzBHraqZBgPgb9N4oKNayi8FZT71+kaYhIhFrESKYRHfHkj5n3kfhgLy+x+jLDbKxMggkgHnJz9RuEHZXBWtkYAQ==', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_verifier', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAMRdS21FeJfzQhM3s', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7396, 'output_tokens': 770, 'output_tokens_details': {'thinking_tokens': 739}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='skeptic', id='lc_run--01a0fe03-4ae9-76c0-89a6-4f0b114e3030-0', tool_calls=[{'name': 'transfer_to_verifier', 'args': {}, 'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7396, 'output_tokens': 770, 'total_tokens': 8166, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 739}}), ToolMessage(content='Successfully transferred to verifier', name='transfer_to_verifier', id='c064563a-974f-4f0f-ab4a-080e63c01cf5', tool_call_id='toolu_01H6fV8JvRJNvXy47eRnd3jH'), AIMessage(content=[{'signature': 'CAQS9gUKEAgSGAI4AUIIdGhpbmtpbmcSDGFyJ1MXoY+Ai6NkbxoMRd2TrUfUU7WrPpA2IjDgy8dUplcZKc7DnJwNLTBfH5UNR+nGnakSPQia8UVIzrjkOnJ2fLrWUsA2oEO8yRsqkwXkkDCsyhB97dwj57RMt91XzgWhL9luNEGO92yW0rNFGoMSUVOxPIrFc5RjyrhKEc3+1RnpxCkylq+LNlQ5NFxLn6v2cF5DzqpvBHNR7XAig+urJIU3gXydCkaGZZDetJtQOUOXgZi9z9100NpdAEkXDo2ICHCGTGpVc0J/pbXxWRBxb6B1/Gl0Yjn68V6HlesH7IoC7GR/3HwRu9NSDBG4ttfQjEWcg+PeAea4Rn7jzx+Tm9H7L1joTKVgDbQg5iKMLfjvPqlnksCLtXOt8jSIS5OYLN7a/7TX6/w5d34vnw8vPDmU1UYi0Q72qUWhUa2vw8OXNpcKL7Bp0lNfBS8rvHY+vBY8/haAoTL3wvLFU1fT+4z/vA3BcsigZDYlfJeDYcxXmRuqxzxEGpXCRe9s1HWtID+41pHu6lpZHkXaghpc6TZHLxNwcVhTb6fMQ7Qo+2L6Pvklh1m32peodxeg2YiPhdpl3w/xkOEu5/JEF7ZBxpdUL+u3wkO5pUlsqfgX7uhzv7HC2WpSWVimrwiQwQOUx0+JPrLtw+lYfq7I3jruo0GW0guWvnuf7JM3YLj84q+JgHomnSSZnnZmrL0UaRvrVyLo877F39ccLS7XoZnnui8J4P58ZTmiLkEJeGcx1Iuag2e33339O7Yew/59tLbQtIGh9VWMwHBnFx39S8/4LEFrK5HldPQhOrloyTtkRs66iQRKtxMKS0VI6QTKZDAbs7a7H1yBv+mJi9Xg4uKmyU6qnn7zEKjjY2bIlCwezx4LLGP5HD3Zr6fZsmdG1UHzDsBvYG2oCI0oqEKM+valsT6hU8MRjbQBD/JJ8JJN/iBGlhBAUHo0UmhhtR2euTVpZWlMnhmpXAdKDXSfPdaPohgB', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_red_team', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAsabDquegHi6YUpCn', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7509, 'output_tokens': 88, 'output_tokens_details': {'thinking_tokens': 55}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='verifier', id='lc_run--01a0fe03-6682-7d21-8c15-ffbce9422c9f-0', tool_calls=[{'name': 'transfer_to_red_team', 'args': {}, 'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7509, 'output_tokens': 88, 'total_tokens': 7597, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 55}}), ToolMessage(content='Successfully transferred to red_team', name='transfer_to_red_team', tool_call_id='toolu_01XEAuhQHCBdyCwtymZwFfDi')], 'active_agent': 'red_team'}, goto='red_team'))Traceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 2967, in stream\n for _ in runner.tick(\n ~~~~~~~~~~~^\n [t for t in loop.tasks.values() if not t.writes],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<2 lines>...\n schedule_task=loop.accept_push,\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ):\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_runner.py\", line 207, in tick\n run_with_retry(\n ~~~~~~~~~~~~~~^\n t,\n ^^\n ...<10 lines>...\n },\n ^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_retry.py\", line 617, in run_with_retry\n return task.proc.invoke(task.input, config)\n ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 709, in invoke\n input = step.invoke(input, config)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 447, in invoke\n ret = self.func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 84, in _write\n self.do_write(\n ~~~~~~~~~~~~~^\n config,\n ^^^^^^^\n writes,\n ^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 126, in do_write\n write(_assemble_writes(writes))\n ~~~~~~~~~~~~~~~~^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 181, in _assemble_writes\n if ww := w.mapper(w.value):\n ~~~~~~~~^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/graph/state.py\", line 1762, in _control_branch\n raise ParentCommand(command)\n\n\nlanggraph.errors.ParentCommand: Command(graph='verifier:544dea40-e16b-458c-1464-128e1b9b10a9', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', id='389058d1-083f-4632-bbc5-09bfd3a0ffd2', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9'), AIMessage(content=[{'signature': 'CAQS1QgKEAgSGAI4AUIIdGhpbmtpbmcSDN6TfU6NDM971lEbzxoMvRUA1J9ox3fJQYT9IjArCQQGHBP4pRHPavXzRlbMbaYHC57Yj+Gbxgr0helvd2OIL/o6Rbt4PFA3FaIFUlgq8gc7KQI9FtGnvSAkLBRzIsUp/2d5Rd2zgEsfkFVeXsn8WYYZkinAQVVy6A7B3LaEJW0xb/x88Di67fedwx0Q+FlZtwcmT3Dcaa11JveyHRr3dm+zGSZP6CXeXNJ8nrTZ7W1YRTysD6w7o7Lp8h1Rwq4f4o8j+QUfY0vje+zJZsxHPmQNt3ykeKh99hgn6dbiz6/EHqmbHjSAu1M4SqEDfqIKThHYenamAZmcQ4XAOo6mdQ6+77VgpV1lC1/k52IiUoJjkGlQrnLCvutiI7KN+sgK8c2+2KCJrPRCvVV+BkStvP5jWWqzytpKQ+nHzhL448OrIA9gdJC39C8eDrXeRnI7t4FqlkXGoMlZAV9UMcrQHRxOMi8tOyTzi9oyBFDTSsQ+LJqer0tz1Ft9gek6vzg50SpVAFQdLERv8R1YqMaeI+xsvTAEW9MRgvRMkzLuzRT2guw3jD+Fjip37xgK5QI0R+CsOAmQPoTZizzcIyAUAuCJFCgq/gD/CibLxoymiVUI1an9Snt3KpNRoWdvWIne33V0pv8PHg9yW2JuLOqQGilLCsK/Q3oQr3upEMXxXGjCL7WUizvRZGFEEV+VplYt7v4tKZMj5pk3vdeM0MJ5XZg7PvUsRzycxcnrT3Knoc3qM+ttqZfTsb8d6U/pQifq7GuahFOHrvroqgJLj+/81Yy1pZFkjfJgY0XklzAnA6CFq/YT0gxbV+TH5IS+WaYOUzK36S8s5qqMwhngv4IzE3ctXXqnhIZXNznyVBqbYa/Gx2bCjcEj2n5RyGYRS/+MculgLBznQ9MkG2D7QP3Mq/nwHXKMIaLS8VK8hG3d0rAjlvnauXShNrJxtkj4qkwnxD4LrEjk1ue+PLcbLqYWwAr8n0s+41jrsIPVyyK9vFaEZrA9KaBxkJyiqx50GZNn7ckx+mkI18gH52iyVPtU8FKMXjugCl6mPLiHYiDQd+G63LudmOKys9itVnBDBNMP2b/I7+bIVRlheGaZWr6MATKl86B3y+0kuOrLRMyWcdibc2iQkZm+8f7dTUwbKbBJsKOFY73/8nlL/JiYpjz6nUIk+IqSjaFyK6XFlCcukWpWHpDJnSvbEzey7qHyCqJ+I+on8iP8GOtempQaS0/4kZGSwlrE6xOj6EwGpfjbDZKHkP/2RAdNYPQq3z7bdVJMJNnFMz6e37mzMpCBihwPdSFTRE3SCY66wvTgJzGCUyhHbSi5i1azzOXd8uHCSpT/6L1o6VU0pjoR8Kaa4wCWHGf7P/WEd+avyp02Jnu1cmpW0+Gq+vK9h0tdb2IClM/eNfdouQP+ep8MJUjraDRICmAJQY5obS71vniOo+reaFy0zRgB', 'thinking': '', 'type': 'thinking'}, {'signature': 'CAQS/REKEQgSGAI4AUIJbmFycmF0aW9uEgwFh3AVJZP3s8y1Hq0aDG76uhHONaScXAGShiIwGgjLXHUx1yZrkLPljjzTOQycpLJiKVwXcxm/EzKJm1s8M5WxuREWEldXcoaN0/msKpkRMT+eLb5ih9rAHULTomUNsUwJeu0jrJe4GLazy7sP5KtjEOEBKQsXUfoQ7fDkZOQUHZi9zzQN4hFNmYJUf3OVqf9TQNdC1+m//HAZPfWmAy6HtDvm7kgBJkCEoiFnm7tn/PX1hXGPRgWc6BTXUMr2WA8aKyqggRUbMH2+JYv5iWZddCc1up1Nbr3sF7WPcot9ZBhyHQXr692zeJk6SJgygIVrASbMly2bBxTWJB623gVgzpCmVSLZFpkKXgPcp8hGyDKOvF6SiQSHqGRxQofr7Vs1aPhfZixROJGD+MlrkggVAnGMyYIDUZpavtCeiU1JmBSz0084bXPtT/tnpsKOnSPYZQo3B0BXKbqA/WsKng8/V3Z8Uuajo6IVZ2hgA1WDduqQOQkExGU6pFuLhebIjuWxSNI/DffvR//LNqCfvdnf0hvoPtDxfx/SawIx4/spq9xbwFzHtLgoHg1F2wFw7clPDlg1PyD/v2yJG3V9T8GL6jGPE6rhfil/u4HNvf7BW0dQzCTTXcqZy4nByy0+p+S7ZxfMl4Kpxa2gOhhSNjfpfuX0QpL4kME8J1nliFN1d8KqEPuDI18BQHUhD/UfY29oA0x510FjM/BsLZ4n5sppkoIi7CFoQq2aAKVNfC1KilhIpRD+1dyDtZQklN8gZpOESHw6ML1TtU0yfAcbmqoxJ6hhjzwH6CMsTQEEy8MbRBM+QKKkIRdRCPHGFW2LkuR50+J0pF/tr6pqVvJeozfiMnrEilB27TZP4nKTJG8rd//vCWen0y7CA/8dBJ9gdp5tx7NVA45k1gdcxER1gYouNX3n1vG3/B1F4+93wXW9Mqo6Us59TQmrb9Nqa7pSQNSrH06w1GxyBAlkFfqAlafk16aMpq416TJYpGy43WCUiCcLWlhiWgz2sgOLAXP9a5weWAKRC8J6jkcUkDeXnW+HFSiPM6+9Bo6kVhLUhN5rc4mHZOZpF7ptPpsQN4XCqLPsqpRH/8o5XpyC6ZneTeF2ExtjhwZVjXDwMbIzVYIbw1y2PPGw8dC7pc4sIVSnI4oQAH0v650ghfhmMevWW4h4Pylbw/DG4fdeT3+8z0f8MOSTtfipx05/QttHlAvODRxa5yDCvOz6n3f1BO0N/4MbGyOM27qW4x8mBVoryUZWfsmhfi0/GfNvB8+ylTGbYNX27OcEF5WZtNqbWEy7V5GbmZUSxspcg7FBZ3j3/3L7muvZXoZi1f5erq4y3lSH+oF7qPFnvieyLOCwubcQy+oyowj4cD0idRecnp+s+52D7+mzF1gpW+f+cJasoxigbxkOloS4yeiKvgM4nWSpGEj12RPx5o80EmziXN9t4zJzW1htA5hXJJeFympMYtyCUCevRWVmrRYktQ7UnCa6tL/AVPi1ni5XCXHfLOuqfK5KLYxjwVtdhhfmCLvpaKVNCEOCf4nNGHqD98wUb0ejWNwDYBnJGkBRw9RVspfwNdoWoVcn2qv8ywGUp7yc/sk2LpI6VziRyvWqbGNG1ZJqVVutRc46+sOE4VSeS2/CTNKNa6NMCf/hxxaUVsYfKdP+S1Lc4ianKp0sUugPpmtoydC0CP2RwjnHLcnzxCnvqxlLQWXMNPTsu2yBcHlWJLtcmdg/McLagy7l5kG6sN7ZxJLD/kAx8E6uqohP+K/jMSrYuQJnhr4tf4biWFY3Jzf/KpAYyVSq+pbfFKXMbL9nHEoZQT15Wyabd6DMAq3QZOthM5pUzdNqUXkcrI1PwjASAg4dVU/2o+98mTXnyhP7VlEkTnddpwyJF+YCCc01pqa6ZX99pnR+YOpIrb5DtPcKq0t5cFCDyQxN1lUuqZcYujvX/GC7+6IpRTz/NkEY/5cntOfZJyd6SkUx/JR5AJIRPCNeib0rcWnnFn0XK8s22x40wLjEtufXDO9+yraf4kHxlBI3SB0meMjepX50CJhAx7NekkRFnwoecLfqIyrjXrgAZsTMPiX94snjdrzisAXtBKanulilSxOfW2HSmfnk0L2GXHJJ3S+vCbgREml9dekfmHhmwEYtv3kfhth7ERIwVT8j69OtIBCmeTIJuA7o73PXc1LJ2whuZK7FZe6NTOs61MP7JVlIc6tbhtOBk3NXU8pBPOCY6Q55d63FtpUU/KCbQ0K2WQ9o5jNGwNNJ6Cw5FDCt+x5YJeP1d2QzDSiq+cSZqwqCZVcNoAftGGzgrRZ4vkpbP0xQZHCtWsUwHVTjrY4NfRDjHdVPRn8J9Xtooc/hIWkqzFaczYTlapu2RCJqe8aEo3jzWZCEJ7AiBzBYraYDgOfeiQyeJUsbbXHdGKiiw22Bdp4M+Yz671MZ0ie9EWRks0Q2DHGUTne9S4tAAJPGbUf4xtCMoJ40Q4QW05QLSZy3wNFAUFYmXE4ECDFB8+IEeNFFF/tgAnfh8GvFh/1xY+LE+ozMminjSRwhq09zQAEkNFyu0IKgvDkyOI+iccF7doWLTUasPxLH1xGPBSKkeWCnSoX/4C2c9du66BbRXamHqae+VCiS6BN953iYKOu8rMkneW+TFjV0CzAf8PNG9LBayUgY5Uw9x0Sap6zI4PpaZKH4d9ktkFU+91yBbN5SRtdd8/EQatJXmBNfw7kRrll+LjwZweWLEb0jT1RpGuDqHLdLTMMgowSczrp2zCsJdQH/seaYrpwkCFR8dRjM7JRUkVa1A0SvdZAX75vuBvvLBzN2c+Rbt5oqy93lqIBIjKb7w2UkRTb4pBWcNKNPUgt3az/PxQxEasaltdmuyvsXYbur3VPNn8JzN/6J5N+QCnz571TccapCmkPvFE+gayXU+UF1VKYyFcUvlev7lsQlmjqzK2QA8X7Q3TzFapqhvJKlt7dzBHraqZBgPgb9N4oKNayi8FZT71+kaYhIhFrESKYRHfHkj5n3kfhgLy+x+jLDbKxMggkgHnJz9RuEHZXBWtkYAQ==', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_verifier', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAMRdS21FeJfzQhM3s', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7396, 'output_tokens': 770, 'output_tokens_details': {'thinking_tokens': 739}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='skeptic', id='lc_run--01a0fe03-4ae9-76c0-89a6-4f0b114e3030-0', tool_calls=[{'name': 'transfer_to_verifier', 'args': {}, 'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7396, 'output_tokens': 770, 'total_tokens': 8166, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 739}}), ToolMessage(content='Successfully transferred to verifier', name='transfer_to_verifier', id='c064563a-974f-4f0f-ab4a-080e63c01cf5', tool_call_id='toolu_01H6fV8JvRJNvXy47eRnd3jH'), AIMessage(content=[{'signature': 'CAQS9gUKEAgSGAI4AUIIdGhpbmtpbmcSDGFyJ1MXoY+Ai6NkbxoMRd2TrUfUU7WrPpA2IjDgy8dUplcZKc7DnJwNLTBfH5UNR+nGnakSPQia8UVIzrjkOnJ2fLrWUsA2oEO8yRsqkwXkkDCsyhB97dwj57RMt91XzgWhL9luNEGO92yW0rNFGoMSUVOxPIrFc5RjyrhKEc3+1RnpxCkylq+LNlQ5NFxLn6v2cF5DzqpvBHNR7XAig+urJIU3gXydCkaGZZDetJtQOUOXgZi9z9100NpdAEkXDo2ICHCGTGpVc0J/pbXxWRBxb6B1/Gl0Yjn68V6HlesH7IoC7GR/3HwRu9NSDBG4ttfQjEWcg+PeAea4Rn7jzx+Tm9H7L1joTKVgDbQg5iKMLfjvPqlnksCLtXOt8jSIS5OYLN7a/7TX6/w5d34vnw8vPDmU1UYi0Q72qUWhUa2vw8OXNpcKL7Bp0lNfBS8rvHY+vBY8/haAoTL3wvLFU1fT+4z/vA3BcsigZDYlfJeDYcxXmRuqxzxEGpXCRe9s1HWtID+41pHu6lpZHkXaghpc6TZHLxNwcVhTb6fMQ7Qo+2L6Pvklh1m32peodxeg2YiPhdpl3w/xkOEu5/JEF7ZBxpdUL+u3wkO5pUlsqfgX7uhzv7HC2WpSWVimrwiQwQOUx0+JPrLtw+lYfq7I3jruo0GW0guWvnuf7JM3YLj84q+JgHomnSSZnnZmrL0UaRvrVyLo877F39ccLS7XoZnnui8J4P58ZTmiLkEJeGcx1Iuag2e33339O7Yew/59tLbQtIGh9VWMwHBnFx39S8/4LEFrK5HldPQhOrloyTtkRs66iQRKtxMKS0VI6QTKZDAbs7a7H1yBv+mJi9Xg4uKmyU6qnn7zEKjjY2bIlCwezx4LLGP5HD3Zr6fZsmdG1UHzDsBvYG2oCI0oqEKM+valsT6hU8MRjbQBD/JJ8JJN/iBGlhBAUHo0UmhhtR2euTVpZWlMnhmpXAdKDXSfPdaPohgB', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_red_team', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAsabDquegHi6YUpCn', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7509, 'output_tokens': 88, 'output_tokens_details': {'thinking_tokens': 55}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='verifier', id='lc_run--01a0fe03-6682-7d21-8c15-ffbce9422c9f-0', tool_calls=[{'name': 'transfer_to_red_team', 'args': {}, 'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7509, 'output_tokens': 88, 'total_tokens': 7597, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 55}}), ToolMessage(content='Successfully transferred to red_team', name='transfer_to_red_team', tool_call_id='toolu_01XEAuhQHCBdyCwtymZwFfDi')], 'active_agent': 'red_team'}, goto='red_team')\n" + } + }, + { + "key": "exception.escaped", + "value": { + "stringValue": "False" + } + } + ] + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "0b1303d7a7aa229b", + "parentSpanId": "04ffc0db9ce0f358", + "name": "verifier", + "kind": 1, + "startTimeUnixNano": "1790968030845710080", + "endTimeUnixNano": "1790968032481081856", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "e30=" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "verifier" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langgraph" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "3" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "verifier" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:verifier\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"verifier\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "verifier:544dea40-e16b-458c-1464-128e1b9b10a9" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "graph:step:3" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W3siY29udGVudCI6IkZvciBhIHN5bnRoZXRpYyB0cmFjaW5nIGZpeHR1cmUsIGV4cGxhaW4gUHl0aG9uIHR1cGxlIHZlcnN1cyBsaXN0IGltbXV0YWJpbGl0eSBpbiBhdCBtb3N0IDEyMCB3b3Jkcy4gVXNlIG9ubHkgb2ZmaWNpYWwgUHl0aG9uIGRvY3VtZW50YXRpb24gaWYgeW91IHNlYXJjaC4gSGF2ZSB0aGUgc2tlcHRpYyBhbmQgdmVyaWZpZXIgY2hlY2sgaXQsIHRoZW4gaGF2ZSB0aGUgZWRpdG9yIHdyaXRlIC9hbnN3ZXIubWQuIEtlZXAgdGhlIHJ1biBzaG9ydCBhbmQgZG8gbm90IGFzayBmb2xsb3ctdXAgcXVlc3Rpb25zLiIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoiaHVtYW4iLCJpZCI6IjQ2MjA0MzA1LWY4ZWUtNDM4NC05Y2U4LWZjZDgwYmZjZGY3NSJ9LHsiY29udGVudCI6W3siaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9LCJuYW1lIjoid2ViX3NlYXJjaCIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHc5YVc5YnJlblBxZ3liZk53UCIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjoyODQ1LCJvdXRwdXRfdG9rZW5zIjo3Mywib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6MH0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoicmVzZWFyY2hlciIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy0yMWEzLTcwMDAtYTM4MS0yODRhNmEwYzAzZjctMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ3ZWJfc2VhcmNoIiwiYXJncyI6eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9LCJpZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6Mjg0NSwib3V0cHV0X3Rva2VucyI6NzMsInRvdGFsX3Rva2VucyI6MjkxOCwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fSx7ImNvbnRlbnQiOiJbe1widGl0bGVcIjogXCI1LiBEYXRhIFN0cnVjdHVyZXMgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3R1dG9yaWFsL2RhdGFzdHJ1Y3R1cmVzLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcIllvdSBtaWdodCBoYXZlIG5vdGljZWQgdGhhdCBtZXRob2RzIGxpa2VgaW5zZXJ0YCxgcmVtb3ZlYCBvcmBzb3J0YCB0aGF0IG9ubHkgbW9kaWZ5IHRoZSBsaXN0IGhhdmUgbm8gcmV0dXJuIHZhbHVlIHByaW50ZWQgXFx1MjAxMyB0aGV5IHJldHVybiB0aGUgZGVmYXVsdGBOb25lYC4gWzFdIFRoaXMgaXMgYSBkZXNpZ24gcHJpbmNpcGxlIGZvciBhbGwgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgaW4gUHl0aG9uLlxcbi4uLlxcbiMjIDUuMy4gVHVwbGVzIGFuZCBTZXF1ZW5jZXNcXHUwMGI2XFxuXFxuV2Ugc2F3IHRoYXQgbGlzdHMgYW5kIHN0cmluZ3MgaGF2ZSBtYW55IGNvbW1vbiBwcm9wZXJ0aWVzLCBzdWNoIGFzIGluZGV4aW5nIGFuZCBzbGljaW5nIG9wZXJhdGlvbnMuIFRoZXkgYXJlIHR3byBleGFtcGxlcyBvZiBzZXF1ZW5jZSBkYXRhIHR5cGVzIChzZWUgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBsaXN0LCB0dXBsZSwgcmFuZ2UpLiBTaW5jZSBQeXRob24gaXMgYW4gZXZvbHZpbmcgbGFuZ3VhZ2UsIG90aGVyIHNlcXVlbmNlIGRhdGEgdHlwZXMgbWF5IGJlIGFkZGVkLiBUaGVyZSBpcyBhbHNvIGFub3RoZXIgc3RhbmRhcmQgc2VxdWVuY2UgZGF0YSB0eXBlOiB0aGUgdHVwbGUuXFxuXFxuQSB0dXBsZSBjb25zaXN0cyBvZiBhIG51bWJlciBvZiB2YWx1ZXMgc2VwYXJhdGVkIGJ5IGNvbW1hcywgZm9yIGluc3RhbmNlOlxcbi4uLlxcbj4+PiAjIFR1cGxlcyBhcmUgaW1tdXRhYmxlOlxcbj4+PiB0WzBdID0gODg4ODhcXG5UcmFjZWJhY2sgKG1vc3QgcmVjZW50IGNhbGwgbGFzdCk6XFxuICBGaWxlIFxcXCI8c3RkaW4+XFxcIiwgbGluZSAxLCBpbiA8bW9kdWxlPlxcblR5cGVFcnJvcjogJ3R1cGxlJyBvYmplY3QgZG9lcyBub3Qgc3VwcG9ydCBpdGVtIGFzc2lnbm1lbnRcXG4+Pj4gIyBidXQgdGhleSBjYW4gY29udGFpbiBtdXRhYmxlIG9iamVjdHM6XFxuPj4+IHYgPSAoWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuPj4+IHZcXG4oWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuLi4uXFxuQXMgeW91IHNlZSwgb24gb3V0cHV0IHR1cGxlcyBhcmUgYWx3YXlzIGVuY2xvc2VkIGluIHBhcmVudGhlc2VzLCBzbyB0aGF0IG5lc3RlZCB0dXBsZXMgYXJlIGludGVycHJldGVkIGNvcnJlY3RseTsgdGhleSBtYXkgYmUgaW5wdXQgd2l0aCBvciB3aXRob3V0IHN1cnJvdW5kaW5nIHBhcmVudGhlc2VzLCBhbHRob3VnaCBvZnRlbiBwYXJlbnRoZXNlcyBhcmUgbmVjZXNzYXJ5IGFueXdheSAoaWYgdGhlIHR1cGxlIGlzIHBhcnQgb2YgYSBsYXJnZXIgZXhwcmVzc2lvbikuIEl0IGlzIG5vdCBwb3NzaWJsZSB0byBhc3NpZ24gdG8gdGhlIGluZGl2aWR1YWwgaXRlbXMgb2YgYSB0dXBsZSwgaG93ZXZlciBpdCBpcyBwb3NzaWJsZSB0byBjcmVhdGUgdHVwbGVzIHdoaWNoIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzLCBzdWNoIGFzIGxpc3RzLlxcblxcblRob3VnaCB0dXBsZXMgbWF5IHNlZW0gc2ltaWxhciB0byBsaXN0cywgdGhleSBhcmUgb2Z0ZW4gdXNlZCBpbiBkaWZmZXJlbnQgc2l0dWF0aW9ucyBhbmQgZm9yIGRpZmZlcmVudCBwdXJwb3Nlcy4gVHVwbGVzIGFyZSBpbW11dGFibGUsIGFuZCB1c3VhbGx5IGNvbnRhaW4gYSBoZXRlcm9nZW5lb3VzIHNlcXVlbmNlIG9mIGVsZW1lbnRzIHRoYXQgYXJlIGFjY2Vzc2VkIHZpYSB1bnBhY2tpbmcgKHNlZSBsYXRlciBpbiB0aGlzIHNlY3Rpb24pIG9yIGluZGV4aW5nIChvciBldmVuIGJ5IGF0dHJpYnV0ZSBpbiB0aGUgY2FzZSBvZiBuYW1lZHR1cGxlcykuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG5Bbm90aGVyIHVzZWZ1bCBkYXRhIHR5cGUgYnVpbHQgaW50byBQeXRob24gaXMgdGhlIGRpY3Rpb25hcnkgKHNlZSBNYXBwaW5nIFR5cGVzIFxcdTIwMTQgZGljdCkuIERpY3Rpb25hcmllcyBhcmUgc29tZXRpbWVzIGZvdW5kIGluIG90aGVyIGxhbmd1YWdlcyBhcyBcXHUyMDFjYXNzb2NpYXRpdmUgbWVtb3JpZXNcXHUyMDFkIG9yIFxcdTIwMWNhc3NvY2lhdGl2ZSBhcnJheXNcXHUyMDFkLiBVbmxpa2Ugc2VxdWVuY2VzLCB3aGljaCBhcmUgaW5kZXhlZCBieSBhIHJhbmdlIG9mIG51bWJlcnMsIGRpY3Rpb25hcmllcyBhcmUgaW5kZXhlZCBieSBrZXlzLCB3aGljaCBjYW4gYmUgYW55IGltbXV0YWJsZSB0eXBlOyBzdHJpbmdzIGFuZCBudW1iZXJzIGNhbiBhbHdheXMgYmUga2V5cy4gVHVwbGVzIGNhbiBiZSB1c2VkIGFzIGtleXMgaWYgdGhleSBjb250YWluIG9ubHkgc3RyaW5ncywgbnVtYmVycywgb3IgdHVwbGVzOyBpZiBhIHR1cGxlIGNvbnRhaW5zIGFueSBtdXRhYmxlIG9iamVjdCBlaXRoZXIgZGlyZWN0bHkgb3IgaW5kaXJlY3RseSwgaXQgY2Fubm90IGJlIHVzZWQgYXMgYSBrZXkuIFlvdSBjYW5cXHUyMDE5dCB1c2UgbGlzdHMgYXMga2V5cywgc2luY2UgbGlzdHMgY2FuIGJlIG1vZGlmaWVkIGluIHBsYWNlIHVzaW5nIGluZGV4IGFzc2lnbm1lbnRzLCBzbGljZSBhc3NpZ25tZW50cywgb3IgbWV0aG9kcyBsaWtlIGFwcGVuZCgpIGFuZCBleHRlbmQoKS5cIl19LCB7XCJ0aXRsZVwiOiBcIjMuIERhdGEgbW9kZWwgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3JlZmVyZW5jZS9kYXRhbW9kZWwuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiVGhlIHZhbHVlIG9mIHNvbWUgb2JqZWN0cyBjYW4gY2hhbmdlLiBPYmplY3RzIHdob3NlIHZhbHVlIGNhbiBjaGFuZ2UgYXJlIHNhaWQgdG8gYmUgbXV0YWJsZTsgb2JqZWN0cyB3aG9zZSB2YWx1ZSBpcyB1bmNoYW5nZWFibGUgb25jZSB0aGV5IGFyZSBjcmVhdGVkIGFyZSBjYWxsZWQgaW1tdXRhYmxlLiAoVGhlIHZhbHVlIG9mIGFuIGltbXV0YWJsZSBjb250YWluZXIgb2JqZWN0IHRoYXQgY29udGFpbnMgYSByZWZlcmVuY2UgdG8gYSBtdXRhYmxlIG9iamVjdCBjYW4gY2hhbmdlIHdoZW4gdGhlIGxhdHRlclxcdTIwMTlzIHZhbHVlIGlzIGNoYW5nZWQ7IGhvd2V2ZXIgdGhlIGNvbnRhaW5lciBpcyBzdGlsbCBjb25zaWRlcmVkIGltbXV0YWJsZSwgYmVjYXVzZSB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGl0IGNvbnRhaW5zIGNhbm5vdCBiZSBjaGFuZ2VkLiBTbywgaW1tdXRhYmlsaXR5IGlzIG5vdCBzdHJpY3RseSB0aGUgc2FtZSBhcyBoYXZpbmcgYW4gdW5jaGFuZ2VhYmxlIHZhbHVlLCBpdCBpcyBtb3JlIHN1YnRsZS4pIEFuIG9iamVjdFxcdTIwMTlzIG11dGFiaWxpdHkgaXMgZGV0ZXJtaW5lZCBieSBpdHMgdHlwZTsgZm9yIGluc3RhbmNlLCBudW1iZXJzLCBzdHJpbmdzIGFuZCB0dXBsZXMgYXJlIGltbXV0YWJsZSwgd2hpbGUgZGljdGlvbmFyaWVzIGFuZCBsaXN0cyBhcmUgbXV0YWJsZS5cXG4uLi5cXG5Tb21lIG9iamVjdHMgY29udGFpbiByZWZlcmVuY2VzIHRvIG90aGVyIG9iamVjdHM7IHRoZXNlIGFyZSBjYWxsZWQgY29udGFpbmVycy4gRXhhbXBsZXMgb2YgY29udGFpbmVycyBhcmUgdHVwbGVzLCBsaXN0cyBhbmQgZGljdGlvbmFyaWVzLiBUaGUgcmVmZXJlbmNlcyBhcmUgcGFydCBvZiBhIGNvbnRhaW5lclxcdTIwMTlzIHZhbHVlLiBJbiBtb3N0IGNhc2VzLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIHZhbHVlIG9mIGEgY29udGFpbmVyLCB3ZSBpbXBseSB0aGUgdmFsdWVzLCBub3QgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGNvbnRhaW5lZCBvYmplY3RzOyBob3dldmVyLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIG11dGFiaWxpdHkgb2YgYSBjb250YWluZXIsIG9ubHkgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGltbWVkaWF0ZWx5IGNvbnRhaW5lZCBvYmplY3RzIGFyZSBpbXBsaWVkLiBTbywgaWYgYW4gaW1tdXRhYmxlIGNvbnRhaW5lciAobGlrZSBhIHR1cGxlKSBjb250YWlucyBhIHJlZmVyZW5jZSB0byBhIG11dGFibGUgb2JqZWN0LCBpdHMgdmFsdWUgY2hhbmdlcyBpZiB0aGF0IG11dGFibGUgb2JqZWN0IGlzIGNoYW5nZWQuXFxuLi4uXFxuIyMjIDMuMi41LiBTZXF1ZW5jZXNcXHUwMGI2XFxuLi4uXFxuIyMjIyAzLjIuNS4xLiBJbW11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbkFuIG9iamVjdCBvZiBhbiBpbW11dGFibGUgc2VxdWVuY2UgdHlwZSBjYW5ub3QgY2hhbmdlIG9uY2UgaXQgaXMgY3JlYXRlZC4gKElmIHRoZSBvYmplY3QgY29udGFpbnMgcmVmZXJlbmNlcyB0byBvdGhlciBvYmplY3RzLCB0aGVzZSBvdGhlciBvYmplY3RzIG1heSBiZSBtdXRhYmxlIGFuZCBtYXkgYmUgY2hhbmdlZDsgaG93ZXZlciwgdGhlIGNvbGxlY3Rpb24gb2Ygb2JqZWN0cyBkaXJlY3RseSByZWZlcmVuY2VkIGJ5IGFuIGltbXV0YWJsZSBvYmplY3QgY2Fubm90IGNoYW5nZS4pXFxuXFxuVGhlIGZvbGxvd2luZyB0eXBlcyBhcmUgaW1tdXRhYmxlIHNlcXVlbmNlczpcXG4uLi5cXG5UdXBsZXNcXG46IFRoZSBpdGVtcyBvZiBhIGB0dXBsZWAgYXJlIGFyYml0cmFyeSBQeXRob24gb2JqZWN0cy4gVHVwbGVzIG9mIHR3byBvciBtb3JlIGl0ZW1zIGFyZSBmb3JtZWQgYnkgY29tbWEtc2VwYXJhdGVkIGxpc3RzIG9mIGV4cHJlc3Npb25zLiBBIHR1cGxlIG9mIG9uZSBpdGVtIChhIFxcdTIwMThzaW5nbGV0b25cXHUyMDE5KSBjYW4gYmUgZm9ybWVkIGJ5IGFmZml4aW5nIGEgY29tbWEgdG8gYW4gZXhwcmVzc2lvbiAoYW4gZXhwcmVzc2lvbiBieSBpdHNlbGYgZG9lcyBub3QgY3JlYXRlIGEgdHVwbGUsIHNpbmNlIHBhcmVudGhlc2VzIG11c3QgYmUgdXNhYmxlIGZvciBncm91cGluZyBvZiBleHByZXNzaW9ucykuIEFuIGVtcHR5IHR1cGxlIGNhbiBiZSBmb3JtZWQgYnkgYW4gZW1wdHkgcGFpciBvZiBwYXJlbnRoZXNlcy5cXG4uLi5cXG4jIyMjIDMuMi41LjIuIE11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbk11dGFibGUgc2VxdWVuY2VzIGNhbiBiZSBjaGFuZ2VkIGFmdGVyIHRoZXkgYXJlIGNyZWF0ZWQuIFRoZSBzdWJzY3JpcHRpb24gYW5kIHNsaWNpbmcgbm90YXRpb25zIGNhbiBiZSB1c2VkIGFzIHRoZSB0YXJnZXQgb2YgYXNzaWdubWVudCBhbmQgYGRlbGAgKGRlbGV0ZSkgc3RhdGVtZW50cy5cXG4uLi5cXG5UaGVyZSBhcmUgY3VycmVudGx5IHR3byBpbnRyaW5zaWMgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlczpcXG5cXG5MaXN0c1xcbjogVGhlIGl0ZW1zIG9mIGEgbGlzdCBhcmUgYXJiaXRyYXJ5IFB5dGhvbiBvYmplY3RzLiBMaXN0cyBhcmUgZm9ybWVkIGJ5IHBsYWNpbmcgYSBjb21tYS1zZXBhcmF0ZWQgbGlzdCBvZiBleHByZXNzaW9ucyBpbiBzcXVhcmUgYnJhY2tldHMuIChOb3RlIHRoYXQgdGhlcmUgYXJlIG5vIHNwZWNpYWwgY2FzZXMgbmVlZGVkIHRvIGZvcm0gbGlzdHMgb2YgbGVuZ3RoIDAgb3IgMS4pXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjcgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvYnVpbHRpbnMvc3RkdHlwZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiY29sbGVjdGlvbiBjbGFzc2VzIGFyZSBtdXRhYmxlLiBUaGUgbWV0aG9kcyB0aGF0IGFkZCwgc3VidHJhY3QsIG9yIHJlYXJyYW5nZSB0aGVpciAuLi4gaW4gcGxhY2UsIGFuZCBkb25cXHUyMDE5dCByZXR1cm4gYSAuLi4gLCBuZXZlciByZXR1cm4gdGhlIGNvbGxlY3Rpb24gaW5zdGFuY2UgaXRzZWxmIGJ1dCBgTm9uZWAuXFxuLi4uXFxuIyMgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBgbGlzdGAsIGB0dXBsZWAsIGByYW5nZWBcXHUwMGI2XFxuXFxuVGhlcmUgYXJlIHRocmVlIGJhc2ljIHNlcXVlbmNlIHR5cGVzOiBsaXN0cywgdHVwbGVzLCBhbmQgcmFuZ2Ugb2JqZWN0cy4gQWRkaXRpb25hbCBzZXF1ZW5jZSB0eXBlcyB0YWlsb3JlZCBmb3IgcHJvY2Vzc2luZyBvZiBiaW5hcnkgZGF0YSBhbmQgdGV4dCBzdHJpbmdzIGFyZSBkZXNjcmliZWQgaW4gZGVkaWNhdGVkIHNlY3Rpb25zLlxcbi4uLlxcblRoZSBvcGVyYXRpb25zIGluIHRoZSBmb2xsb3dpbmcgdGFibGUgLi4uIG11dGFibGUgYW5kIGltbXV0YWJsZS4gVGhlIGBjb2xsZWN0aW9ucy4gLi4uIGlzIHByb3ZpZGVkIHRvIG1ha2UgLi4uIGVhc2llciB0byBjb3JyZWN0bHkgaW1wbGVtZW50IHRoZXNlIG9wZXJhdGlvbnMgb24gY3VzdG9tIHNlcXVlbmNlIHR5cGVzXFxuLi4uXFxuIyMjIEltbXV0YWJsZSBTZXF1ZW5jZSBUeXBlc1xcdTAwYjZcXG4uLi5cXG5tdXRhYmxlIHNlcXVlbmNlIHR5cGVzIGlzXFxuLi4uXFxuc3VwcG9ydCBhbGxvd3MgaW1tdXRhYmxlIHNlcXVlbmNlcywgLi4uICwgdG8gYmUgdXNlZCBhcyBgZGljdGAga2V5cyBhbmQgc3RvcmVkIGluIC4uLiBlbnNldGAgaW5zdGFuY2VzXFxuLi4uXFxuIyMjIE11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxuIyMjIExpc3RzXFx1MDBiNlxcblxcbkxpc3RzIGFyZSBtdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaG9tb2dlbmVvdXMgaXRlbXMgKHdoZXJlIHRoZSBwcmVjaXNlIGRlZ3JlZSBvZiBzaW1pbGFyaXR5IHdpbGwgdmFyeSBieSBhcHBsaWNhdGlvbikuXFxuLi4uXFxuIyMjIFR1cGxlc1xcdTAwYjZcXG5cXG5UdXBsZXMgYXJlIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhldGVyb2dlbmVvdXMgZGF0YSAoc3VjaCBhcyB0aGUgMi10dXBsZXMgcHJvZHVjZWQgYnkgdGhlIGBlbnVtZXJhdGUoKWAgYnVpbHQtaW4pLiBUdXBsZXMgYXJlIGFsc28gdXNlZCBmb3IgY2FzZXMgd2hlcmUgYW4gaW1tdXRhYmxlIHNlcXVlbmNlIG9mIGhvbW9nZW5lb3VzIGRhdGEgaXMgbmVlZGVkIChzdWNoIGFzIGFsbG93aW5nIHN0b3JhZ2UgaW4gYSBgc2V0YCBvciBgZGljdGAgaW5zdGFuY2UpLlxcbi4uLlxcbnR1cGxlKGl0ZXJhYmxlPVxcbi4uLlxcblRoZSBjb25zdHJ1Y3RvciBidWlsZHMgYSB0dXBsZSB3aG9zZSBpdGVtcyBhcmUgdGhlIHNhbWUgYW5kIGluIHRoZSBzYW1lIG9yZGVyIGFzIGl0ZXJhYmxlXFx1MjAxOXMgaXRlbXMuIGl0ZXJhYmxlIG1heSBiZSBlaXRoZXIgYSBzZXF1ZW5jZSwgYSBjb250YWluZXIgdGhhdCBzdXBwb3J0cyBpdGVyYXRpb24sIG9yIGFuIGl0ZXJhdG9yIG9iamVjdC4gSWYgaXRlcmFibGUgaXMgYWxyZWFkeSBhIHR1cGxlLCBpdCBpcyByZXR1cm5lZCB1bmNoYW5nZWQuIEZvciBleGFtcGxlLCBgdHVwbGUoJ2FiYycpYCByZXR1cm5zIGAoJ2EnLCAnYicsICdjJylgIGFuZCBgdHVwbGUoIFsxLCAyLCAzXSApYCByZXR1cm5zIGAoMSwgMiwgMylgLiBJZiBubyBhcmd1bWVudCBpcyBnaXZlbiwgdGhlIGNvbnN0cnVjdG9yIGNyZWF0ZXMgYSBuZXcgZW1wdHkgdHVwbGUsIGAoKWAuXFxuLi4uXFxuVHVwbGVzIGltcGxlbWVudCBhbGwgb2YgdGhlIGNvbW1vbiBzZXF1ZW5jZSBvcGVyYXRpb25zLlxcbi4uLlxcbkZvciBoZXRlcm9nZW5lb3VzIGNvbGxlY3Rpb25zIG9mIGRhdGEgd2hlcmUgYWNjZXNzIGJ5IG5hbWUgaXMgY2xlYXJlciB0aGFuIGFjY2VzcyBieSBpbmRleCwgYGNvbGxlY3Rpb25zLm5hbWVkdHVwbGUoKWAgbWF5IGJlIGEgbW9yZSBhcHByb3ByaWF0ZSBjaG9pY2UgdGhhbiBhIHNpbXBsZSB0dXBsZSBvYmplY3QuXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvbGlicmFyeS9zdGR0eXBlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJTb21lIGNvbGxlY3Rpb24gY2xhc3NlcyBhcmUgbXV0YWJsZS4gVGhlIG1ldGhvZHMgdGhhdCBhZGQsIHN1YnRyYWN0LCBvciByZWFycmFuZ2UgdGhlaXIgLi4uIGluIHBsYWNlLCBhbmQgZG9uXFx1MjAxOXQgcmV0dXJuIGEgLi4uICwgbmV2ZXIgcmV0dXJuIHRoZSBjb2xsZWN0aW9uIGluc3RhbmNlIGl0c2VsZiBidXQgYE5vbmVgLlxcbi4uLlxcbiMjIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgYGxpc3RgLCBgdHVwbGVgLCBgcmFuZ2VgXFx1MDBiNlxcblxcblRoZXJlIGFyZSB0aHJlZSBiYXNpYyBzZXF1ZW5jZSB0eXBlczogbGlzdHMsIHR1cGxlcywgYW5kIC4uLiBvYmplY3RzLiBBZGRpdGlvbmFsIHNlcXVlbmNlIHR5cGVzIHRhaWxvcmVkIGZvciBwcm9jZXNzaW5nIG9mIGJpbmFyeSBkYXRhIGFuZCB0ZXh0IHN0cmluZ3MgYXJlIGRlc2NyaWJlZCBpbiBkZWRpY2F0ZWQgc2VjdGlvbnMuXFxuLi4uXFxuVGhlIG9wZXJhdGlvbnMgaW4gdGhlIGZvbGxvd2luZyB0YWJsZSAuLi4gaW1tdXRhYmxlLiBUaGUgYCAuLi4gaXMgcHJvdmlkZWQgdG8gbWFrZSAuLi4gZWFzaWVyIHRvIGNvcnJlY3RseSBpbXBsZW1lbnQgdGhlc2Ugb3BlcmF0aW9ucyBvbiBjdXN0b20gc2VxdWVuY2UgdHlwZXNcXG4uLi5cXG4jIyMgSW1tdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbm9wZXJhdGlvbiB0aGF0IGltbXV0YWJsZSBzZXF1ZW5jZSAuLi4gYnkgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlcyBpcyAuLi4gYGhhc2goKWBcXG4uLi5cXG5UaGlzIHN1cHBvcnQgYWxsb3dzIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHN1Y2ggYXMgYHR1cGxlYCBpbnN0YW5jZXMsIHRvIGJlIHVzZWQgYXMgYGRpY3RgIGtleXMgYW5kIHN0b3JlZCBpbiBgc2V0YCBhbmQgYGZyb3plbnNldGAgaW5zdGFuY2VzLlxcbi4uLlxcbiMjIyBNdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbiMjIyBMaXN0c1xcdTAwYjZcXG5cXG5MaXN0cyBhcmUgbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhvbW9nZW5lb3VzIGl0ZW1zICh3aGVyZSB0aGUgcHJlY2lzZSBkZWdyZWUgb2Ygc2ltaWxhcml0eSB3aWxsIHZhcnkgYnkgYXBwbGljYXRpb24pLlxcbi4uLlxcbiMjIyBUdXBsZXNcXHUwMGI2XFxuXFxuVHVwbGVzIGFyZSBpbW11dGFibGUgc2VxdWVuY2VzLCB0eXBpY2FsbHkgdXNlZCB0byBzdG9yZSBjb2xsZWN0aW9ucyBvZiBoZXRlcm9nZW5lb3VzIGRhdGEgKHN1Y2ggYXMgdGhlIDItdHVwbGVzIHByb2R1Y2VkIGJ5IHRoZSBgZW51bWVyYXRlKClgIGJ1aWx0LWluKS4gVHVwbGVzIGFyZSBhbHNvIHVzZWQgZm9yIGNhc2VzIHdoZXJlIGFuIGltbXV0YWJsZSBzZXF1ZW5jZSBvZiBob21vZ2VuZW91cyBkYXRhIGlzIG5lZWRlZCAoc3VjaCBhcyBhbGxvd2luZyBzdG9yYWdlIGluIGEgYHNldGAgb3IgYGRpY3RgIGluc3RhbmNlKS5cXG4uLi5cXG5UaGUgY29uc3RydWN0b3IgYnVpbGRzIGEgdHVwbGUgd2hvc2UgaXRlbXMgYXJlIHRoZSBzYW1lIGFuZCBpbiB0aGUgc2FtZSBvcmRlciBhcyBpdGVyYWJsZVxcdTIwMTlzIGl0ZW1zLiBpdGVyYWJsZSBtYXkgYmUgZWl0aGVyIGEgc2VxdWVuY2UsIGEgY29udGFpbmVyIHRoYXQgc3VwcG9ydHMgaXRlcmF0aW9uLCBvciBhbiBpdGVyYXRvciBvYmplY3QuIElmIGl0ZXJhYmxlIGlzIGFscmVhZHkgYSB0dXBsZSwgaXQgaXMgcmV0dXJuZWQgdW5jaGFuZ2VkLiBGb3IgZXhhbXBsZSwgYHR1cGxlKCdhYmMnKWAgcmV0dXJucyBgKCdhJywgJ2InLCAnYycpYCBhbmQgYHR1cGxlKCBbMSwgMiwgM10gKWAgcmV0dXJucyBgKDEsIDIsIDMpYC4gSWYgbm8gYXJndW1lbnQgaXMgZ2l2ZW4sIHRoZSBjb25zdHJ1Y3RvciBjcmVhdGVzIGEgbmV3IGVtcHR5IHR1cGxlLCBgKClgLlxcbi4uLlxcblR1cGxlcyBpbXBsZW1lbnQgYWxsIG9mIHRoZSBjb21tb24gc2VxdWVuY2Ugb3BlcmF0aW9ucy5cXG4uLi5cXG5Gb3IgaGV0ZXJvZ2VuZW91cyBjb2xsZWN0aW9ucyBvZiBkYXRhIHdoZXJlIGFjY2VzcyBieSBuYW1lIGlzIGNsZWFyZXIgdGhhbiBhY2Nlc3MgYnkgaW5kZXgsIGBjb2xsZWN0aW9ucy5uYW1lZHR1cGxlKClgIG1heSBiZSBhIG1vcmUgYXBwcm9wcmlhdGUgY2hvaWNlIHRoYW4gYSBzaW1wbGUgdHVwbGUgb2JqZWN0LlwiXX0sIHtcInRpdGxlXCI6IFwiNS4gRGF0YSBTdHJ1Y3R1cmVzIFxcdTIwMTQgUHl0aG9uIDMuMTAuMjAgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMuMTAvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiWW91IG1pZ2h0IGhhdmUgbm90aWNlZCB0aGF0IG1ldGhvZHMgbGlrZSBgaW5zZXJ0YCwgYHJlbW92ZWAgb3IgYHNvcnRgIHRoYXQgb25seSBtb2RpZnkgdGhlIGxpc3QgaGF2ZSBubyByZXR1cm4gdmFsdWUgcHJpbnRlZCBcXHUyMDEzIHRoZXkgcmV0dXJuIHRoZSBkZWZhdWx0IGBOb25lYC4gMSBUaGlzIGlzIGEgZGVzaWduIHByaW5jaXBsZSBmb3IgYWxsIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIGluIFB5dGhvbi5cXG4uLi5cXG4jIyA1LjMuIFR1cGxlcyBhbmQgU2VxdWVuY2VzXFx1MDBiNlxcblxcbldlIHNhdyB0aGF0IGxpc3RzIGFuZCBzdHJpbmdzIGhhdmUgbWFueSBjb21tb24gcHJvcGVydGllcywgc3VjaCBhcyBpbmRleGluZyBhbmQgc2xpY2luZyBvcGVyYXRpb25zLiBUaGV5IGFyZSB0d28gZXhhbXBsZXMgb2Ygc2VxdWVuY2UgZGF0YSB0eXBlcyAoc2VlIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgbGlzdCwgdHVwbGUsIHJhbmdlKS4gU2luY2UgUHl0aG9uIGlzIGFuIGV2b2x2aW5nIGxhbmd1YWdlLCBvdGhlciBzZXF1ZW5jZSBkYXRhIHR5cGVzIG1heSBiZSBhZGRlZC4gVGhlcmUgaXMgYWxzbyBhbm90aGVyIHN0YW5kYXJkIHNlcXVlbmNlIGRhdGEgdHlwZTogdGhlIHR1cGxlLlxcblxcbkEgdHVwbGUgY29uc2lzdHMgb2YgYSBudW1iZXIgb2YgdmFsdWVzIHNlcGFyYXRlZCBieSBjb21tYXMsIGZvciBpbnN0YW5jZTpcXG4uLi5cXG4+Pj4gIyBUdXBsZXMgYXJlIGltbXV0YWJsZTpcXG4uLi4gdFswXSA9IDg4ODg4XFxuVHJhY2ViYWNrIChtb3N0IHJlY2VudCBjYWxsIGxhc3QpOlxcbiAgRmlsZSBcXFwiPHN0ZGluPlxcXCIsIGxpbmUgMSwgaW4gPG1vZHVsZT5cXG5UeXBlRXJyb3I6ICd0dXBsZScgb2JqZWN0IGRvZXMgbm90IHN1cHBvcnQgaXRlbSBhc3NpZ25tZW50XFxuPj4+ICMgYnV0IHRoZXkgY2FuIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzOlxcbi4uLiB2ID0gKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbj4+PiB2XFxuKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbi4uLlxcbkFzIHlvdSBzZWUsIG9uIG91dHB1dCB0dXBsZXMgYXJlIGFsd2F5cyBlbmNsb3NlZCBpbiBwYXJlbnRoZXNlcywgc28gdGhhdCBuZXN0ZWQgdHVwbGVzIGFyZSBpbnRlcnByZXRlZCBjb3JyZWN0bHk7IHRoZXkgbWF5IGJlIGlucHV0IHdpdGggb3Igd2l0aG91dCBzdXJyb3VuZGluZyBwYXJlbnRoZXNlcywgYWx0aG91Z2ggb2Z0ZW4gcGFyZW50aGVzZXMgYXJlIG5lY2Vzc2FyeSBhbnl3YXkgKGlmIHRoZSB0dXBsZSBpcyBwYXJ0IG9mIGEgbGFyZ2VyIGV4cHJlc3Npb24pLiBJdCBpcyBub3QgcG9zc2libGUgdG8gYXNzaWduIHRvIHRoZSBpbmRpdmlkdWFsIGl0ZW1zIG9mIGEgdHVwbGUsIGhvd2V2ZXIgaXQgaXMgcG9zc2libGUgdG8gY3JlYXRlIHR1cGxlcyB3aGljaCBjb250YWluIG11dGFibGUgb2JqZWN0cywgc3VjaCBhcyBsaXN0cy5cXG5cXG5UaG91Z2ggdHVwbGVzIG1heSBzZWVtIHNpbWlsYXIgdG8gbGlzdHMsIHRoZXkgYXJlIG9mdGVuIHVzZWQgaW4gZGlmZmVyZW50IHNpdHVhdGlvbnMgYW5kIGZvciBkaWZmZXJlbnQgcHVycG9zZXMuIFR1cGxlcyBhcmUgaW1tdXRhYmxlLCBhbmQgdXN1YWxseSBjb250YWluIGEgaGV0ZXJvZ2VuZW91cyBzZXF1ZW5jZSBvZiBlbGVtZW50cyB0aGF0IGFyZSBhY2Nlc3NlZCB2aWEgdW5wYWNraW5nIChzZWUgbGF0ZXIgaW4gdGhpcyBzZWN0aW9uKSBvciBpbmRleGluZyAob3IgZXZlbiBieSBhdHRyaWJ1dGUgaW4gdGhlIGNhc2Ugb2YgYG5hbWVkdHVwbGVzYCkuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG4jIyA1LjQuXFxuLi4uXFxuLiBTZXQgb2JqZWN0c1xcbi4uLlxcbiMjIDUuNS4gRGljdGlvbmFyaWVzXFx1MDBiNlxcblxcbkFub3RoZXIgdXNlZnVsIGRhdGEgdHlwZSBidWlsdCBpbnRvIFB5dGhvbiBpcyB0aGUgZGljdGlvbmFyeSAoc2VlIE1hcHBpbmcgVHlwZXMgXFx1MjAxNCBkaWN0KS4gRGljdGlvbmFyaWVzIGFyZSBzb21ldGltZXMgZm91bmQgaW4gb3RoZXIgbGFuZ3VhZ2VzIGFzIFxcdTIwMWNhc3NvY2lhdGl2ZSBtZW1vcmllc1xcdTIwMWQgb3IgXFx1MjAxY2Fzc29jaWF0aXZlIGFycmF5c1xcdTIwMWQuIFVubGlrZSBzZXF1ZW5jZXMsIHdoaWNoIGFyZSBpbmRleGVkIGJ5IGEgcmFuZ2Ugb2YgbnVtYmVycywgZGljdGlvbmFyaWVzIGFyZSBpbmRleGVkIGJ5IGtleXMsIHdoaWNoIGNhbiBiZSBhbnkgaW1tdXRhYmxlIHR5cGU7IHN0cmluZ3MgYW5kIG51bWJlcnMgY2FuIGFsd2F5cyBiZSBrZXlzLiBUdXBsZXMgY2FuIGJlIHVzZWQgYXMga2V5cyBpZiB0aGV5IGNvbnRhaW4gb25seSBzdHJpbmdzLCBudW1iZXJzLCBvciB0dXBsZXM7IGlmIGEgdHVwbGUgY29udGFpbnMgYW55IG11dGFibGUgb2JqZWN0IGVpdGhlciBkaXJlY3RseSBvciBpbmRpcmVjdGx5LCBpdCBjYW5ub3QgYmUgdXNlZCBhcyBhIGtleS4gWW91IGNhblxcdTIwMTl0IHVzZSBsaXN0cyBhcyBrZXlzLCBzaW5jZSBsaXN0cyBjYW4gYmUgbW9kaWZpZWQgaW4gcGxhY2UgdXNpbmcgaW5kZXggYXNzaWdubWVudHMsIHNsaWNlIGFzc2lnbm1lbnRzLCBvciBtZXRob2RzIGxpa2UgYGFwcGVuZCgpYCBhbmQgYGV4dGVuZCgpYC5cIl19XSIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ3ZWJfc2VhcmNoIiwiaWQiOiI2ZTUxMjVhYy0zMTY0LTRhNzYtOWM1Mi1hYzM0NjBjZTM2ZTQiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJzdGF0dXMiOiJzdWNjZXNzIn0seyJjb250ZW50IjpbeyJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3NrZXB0aWMiLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QTZGNGNDTjV5aEQ0Q3JzOU0iLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjB9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InJlc2VhcmNoZXIiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtM2NkOC03OWYxLTg4YTEtY2U2Y2VlMjk0ZTc5LTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoidHJhbnNmZXJfdG9fc2tlcHRpYyIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsInRvdGFsX3Rva2VucyI6NzQxMSwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fSx7ImNvbnRlbnQiOiJTdWNjZXNzZnVsbHkgdHJhbnNmZXJyZWQgdG8gc2tlcHRpYyIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ0cmFuc2Zlcl90b19za2VwdGljIiwiaWQiOiIzODkwNThkMS0wODNmLTQ2MzItYmJjNS0wOWJmZDNhMGZmZDIiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMVhqajNhUmRXeWhCSmZQbVRKQnhRYTkiLCJzdGF0dXMiOiJzdWNjZXNzIn0seyJjb250ZW50IjpbeyJzaWduYXR1cmUiOiJDQVFTMVFnS0VBZ1NHQUk0QVVJSWRHaHBibXRwYm1jU0RONlRmVTZORE05NzFsRWJ6eG9NdlJVQTFKOW94M2ZKUVlUOUlqQXJDUVFHSEJQNHBSSFBhdlh6UmxiTWJhWUhDNTdZaitHYnhncjBoZWx2ZDJPSUwvbzZSYnQ0UEZBM0ZhSUZVbGdxOGdjN0tRSTlGdEdudlNBa0xCUnpJc1VwLzJkNVJkMnpnRXNma0ZWZVhzbjhXWVlaa2luQVFWVnk2QTdCM0xhRUpXMHhiL3g4OERpNjdmZWR3eDBRK0ZsWnR3Y21UM0RjYWExMUp2ZXlIUnIzZG0rekdTWlA2Q1hlWE5KOG5yVFo3VzFZUlR5c0Q2dzdvN0xwOGgxUndxNGY0bzhqK1FVZlkwdmplK3pKWnN4SFBtUU50M3lrZUtoOTloZ242ZGJpejYvRUhxbWJIalNBdTFNNFNxRURmcUlLVGhIWWVuYW1BWm1jUTRYQU9vNm1kUTYrNzdWZ3BWMWxDMS9rNTJJaVVvSmprR2xRcm5MQ3Z1dGlJN0tOK3NnSzhjMisyS0NKclBSQ3ZWVitCa1N0dlA1aldXcXp5dHBLUStuSHpoTDQ0OE9ySUE5Z2RKQzM5QzhlRHJYZVJuSTd0NEZxbGtYR29NbFpBVjlVTWNyUUhSeE9NaTh0T3lUemk5b3lCRkRUU3NRK0xKcWVyMHR6MUZ0OWdlazZ2emc1MFNwVkFGUWRMRVJ2OFIxWXFNYWVJK3hzdlRBRVc5TVJndlJNa3pMdXpSVDJndXczakQrRmppcDM3eGdLNVFJMFIrQ3NPQW1RUG9UWml6emNJeUFVQXVDSkZDZ3EvZ0QvQ2liTHhveW1pVlVJMWFuOVNudDNLcE5Sb1dkdldJbmUzM1YwcHY4UEhnOXlXMkp1TE9xUUdpbExDc0svUTNvUXIzdXBFTVh4WEdqQ0w3V1VpenZSWkdGRUVWK1ZwbFl0N3Y0dEtaTWo1cGszdmRlTTBNSjVYWmc3UHZVc1J6eWN4Y25yVDNLbm9jM3FNK3R0cVpmVHNiOGQ2VS9wUWlmcTdHdWFoRk9IcnZyb3FnSkxqKy84MVl5MXBaRmtqZkpnWTBYa2x6QW5BNkNGcS9ZVDBneGJWK1RINUlTK1dhWU9VekszNlM4czVxcU13aG5ndjRJekUzY3RYWHFuaElaWE56bnlWQnFiWWEvR3gyYkNqY0VqMm41UnlHWVJTLytNY3VsZ0xCem5ROU1rRzJEN1FQM01xL253SFhLTUlhTFM4Vks4aEczZDByQWpsdm5hdVhTaE5ySnh0a2o0cWt3bnhENExyRWprMXVlK1BMY2JMcVlXd0FyOG4wcys0MWpyc0lQVnl5Szl2RmFFWnJBOUthQnhrSnlpcXg1MEdaTm43Y2t4K21rSTE4Z0g1Mml5VlB0VThGS01YanVnQ2w2bVBMaUhZaURRZCtHNjNMdWRtT0t5czlpdFZuQkRCTk1QMmIvSTcrYklWUmxoZUdhWldyNk1BVEtsODZCM3krMGt1T3JMUk15V2NkaWJjMmlRa1ptKzhmN2RUVXdiS2JCSnNLT0ZZNzMvOG5sTC9KaVlwano2blVJaytJcVNqYUZ5SzZYRmxDY3VrV3BXSHBESm5TdmJFemV5N3FIeUNxSitJK29uOGlQOEdPdGVtcFFhUzAvNGtaR1N3bHJFNnhPajZFd0dwZmpiRFpLSGtQLzJSQWROWVBRcTN6N2JkVkpNSk5uRk16NmUzN216TXBDQmlod1BkU0ZUUkUzU0NZNjZ3dlRnSnpHQ1V5aEhiU2k1aTFhenpPWGQ4dUhDU3BULzZMMW82VlUwcGpvUjhLYWE0d0NXSEdmN1AvV0VkK2F2eXAwMkpudTFjbXBXMCtHcSt2SzloMHRkYjJJQ2xNL2VOZmRvdVFQK2VwOE1KVWpyYURSSUNtQUpRWTVvYlM3MXZuaU9vK3JlYUZ5MHpSZ0IiLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsic2lnbmF0dXJlIjoiQ0FRUy9SRUtFUWdTR0FJNEFVSUpibUZ5Y21GMGFXOXVFZ3dGaDNBVkpaUDNzOHkxSHEwYURHNzZ1aEhPTmFTY1hBR1NoaUl3R2dqTFhIVXgxeVpya0xQbGpqelRPUXljcExKaUtWd1hjeG0vRXpLSm0xczhNNVd4dVJFV0VsZFhjb2FOMC9tc0twa1JNVCtlTGI1aWg5ckFIVUxUb21VTnNVd0pldTBqckplNEdMYXp5N3NQNUt0akVPRUJLUXNYVWZvUTdmRGtaT1FVSFppOXp6UU40aEZObVlKVWYzT1ZxZjlUUU5kQzErbS8vSEFaUGZXbUF5Nkh0RHZtN2tnQkprQ0VvaUZubTd0bi9QWDFoWEdQUmdXYzZCVFhVTXIyV0E4YUt5cWdnUlViTUgyK0pZdjVpV1pkZENjMXVwMU5icjNzRjdXUGNvdDlaQmh5SFFYcjY5MnplSms2U0pneWdJVnJBU2JNbHkyYkJ4VFdKQjYyM2dWZ3pwQ21WU0xaRnBrS1hnUGNwOGhHeURLT3ZGNlNpUVNIcUdSeFFvZnI3VnMxYVBoZlppeFJPSkdEK01scmtnZ1ZBbkdNeVlJRFVacGF2dENlaVUxSm1CU3owMDg0YlhQdFQvdG5wc0tPblNQWVpRbzNCMEJYS2JxQS9Xc0tuZzgvVjNaOFV1YWpvNklWWjJoZ0ExV0RkdXFRT1FrRXhHVTZwRnVMaGViSWp1V3hTTkkvRGZmdlIvL0xOcUNmdmRuZjBodm9QdER4ZngvU2F3SXg0L3NwcTl4YndGekh0TGdvSGcxRjJ3Rnc3Y2xQRGxnMVB5RC92MnlKRzNWOVQ4R0w2akdQRTZyaGZpbC91NEhOdmY3QlcwZFF6Q1RUWGNxWnk0bkJ5eTArcCtTN1p4Zk1sNEtweGEyZ09oaFNOamZwZnVYMFFwTDRrTUU4SjFubGlGTjFkOEtxRVB1REkxOEJRSFVoRC9VZlkyOW9BMHg1MTBGak0vQnNMWjRuNXNwcGtvSWk3Q0ZvUXEyYUFLVk5mQzFLaWxoSXBSRCsxZHlEdFpRa2xOOGdacE9FU0h3Nk1MMVR0VTB5ZkFjYm1xb3hKNmhoanp3SDZDTXNUUUVFeThNYlJCTStRS0trSVJkUkNQSEdGVzJMa3VSNTArSjBwRi90cjZwcVZ2SmVvemZpTW5yRWlsQjI3VFpQNG5LVEpHOHJkLy92Q1dlbjB5N0NBLzhkQko5Z2RwNXR4N05WQTQ1azFnZGN4RVIxZ1lvdU5YM24xdkczL0IxRjQrOTN3WFc5TXFvNlVzNTlUUW1yYjlOcWE3cFNRTlNySDA2dzFHeHlCQWxrRmZxQWxhZmsxNmFNcHE0MTZUSllwR3k0M1dDVWlDY0xXbGhpV2d6MnNnT0xBWFA5YTV3ZVdBS1JDOEo2amtjVWtEZVhuVytIRlNpUE02KzlCbzZrVmhMVWhONXJjNG1IWk9acEY3cHRQcHNRTjRYQ3FMUHNxcFJILzhvNVhweUM2Wm5lVGVGMkV4dGpod1pWalhEd01iSXpWWUlidzF5MlBQR3c4ZEM3cGM0c0lWU25JNG9RQUgwdjY1MGdoZmhtTWV2V1c0aDRQeWxidy9ERzRmZGVUMys4ejBmOE1PU1R0ZmlweDA1L1F0dEhsQXZPRFJ4YTV5REN2T3o2bjNmMUJPME4vNE1iR3lPTTI3cVc0eDhtQlZvcnlVWldmc21oZmkwL0dmTnZCOCt5bFRHYllOWDI3T2NFRjVXWnROcWJXRXk3VjVHYm1aVVN4c3BjZzdGQlozajMvM0w3bXV2WlhvWmkxZjVlcnE0eTNsU0grb0Y3cVBGbnZpZXlMT0N3dWJjUXkrb3lvd2o0Y0QwaWRSZWNucCtzKzUyRDcrbXpGMWdwVytmK2NKYXNveGlnYnhrT2xvUzR5ZWlLdmdNNG5XU3BHRWoxMlJQeDVvODBFbXppWE45dDR6SnpXMWh0QTVoWEpKZUZ5bXBNWXR5Q1VDZXZSV1ZtclJZa3RRN1VuQ2E2dEwvQVZQaTFuaTVYQ1hIZkxPdXFmSzVLTFl4andWdGRoaGZtQ0x2cGFLVk5DRU9DZjRuTkdIcUQ5OHdVYjBlaldOd0RZQm5KR2tCUnc5UlZzcGZ3TmRvV29WY24ycXY4eXdHVXA3eWMvc2syTHBJNlZ6aVJ5dldxYkdORzFaSnFWVnV0UmM0NitzT0U0VlNlUzIvQ1ROS05hNk5NQ2YvaHh4YVVWc1lmS2RQK1MxTGM0aWFuS3Awc1V1Z1BwbXRveWRDMENQMlJ3am5ITGNuenhDbnZxeGxMUVdYTU5QVHN1MnlCY0hsV0pMdGNtZGcvTWNMYWd5N2w1a0c2c043WnhKTEQva0F4OEU2dXFvaFArSy9qTVNyWXVRSm5ocjR0ZjRiaVdGWTNKemYvS3BBWXlWU3ErcGJmRktYTWJMOW5IRW9aUVQxNVd5YWJkNkRNQXEzUVpPdGhNNXBVemROcVVYa2NySTFQd2pBU0FnNGRWVS8ybys5OG1UWG55aFA3VmxFa1RuZGRwd3lKRitZQ0NjMDFwcWE2Wlg5OXBuUitZT3BJcmI1RHRQY0txMHQ1Y0ZDRHlReE4xbFV1cVpjWXVqdlgvR0M3KzZJcFJUei9Oa0VZLzVjbnRPZlpKeWQ2U2tVeC9KUjVBSklSUENOZWliMHJjV25uRm4wWEs4czIyeDQwd0xqRXR1ZlhETzkreXJhZjRrSHhsQkkzU0IwbWVNamVwWDUwQ0poQXg3TmVra1JGbndvZWNMZnFJeXJqWHJnQVpzVE1QaVg5NHNuamRyemlzQVh0QkthbnVsaWxTeE9mVzJIU21mbmswTDJHWEhKSjNTK3ZDYmdSRW1sOWRla2ZtSGhtd0VZdHYza2ZodGg3RVJJd1ZUOGo2OU90SUJDbWVUSUp1QTdvNzNQWGMxTEoyd2h1Wks3RlplNk5UT3M2MU1QN0pWbEljNnRiaHRPQmszTlhVOHBCUE9DWTZRNTVkNjNGdHBVVS9LQ2JRMEsyV1E5bzVqTkd3Tk5KNkN3NUZEQ3QreDVZSmVQMWQyUXpEU2lxK2NTWnF3cUNaVmNOb0FmdEdHemdyUlo0dmtwYlAweFFaSEN0V3NVd0hWVGpyWTROZlJEakhkVlBSbjhKOVh0b29jL2hJV2txekZhY3pZVGxhcHUyUkNKcWU4YUVvM2p6V1pDRUo3QWlCekJZcmFZRGdPZmVpUXllSlVzYmJYSGRHS2lpdzIyQmRwNE0rWXo2NzFNWjBpZTlFV1JrczBRMkRIR1VUbmU5UzR0QUFKUEdiVWY0eHRDTW9KNDBRNFFXMDVRTFNaeTN3TkZBVUZZbVhFNEVDREZCOCtJRWVORkZGL3RnQW5maDhHdkZoLzF4WStMRStvek1taW5qU1J3aHEwOXpRQUVrTkZ5dTBJS2d2RGt5T0kraWNjRjdkb1dMVFVhc1B4TEgxeEdQQlNLa2VXQ25Tb1gvNEMyYzlkdTY2QmJSWGFtSHFhZStWQ2lTNkJOOTUzaVlLT3U4ck1rbmVXK1RGalYwQ3pBZjhQTkc5TEJheVVnWTVVdzl4MFNhcDZ6STRQcGFaS0g0ZDlrdGtGVSs5MXlCYk41U1J0ZGQ4L0VRYXRKWG1CTmZ3N2tScmxsK0xqd1p3ZVdMRWIwalQxUnBHdURxSExkTFRNTWdvd1NjenJwMnpDc0pkUUgvc2VhWXJwd2tDRlI4ZFJqTTdKUlVrVmExQTBTdmRaQVg3NXZ1QnZ2TEJ6TjJjK1JidDVvcXk5M2xxSUJJaktiN3cyVWtSVGI0cEJXY05LTlBVZ3QzYXovUHhReEVhc2FsdGRtdXl2c1hZYnVyM1ZQTm44SnpOLzZKNU4rUUNuejU3MVRjY2FwQ21rUHZGRStnYXlYVStVRjFWS1l5RmNVdmxldjdsc1FsbWpxeksyUUE4WDdRM1R6RmFwcWh2SktsdDdkekJIcmFxWkJnUGdiOU40b0tOYXlpOEZaVDcxK2thWWhJaEZyRVNLWVJIZkhrajVuM2tmaGdMeSt4K2pMRGJLeE1nZ2tnSG5KejlSdUVIWlhCV3RrWUFRPT0iLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsiaWQiOiJ0b29sdV8wMUg2ZlY4SnZSSk52WHk0N2VSbmQzakgiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6e30sIm5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHdBTVJkUzIxRmVKZnpRaE0zcyIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo3Mzk2LCJvdXRwdXRfdG9rZW5zIjo3NzAsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjczOX0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoic2tlcHRpYyIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy00YWU5LTc2YzAtODlhNi00ZjBiMTE0ZTMwMzAtMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzM5Niwib3V0cHV0X3Rva2VucyI6NzcwLCJ0b3RhbF90b2tlbnMiOjgxNjYsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjczOX19fSx7ImNvbnRlbnQiOiJTdWNjZXNzZnVsbHkgdHJhbnNmZXJyZWQgdG8gdmVyaWZpZXIiLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7fSwidHlwZSI6InRvb2wiLCJuYW1lIjoidHJhbnNmZXJfdG9fdmVyaWZpZXIiLCJpZCI6ImMwNjQ1NjNhLTk3NGYtNGYwZi1hYjRhLTA4MGU2M2MwMWNmNSIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsInN0YXR1cyI6InN1Y2Nlc3MifV0sImFjdGl2ZV9hZ2VudCI6InZlcmlmaWVyIn0=" + } + } + ], + "events": [ + { + "timeUnixNano": "1790968032543832000", + "name": "exception", + "attributes": [ + { + "key": "exception.type", + "value": { + "stringValue": "Exception" + } + }, + { + "key": "exception.message", + "value": { + "stringValue": "ParentCommand(Command(graph='verifier:544dea40-e16b-458c-1464-128e1b9b10a9', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', id='389058d1-083f-4632-bbc5-09bfd3a0ffd2', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9'), AIMessage(content=[{'signature': 'CAQS1QgKEAgSGAI4AUIIdGhpbmtpbmcSDN6TfU6NDM971lEbzxoMvRUA1J9ox3fJQYT9IjArCQQGHBP4pRHPavXzRlbMbaYHC57Yj+Gbxgr0helvd2OIL/o6Rbt4PFA3FaIFUlgq8gc7KQI9FtGnvSAkLBRzIsUp/2d5Rd2zgEsfkFVeXsn8WYYZkinAQVVy6A7B3LaEJW0xb/x88Di67fedwx0Q+FlZtwcmT3Dcaa11JveyHRr3dm+zGSZP6CXeXNJ8nrTZ7W1YRTysD6w7o7Lp8h1Rwq4f4o8j+QUfY0vje+zJZsxHPmQNt3ykeKh99hgn6dbiz6/EHqmbHjSAu1M4SqEDfqIKThHYenamAZmcQ4XAOo6mdQ6+77VgpV1lC1/k52IiUoJjkGlQrnLCvutiI7KN+sgK8c2+2KCJrPRCvVV+BkStvP5jWWqzytpKQ+nHzhL448OrIA9gdJC39C8eDrXeRnI7t4FqlkXGoMlZAV9UMcrQHRxOMi8tOyTzi9oyBFDTSsQ+LJqer0tz1Ft9gek6vzg50SpVAFQdLERv8R1YqMaeI+xsvTAEW9MRgvRMkzLuzRT2guw3jD+Fjip37xgK5QI0R+CsOAmQPoTZizzcIyAUAuCJFCgq/gD/CibLxoymiVUI1an9Snt3KpNRoWdvWIne33V0pv8PHg9yW2JuLOqQGilLCsK/Q3oQr3upEMXxXGjCL7WUizvRZGFEEV+VplYt7v4tKZMj5pk3vdeM0MJ5XZg7PvUsRzycxcnrT3Knoc3qM+ttqZfTsb8d6U/pQifq7GuahFOHrvroqgJLj+/81Yy1pZFkjfJgY0XklzAnA6CFq/YT0gxbV+TH5IS+WaYOUzK36S8s5qqMwhngv4IzE3ctXXqnhIZXNznyVBqbYa/Gx2bCjcEj2n5RyGYRS/+MculgLBznQ9MkG2D7QP3Mq/nwHXKMIaLS8VK8hG3d0rAjlvnauXShNrJxtkj4qkwnxD4LrEjk1ue+PLcbLqYWwAr8n0s+41jrsIPVyyK9vFaEZrA9KaBxkJyiqx50GZNn7ckx+mkI18gH52iyVPtU8FKMXjugCl6mPLiHYiDQd+G63LudmOKys9itVnBDBNMP2b/I7+bIVRlheGaZWr6MATKl86B3y+0kuOrLRMyWcdibc2iQkZm+8f7dTUwbKbBJsKOFY73/8nlL/JiYpjz6nUIk+IqSjaFyK6XFlCcukWpWHpDJnSvbEzey7qHyCqJ+I+on8iP8GOtempQaS0/4kZGSwlrE6xOj6EwGpfjbDZKHkP/2RAdNYPQq3z7bdVJMJNnFMz6e37mzMpCBihwPdSFTRE3SCY66wvTgJzGCUyhHbSi5i1azzOXd8uHCSpT/6L1o6VU0pjoR8Kaa4wCWHGf7P/WEd+avyp02Jnu1cmpW0+Gq+vK9h0tdb2IClM/eNfdouQP+ep8MJUjraDRICmAJQY5obS71vniOo+reaFy0zRgB', 'thinking': '', 'type': 'thinking'}, {'signature': 'CAQS/REKEQgSGAI4AUIJbmFycmF0aW9uEgwFh3AVJZP3s8y1Hq0aDG76uhHONaScXAGShiIwGgjLXHUx1yZrkLPljjzTOQycpLJiKVwXcxm/EzKJm1s8M5WxuREWEldXcoaN0/msKpkRMT+eLb5ih9rAHULTomUNsUwJeu0jrJe4GLazy7sP5KtjEOEBKQsXUfoQ7fDkZOQUHZi9zzQN4hFNmYJUf3OVqf9TQNdC1+m//HAZPfWmAy6HtDvm7kgBJkCEoiFnm7tn/PX1hXGPRgWc6BTXUMr2WA8aKyqggRUbMH2+JYv5iWZddCc1up1Nbr3sF7WPcot9ZBhyHQXr692zeJk6SJgygIVrASbMly2bBxTWJB623gVgzpCmVSLZFpkKXgPcp8hGyDKOvF6SiQSHqGRxQofr7Vs1aPhfZixROJGD+MlrkggVAnGMyYIDUZpavtCeiU1JmBSz0084bXPtT/tnpsKOnSPYZQo3B0BXKbqA/WsKng8/V3Z8Uuajo6IVZ2hgA1WDduqQOQkExGU6pFuLhebIjuWxSNI/DffvR//LNqCfvdnf0hvoPtDxfx/SawIx4/spq9xbwFzHtLgoHg1F2wFw7clPDlg1PyD/v2yJG3V9T8GL6jGPE6rhfil/u4HNvf7BW0dQzCTTXcqZy4nByy0+p+S7ZxfMl4Kpxa2gOhhSNjfpfuX0QpL4kME8J1nliFN1d8KqEPuDI18BQHUhD/UfY29oA0x510FjM/BsLZ4n5sppkoIi7CFoQq2aAKVNfC1KilhIpRD+1dyDtZQklN8gZpOESHw6ML1TtU0yfAcbmqoxJ6hhjzwH6CMsTQEEy8MbRBM+QKKkIRdRCPHGFW2LkuR50+J0pF/tr6pqVvJeozfiMnrEilB27TZP4nKTJG8rd//vCWen0y7CA/8dBJ9gdp5tx7NVA45k1gdcxER1gYouNX3n1vG3/B1F4+93wXW9Mqo6Us59TQmrb9Nqa7pSQNSrH06w1GxyBAlkFfqAlafk16aMpq416TJYpGy43WCUiCcLWlhiWgz2sgOLAXP9a5weWAKRC8J6jkcUkDeXnW+HFSiPM6+9Bo6kVhLUhN5rc4mHZOZpF7ptPpsQN4XCqLPsqpRH/8o5XpyC6ZneTeF2ExtjhwZVjXDwMbIzVYIbw1y2PPGw8dC7pc4sIVSnI4oQAH0v650ghfhmMevWW4h4Pylbw/DG4fdeT3+8z0f8MOSTtfipx05/QttHlAvODRxa5yDCvOz6n3f1BO0N/4MbGyOM27qW4x8mBVoryUZWfsmhfi0/GfNvB8+ylTGbYNX27OcEF5WZtNqbWEy7V5GbmZUSxspcg7FBZ3j3/3L7muvZXoZi1f5erq4y3lSH+oF7qPFnvieyLOCwubcQy+oyowj4cD0idRecnp+s+52D7+mzF1gpW+f+cJasoxigbxkOloS4yeiKvgM4nWSpGEj12RPx5o80EmziXN9t4zJzW1htA5hXJJeFympMYtyCUCevRWVmrRYktQ7UnCa6tL/AVPi1ni5XCXHfLOuqfK5KLYxjwVtdhhfmCLvpaKVNCEOCf4nNGHqD98wUb0ejWNwDYBnJGkBRw9RVspfwNdoWoVcn2qv8ywGUp7yc/sk2LpI6VziRyvWqbGNG1ZJqVVutRc46+sOE4VSeS2/CTNKNa6NMCf/hxxaUVsYfKdP+S1Lc4ianKp0sUugPpmtoydC0CP2RwjnHLcnzxCnvqxlLQWXMNPTsu2yBcHlWJLtcmdg/McLagy7l5kG6sN7ZxJLD/kAx8E6uqohP+K/jMSrYuQJnhr4tf4biWFY3Jzf/KpAYyVSq+pbfFKXMbL9nHEoZQT15Wyabd6DMAq3QZOthM5pUzdNqUXkcrI1PwjASAg4dVU/2o+98mTXnyhP7VlEkTnddpwyJF+YCCc01pqa6ZX99pnR+YOpIrb5DtPcKq0t5cFCDyQxN1lUuqZcYujvX/GC7+6IpRTz/NkEY/5cntOfZJyd6SkUx/JR5AJIRPCNeib0rcWnnFn0XK8s22x40wLjEtufXDO9+yraf4kHxlBI3SB0meMjepX50CJhAx7NekkRFnwoecLfqIyrjXrgAZsTMPiX94snjdrzisAXtBKanulilSxOfW2HSmfnk0L2GXHJJ3S+vCbgREml9dekfmHhmwEYtv3kfhth7ERIwVT8j69OtIBCmeTIJuA7o73PXc1LJ2whuZK7FZe6NTOs61MP7JVlIc6tbhtOBk3NXU8pBPOCY6Q55d63FtpUU/KCbQ0K2WQ9o5jNGwNNJ6Cw5FDCt+x5YJeP1d2QzDSiq+cSZqwqCZVcNoAftGGzgrRZ4vkpbP0xQZHCtWsUwHVTjrY4NfRDjHdVPRn8J9Xtooc/hIWkqzFaczYTlapu2RCJqe8aEo3jzWZCEJ7AiBzBYraYDgOfeiQyeJUsbbXHdGKiiw22Bdp4M+Yz671MZ0ie9EWRks0Q2DHGUTne9S4tAAJPGbUf4xtCMoJ40Q4QW05QLSZy3wNFAUFYmXE4ECDFB8+IEeNFFF/tgAnfh8GvFh/1xY+LE+ozMminjSRwhq09zQAEkNFyu0IKgvDkyOI+iccF7doWLTUasPxLH1xGPBSKkeWCnSoX/4C2c9du66BbRXamHqae+VCiS6BN953iYKOu8rMkneW+TFjV0CzAf8PNG9LBayUgY5Uw9x0Sap6zI4PpaZKH4d9ktkFU+91yBbN5SRtdd8/EQatJXmBNfw7kRrll+LjwZweWLEb0jT1RpGuDqHLdLTMMgowSczrp2zCsJdQH/seaYrpwkCFR8dRjM7JRUkVa1A0SvdZAX75vuBvvLBzN2c+Rbt5oqy93lqIBIjKb7w2UkRTb4pBWcNKNPUgt3az/PxQxEasaltdmuyvsXYbur3VPNn8JzN/6J5N+QCnz571TccapCmkPvFE+gayXU+UF1VKYyFcUvlev7lsQlmjqzK2QA8X7Q3TzFapqhvJKlt7dzBHraqZBgPgb9N4oKNayi8FZT71+kaYhIhFrESKYRHfHkj5n3kfhgLy+x+jLDbKxMggkgHnJz9RuEHZXBWtkYAQ==', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_verifier', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAMRdS21FeJfzQhM3s', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7396, 'output_tokens': 770, 'output_tokens_details': {'thinking_tokens': 739}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='skeptic', id='lc_run--01a0fe03-4ae9-76c0-89a6-4f0b114e3030-0', tool_calls=[{'name': 'transfer_to_verifier', 'args': {}, 'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7396, 'output_tokens': 770, 'total_tokens': 8166, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 739}}), ToolMessage(content='Successfully transferred to verifier', name='transfer_to_verifier', id='c064563a-974f-4f0f-ab4a-080e63c01cf5', tool_call_id='toolu_01H6fV8JvRJNvXy47eRnd3jH'), AIMessage(content=[{'signature': 'CAQS9gUKEAgSGAI4AUIIdGhpbmtpbmcSDGFyJ1MXoY+Ai6NkbxoMRd2TrUfUU7WrPpA2IjDgy8dUplcZKc7DnJwNLTBfH5UNR+nGnakSPQia8UVIzrjkOnJ2fLrWUsA2oEO8yRsqkwXkkDCsyhB97dwj57RMt91XzgWhL9luNEGO92yW0rNFGoMSUVOxPIrFc5RjyrhKEc3+1RnpxCkylq+LNlQ5NFxLn6v2cF5DzqpvBHNR7XAig+urJIU3gXydCkaGZZDetJtQOUOXgZi9z9100NpdAEkXDo2ICHCGTGpVc0J/pbXxWRBxb6B1/Gl0Yjn68V6HlesH7IoC7GR/3HwRu9NSDBG4ttfQjEWcg+PeAea4Rn7jzx+Tm9H7L1joTKVgDbQg5iKMLfjvPqlnksCLtXOt8jSIS5OYLN7a/7TX6/w5d34vnw8vPDmU1UYi0Q72qUWhUa2vw8OXNpcKL7Bp0lNfBS8rvHY+vBY8/haAoTL3wvLFU1fT+4z/vA3BcsigZDYlfJeDYcxXmRuqxzxEGpXCRe9s1HWtID+41pHu6lpZHkXaghpc6TZHLxNwcVhTb6fMQ7Qo+2L6Pvklh1m32peodxeg2YiPhdpl3w/xkOEu5/JEF7ZBxpdUL+u3wkO5pUlsqfgX7uhzv7HC2WpSWVimrwiQwQOUx0+JPrLtw+lYfq7I3jruo0GW0guWvnuf7JM3YLj84q+JgHomnSSZnnZmrL0UaRvrVyLo877F39ccLS7XoZnnui8J4P58ZTmiLkEJeGcx1Iuag2e33339O7Yew/59tLbQtIGh9VWMwHBnFx39S8/4LEFrK5HldPQhOrloyTtkRs66iQRKtxMKS0VI6QTKZDAbs7a7H1yBv+mJi9Xg4uKmyU6qnn7zEKjjY2bIlCwezx4LLGP5HD3Zr6fZsmdG1UHzDsBvYG2oCI0oqEKM+valsT6hU8MRjbQBD/JJ8JJN/iBGlhBAUHo0UmhhtR2euTVpZWlMnhmpXAdKDXSfPdaPohgB', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_red_team', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAsabDquegHi6YUpCn', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7509, 'output_tokens': 88, 'output_tokens_details': {'thinking_tokens': 55}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='verifier', id='lc_run--01a0fe03-6682-7d21-8c15-ffbce9422c9f-0', tool_calls=[{'name': 'transfer_to_red_team', 'args': {}, 'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7509, 'output_tokens': 88, 'total_tokens': 7597, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 55}}), ToolMessage(content='Successfully transferred to red_team', name='transfer_to_red_team', tool_call_id='toolu_01XEAuhQHCBdyCwtymZwFfDi')], 'active_agent': 'red_team'}, goto='red_team'))Traceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 707, in invoke\n input = context.run(step.invoke, input, config, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 3913, in invoke\n for chunk in self.stream(\n ~~~~~~~~~~~^\n input,\n ^^^^^^\n ...<11 lines>...\n **kwargs,\n ^^^^^^^^^\n ):\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 2967, in stream\n for _ in runner.tick(\n ~~~~~~~~~~~^\n [t for t in loop.tasks.values() if not t.writes],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<2 lines>...\n schedule_task=loop.accept_push,\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ):\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_runner.py\", line 207, in tick\n run_with_retry(\n ~~~~~~~~~~~~~~^\n t,\n ^^\n ...<10 lines>...\n },\n ^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_retry.py\", line 617, in run_with_retry\n return task.proc.invoke(task.input, config)\n ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 709, in invoke\n input = step.invoke(input, config)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 447, in invoke\n ret = self.func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 84, in _write\n self.do_write(\n ~~~~~~~~~~~~~^\n config,\n ^^^^^^^\n writes,\n ^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 126, in do_write\n write(_assemble_writes(writes))\n ~~~~~~~~~~~~~~~~^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 181, in _assemble_writes\n if ww := w.mapper(w.value):\n ~~~~~~~~^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/graph/state.py\", line 1762, in _control_branch\n raise ParentCommand(command)\n\n\nlanggraph.errors.ParentCommand: Command(graph='verifier:544dea40-e16b-458c-1464-128e1b9b10a9', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', id='389058d1-083f-4632-bbc5-09bfd3a0ffd2', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9'), AIMessage(content=[{'signature': 'CAQS1QgKEAgSGAI4AUIIdGhpbmtpbmcSDN6TfU6NDM971lEbzxoMvRUA1J9ox3fJQYT9IjArCQQGHBP4pRHPavXzRlbMbaYHC57Yj+Gbxgr0helvd2OIL/o6Rbt4PFA3FaIFUlgq8gc7KQI9FtGnvSAkLBRzIsUp/2d5Rd2zgEsfkFVeXsn8WYYZkinAQVVy6A7B3LaEJW0xb/x88Di67fedwx0Q+FlZtwcmT3Dcaa11JveyHRr3dm+zGSZP6CXeXNJ8nrTZ7W1YRTysD6w7o7Lp8h1Rwq4f4o8j+QUfY0vje+zJZsxHPmQNt3ykeKh99hgn6dbiz6/EHqmbHjSAu1M4SqEDfqIKThHYenamAZmcQ4XAOo6mdQ6+77VgpV1lC1/k52IiUoJjkGlQrnLCvutiI7KN+sgK8c2+2KCJrPRCvVV+BkStvP5jWWqzytpKQ+nHzhL448OrIA9gdJC39C8eDrXeRnI7t4FqlkXGoMlZAV9UMcrQHRxOMi8tOyTzi9oyBFDTSsQ+LJqer0tz1Ft9gek6vzg50SpVAFQdLERv8R1YqMaeI+xsvTAEW9MRgvRMkzLuzRT2guw3jD+Fjip37xgK5QI0R+CsOAmQPoTZizzcIyAUAuCJFCgq/gD/CibLxoymiVUI1an9Snt3KpNRoWdvWIne33V0pv8PHg9yW2JuLOqQGilLCsK/Q3oQr3upEMXxXGjCL7WUizvRZGFEEV+VplYt7v4tKZMj5pk3vdeM0MJ5XZg7PvUsRzycxcnrT3Knoc3qM+ttqZfTsb8d6U/pQifq7GuahFOHrvroqgJLj+/81Yy1pZFkjfJgY0XklzAnA6CFq/YT0gxbV+TH5IS+WaYOUzK36S8s5qqMwhngv4IzE3ctXXqnhIZXNznyVBqbYa/Gx2bCjcEj2n5RyGYRS/+MculgLBznQ9MkG2D7QP3Mq/nwHXKMIaLS8VK8hG3d0rAjlvnauXShNrJxtkj4qkwnxD4LrEjk1ue+PLcbLqYWwAr8n0s+41jrsIPVyyK9vFaEZrA9KaBxkJyiqx50GZNn7ckx+mkI18gH52iyVPtU8FKMXjugCl6mPLiHYiDQd+G63LudmOKys9itVnBDBNMP2b/I7+bIVRlheGaZWr6MATKl86B3y+0kuOrLRMyWcdibc2iQkZm+8f7dTUwbKbBJsKOFY73/8nlL/JiYpjz6nUIk+IqSjaFyK6XFlCcukWpWHpDJnSvbEzey7qHyCqJ+I+on8iP8GOtempQaS0/4kZGSwlrE6xOj6EwGpfjbDZKHkP/2RAdNYPQq3z7bdVJMJNnFMz6e37mzMpCBihwPdSFTRE3SCY66wvTgJzGCUyhHbSi5i1azzOXd8uHCSpT/6L1o6VU0pjoR8Kaa4wCWHGf7P/WEd+avyp02Jnu1cmpW0+Gq+vK9h0tdb2IClM/eNfdouQP+ep8MJUjraDRICmAJQY5obS71vniOo+reaFy0zRgB', 'thinking': '', 'type': 'thinking'}, {'signature': 'CAQS/REKEQgSGAI4AUIJbmFycmF0aW9uEgwFh3AVJZP3s8y1Hq0aDG76uhHONaScXAGShiIwGgjLXHUx1yZrkLPljjzTOQycpLJiKVwXcxm/EzKJm1s8M5WxuREWEldXcoaN0/msKpkRMT+eLb5ih9rAHULTomUNsUwJeu0jrJe4GLazy7sP5KtjEOEBKQsXUfoQ7fDkZOQUHZi9zzQN4hFNmYJUf3OVqf9TQNdC1+m//HAZPfWmAy6HtDvm7kgBJkCEoiFnm7tn/PX1hXGPRgWc6BTXUMr2WA8aKyqggRUbMH2+JYv5iWZddCc1up1Nbr3sF7WPcot9ZBhyHQXr692zeJk6SJgygIVrASbMly2bBxTWJB623gVgzpCmVSLZFpkKXgPcp8hGyDKOvF6SiQSHqGRxQofr7Vs1aPhfZixROJGD+MlrkggVAnGMyYIDUZpavtCeiU1JmBSz0084bXPtT/tnpsKOnSPYZQo3B0BXKbqA/WsKng8/V3Z8Uuajo6IVZ2hgA1WDduqQOQkExGU6pFuLhebIjuWxSNI/DffvR//LNqCfvdnf0hvoPtDxfx/SawIx4/spq9xbwFzHtLgoHg1F2wFw7clPDlg1PyD/v2yJG3V9T8GL6jGPE6rhfil/u4HNvf7BW0dQzCTTXcqZy4nByy0+p+S7ZxfMl4Kpxa2gOhhSNjfpfuX0QpL4kME8J1nliFN1d8KqEPuDI18BQHUhD/UfY29oA0x510FjM/BsLZ4n5sppkoIi7CFoQq2aAKVNfC1KilhIpRD+1dyDtZQklN8gZpOESHw6ML1TtU0yfAcbmqoxJ6hhjzwH6CMsTQEEy8MbRBM+QKKkIRdRCPHGFW2LkuR50+J0pF/tr6pqVvJeozfiMnrEilB27TZP4nKTJG8rd//vCWen0y7CA/8dBJ9gdp5tx7NVA45k1gdcxER1gYouNX3n1vG3/B1F4+93wXW9Mqo6Us59TQmrb9Nqa7pSQNSrH06w1GxyBAlkFfqAlafk16aMpq416TJYpGy43WCUiCcLWlhiWgz2sgOLAXP9a5weWAKRC8J6jkcUkDeXnW+HFSiPM6+9Bo6kVhLUhN5rc4mHZOZpF7ptPpsQN4XCqLPsqpRH/8o5XpyC6ZneTeF2ExtjhwZVjXDwMbIzVYIbw1y2PPGw8dC7pc4sIVSnI4oQAH0v650ghfhmMevWW4h4Pylbw/DG4fdeT3+8z0f8MOSTtfipx05/QttHlAvODRxa5yDCvOz6n3f1BO0N/4MbGyOM27qW4x8mBVoryUZWfsmhfi0/GfNvB8+ylTGbYNX27OcEF5WZtNqbWEy7V5GbmZUSxspcg7FBZ3j3/3L7muvZXoZi1f5erq4y3lSH+oF7qPFnvieyLOCwubcQy+oyowj4cD0idRecnp+s+52D7+mzF1gpW+f+cJasoxigbxkOloS4yeiKvgM4nWSpGEj12RPx5o80EmziXN9t4zJzW1htA5hXJJeFympMYtyCUCevRWVmrRYktQ7UnCa6tL/AVPi1ni5XCXHfLOuqfK5KLYxjwVtdhhfmCLvpaKVNCEOCf4nNGHqD98wUb0ejWNwDYBnJGkBRw9RVspfwNdoWoVcn2qv8ywGUp7yc/sk2LpI6VziRyvWqbGNG1ZJqVVutRc46+sOE4VSeS2/CTNKNa6NMCf/hxxaUVsYfKdP+S1Lc4ianKp0sUugPpmtoydC0CP2RwjnHLcnzxCnvqxlLQWXMNPTsu2yBcHlWJLtcmdg/McLagy7l5kG6sN7ZxJLD/kAx8E6uqohP+K/jMSrYuQJnhr4tf4biWFY3Jzf/KpAYyVSq+pbfFKXMbL9nHEoZQT15Wyabd6DMAq3QZOthM5pUzdNqUXkcrI1PwjASAg4dVU/2o+98mTXnyhP7VlEkTnddpwyJF+YCCc01pqa6ZX99pnR+YOpIrb5DtPcKq0t5cFCDyQxN1lUuqZcYujvX/GC7+6IpRTz/NkEY/5cntOfZJyd6SkUx/JR5AJIRPCNeib0rcWnnFn0XK8s22x40wLjEtufXDO9+yraf4kHxlBI3SB0meMjepX50CJhAx7NekkRFnwoecLfqIyrjXrgAZsTMPiX94snjdrzisAXtBKanulilSxOfW2HSmfnk0L2GXHJJ3S+vCbgREml9dekfmHhmwEYtv3kfhth7ERIwVT8j69OtIBCmeTIJuA7o73PXc1LJ2whuZK7FZe6NTOs61MP7JVlIc6tbhtOBk3NXU8pBPOCY6Q55d63FtpUU/KCbQ0K2WQ9o5jNGwNNJ6Cw5FDCt+x5YJeP1d2QzDSiq+cSZqwqCZVcNoAftGGzgrRZ4vkpbP0xQZHCtWsUwHVTjrY4NfRDjHdVPRn8J9Xtooc/hIWkqzFaczYTlapu2RCJqe8aEo3jzWZCEJ7AiBzBYraYDgOfeiQyeJUsbbXHdGKiiw22Bdp4M+Yz671MZ0ie9EWRks0Q2DHGUTne9S4tAAJPGbUf4xtCMoJ40Q4QW05QLSZy3wNFAUFYmXE4ECDFB8+IEeNFFF/tgAnfh8GvFh/1xY+LE+ozMminjSRwhq09zQAEkNFyu0IKgvDkyOI+iccF7doWLTUasPxLH1xGPBSKkeWCnSoX/4C2c9du66BbRXamHqae+VCiS6BN953iYKOu8rMkneW+TFjV0CzAf8PNG9LBayUgY5Uw9x0Sap6zI4PpaZKH4d9ktkFU+91yBbN5SRtdd8/EQatJXmBNfw7kRrll+LjwZweWLEb0jT1RpGuDqHLdLTMMgowSczrp2zCsJdQH/seaYrpwkCFR8dRjM7JRUkVa1A0SvdZAX75vuBvvLBzN2c+Rbt5oqy93lqIBIjKb7w2UkRTb4pBWcNKNPUgt3az/PxQxEasaltdmuyvsXYbur3VPNn8JzN/6J5N+QCnz571TccapCmkPvFE+gayXU+UF1VKYyFcUvlev7lsQlmjqzK2QA8X7Q3TzFapqhvJKlt7dzBHraqZBgPgb9N4oKNayi8FZT71+kaYhIhFrESKYRHfHkj5n3kfhgLy+x+jLDbKxMggkgHnJz9RuEHZXBWtkYAQ==', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_verifier', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAMRdS21FeJfzQhM3s', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7396, 'output_tokens': 770, 'output_tokens_details': {'thinking_tokens': 739}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='skeptic', id='lc_run--01a0fe03-4ae9-76c0-89a6-4f0b114e3030-0', tool_calls=[{'name': 'transfer_to_verifier', 'args': {}, 'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7396, 'output_tokens': 770, 'total_tokens': 8166, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 739}}), ToolMessage(content='Successfully transferred to verifier', name='transfer_to_verifier', id='c064563a-974f-4f0f-ab4a-080e63c01cf5', tool_call_id='toolu_01H6fV8JvRJNvXy47eRnd3jH'), AIMessage(content=[{'signature': 'CAQS9gUKEAgSGAI4AUIIdGhpbmtpbmcSDGFyJ1MXoY+Ai6NkbxoMRd2TrUfUU7WrPpA2IjDgy8dUplcZKc7DnJwNLTBfH5UNR+nGnakSPQia8UVIzrjkOnJ2fLrWUsA2oEO8yRsqkwXkkDCsyhB97dwj57RMt91XzgWhL9luNEGO92yW0rNFGoMSUVOxPIrFc5RjyrhKEc3+1RnpxCkylq+LNlQ5NFxLn6v2cF5DzqpvBHNR7XAig+urJIU3gXydCkaGZZDetJtQOUOXgZi9z9100NpdAEkXDo2ICHCGTGpVc0J/pbXxWRBxb6B1/Gl0Yjn68V6HlesH7IoC7GR/3HwRu9NSDBG4ttfQjEWcg+PeAea4Rn7jzx+Tm9H7L1joTKVgDbQg5iKMLfjvPqlnksCLtXOt8jSIS5OYLN7a/7TX6/w5d34vnw8vPDmU1UYi0Q72qUWhUa2vw8OXNpcKL7Bp0lNfBS8rvHY+vBY8/haAoTL3wvLFU1fT+4z/vA3BcsigZDYlfJeDYcxXmRuqxzxEGpXCRe9s1HWtID+41pHu6lpZHkXaghpc6TZHLxNwcVhTb6fMQ7Qo+2L6Pvklh1m32peodxeg2YiPhdpl3w/xkOEu5/JEF7ZBxpdUL+u3wkO5pUlsqfgX7uhzv7HC2WpSWVimrwiQwQOUx0+JPrLtw+lYfq7I3jruo0GW0guWvnuf7JM3YLj84q+JgHomnSSZnnZmrL0UaRvrVyLo877F39ccLS7XoZnnui8J4P58ZTmiLkEJeGcx1Iuag2e33339O7Yew/59tLbQtIGh9VWMwHBnFx39S8/4LEFrK5HldPQhOrloyTtkRs66iQRKtxMKS0VI6QTKZDAbs7a7H1yBv+mJi9Xg4uKmyU6qnn7zEKjjY2bIlCwezx4LLGP5HD3Zr6fZsmdG1UHzDsBvYG2oCI0oqEKM+valsT6hU8MRjbQBD/JJ8JJN/iBGlhBAUHo0UmhhtR2euTVpZWlMnhmpXAdKDXSfPdaPohgB', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_red_team', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAsabDquegHi6YUpCn', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7509, 'output_tokens': 88, 'output_tokens_details': {'thinking_tokens': 55}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='verifier', id='lc_run--01a0fe03-6682-7d21-8c15-ffbce9422c9f-0', tool_calls=[{'name': 'transfer_to_red_team', 'args': {}, 'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7509, 'output_tokens': 88, 'total_tokens': 7597, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 55}}), ToolMessage(content='Successfully transferred to red_team', name='transfer_to_red_team', tool_call_id='toolu_01XEAuhQHCBdyCwtymZwFfDi')], 'active_agent': 'red_team'}, goto='red_team')" + } + }, + { + "key": "exception.stacktrace", + "value": { + "stringValue": "Exception: ParentCommand(Command(graph='verifier:544dea40-e16b-458c-1464-128e1b9b10a9', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', id='389058d1-083f-4632-bbc5-09bfd3a0ffd2', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9'), AIMessage(content=[{'signature': 'CAQS1QgKEAgSGAI4AUIIdGhpbmtpbmcSDN6TfU6NDM971lEbzxoMvRUA1J9ox3fJQYT9IjArCQQGHBP4pRHPavXzRlbMbaYHC57Yj+Gbxgr0helvd2OIL/o6Rbt4PFA3FaIFUlgq8gc7KQI9FtGnvSAkLBRzIsUp/2d5Rd2zgEsfkFVeXsn8WYYZkinAQVVy6A7B3LaEJW0xb/x88Di67fedwx0Q+FlZtwcmT3Dcaa11JveyHRr3dm+zGSZP6CXeXNJ8nrTZ7W1YRTysD6w7o7Lp8h1Rwq4f4o8j+QUfY0vje+zJZsxHPmQNt3ykeKh99hgn6dbiz6/EHqmbHjSAu1M4SqEDfqIKThHYenamAZmcQ4XAOo6mdQ6+77VgpV1lC1/k52IiUoJjkGlQrnLCvutiI7KN+sgK8c2+2KCJrPRCvVV+BkStvP5jWWqzytpKQ+nHzhL448OrIA9gdJC39C8eDrXeRnI7t4FqlkXGoMlZAV9UMcrQHRxOMi8tOyTzi9oyBFDTSsQ+LJqer0tz1Ft9gek6vzg50SpVAFQdLERv8R1YqMaeI+xsvTAEW9MRgvRMkzLuzRT2guw3jD+Fjip37xgK5QI0R+CsOAmQPoTZizzcIyAUAuCJFCgq/gD/CibLxoymiVUI1an9Snt3KpNRoWdvWIne33V0pv8PHg9yW2JuLOqQGilLCsK/Q3oQr3upEMXxXGjCL7WUizvRZGFEEV+VplYt7v4tKZMj5pk3vdeM0MJ5XZg7PvUsRzycxcnrT3Knoc3qM+ttqZfTsb8d6U/pQifq7GuahFOHrvroqgJLj+/81Yy1pZFkjfJgY0XklzAnA6CFq/YT0gxbV+TH5IS+WaYOUzK36S8s5qqMwhngv4IzE3ctXXqnhIZXNznyVBqbYa/Gx2bCjcEj2n5RyGYRS/+MculgLBznQ9MkG2D7QP3Mq/nwHXKMIaLS8VK8hG3d0rAjlvnauXShNrJxtkj4qkwnxD4LrEjk1ue+PLcbLqYWwAr8n0s+41jrsIPVyyK9vFaEZrA9KaBxkJyiqx50GZNn7ckx+mkI18gH52iyVPtU8FKMXjugCl6mPLiHYiDQd+G63LudmOKys9itVnBDBNMP2b/I7+bIVRlheGaZWr6MATKl86B3y+0kuOrLRMyWcdibc2iQkZm+8f7dTUwbKbBJsKOFY73/8nlL/JiYpjz6nUIk+IqSjaFyK6XFlCcukWpWHpDJnSvbEzey7qHyCqJ+I+on8iP8GOtempQaS0/4kZGSwlrE6xOj6EwGpfjbDZKHkP/2RAdNYPQq3z7bdVJMJNnFMz6e37mzMpCBihwPdSFTRE3SCY66wvTgJzGCUyhHbSi5i1azzOXd8uHCSpT/6L1o6VU0pjoR8Kaa4wCWHGf7P/WEd+avyp02Jnu1cmpW0+Gq+vK9h0tdb2IClM/eNfdouQP+ep8MJUjraDRICmAJQY5obS71vniOo+reaFy0zRgB', 'thinking': '', 'type': 'thinking'}, {'signature': 'CAQS/REKEQgSGAI4AUIJbmFycmF0aW9uEgwFh3AVJZP3s8y1Hq0aDG76uhHONaScXAGShiIwGgjLXHUx1yZrkLPljjzTOQycpLJiKVwXcxm/EzKJm1s8M5WxuREWEldXcoaN0/msKpkRMT+eLb5ih9rAHULTomUNsUwJeu0jrJe4GLazy7sP5KtjEOEBKQsXUfoQ7fDkZOQUHZi9zzQN4hFNmYJUf3OVqf9TQNdC1+m//HAZPfWmAy6HtDvm7kgBJkCEoiFnm7tn/PX1hXGPRgWc6BTXUMr2WA8aKyqggRUbMH2+JYv5iWZddCc1up1Nbr3sF7WPcot9ZBhyHQXr692zeJk6SJgygIVrASbMly2bBxTWJB623gVgzpCmVSLZFpkKXgPcp8hGyDKOvF6SiQSHqGRxQofr7Vs1aPhfZixROJGD+MlrkggVAnGMyYIDUZpavtCeiU1JmBSz0084bXPtT/tnpsKOnSPYZQo3B0BXKbqA/WsKng8/V3Z8Uuajo6IVZ2hgA1WDduqQOQkExGU6pFuLhebIjuWxSNI/DffvR//LNqCfvdnf0hvoPtDxfx/SawIx4/spq9xbwFzHtLgoHg1F2wFw7clPDlg1PyD/v2yJG3V9T8GL6jGPE6rhfil/u4HNvf7BW0dQzCTTXcqZy4nByy0+p+S7ZxfMl4Kpxa2gOhhSNjfpfuX0QpL4kME8J1nliFN1d8KqEPuDI18BQHUhD/UfY29oA0x510FjM/BsLZ4n5sppkoIi7CFoQq2aAKVNfC1KilhIpRD+1dyDtZQklN8gZpOESHw6ML1TtU0yfAcbmqoxJ6hhjzwH6CMsTQEEy8MbRBM+QKKkIRdRCPHGFW2LkuR50+J0pF/tr6pqVvJeozfiMnrEilB27TZP4nKTJG8rd//vCWen0y7CA/8dBJ9gdp5tx7NVA45k1gdcxER1gYouNX3n1vG3/B1F4+93wXW9Mqo6Us59TQmrb9Nqa7pSQNSrH06w1GxyBAlkFfqAlafk16aMpq416TJYpGy43WCUiCcLWlhiWgz2sgOLAXP9a5weWAKRC8J6jkcUkDeXnW+HFSiPM6+9Bo6kVhLUhN5rc4mHZOZpF7ptPpsQN4XCqLPsqpRH/8o5XpyC6ZneTeF2ExtjhwZVjXDwMbIzVYIbw1y2PPGw8dC7pc4sIVSnI4oQAH0v650ghfhmMevWW4h4Pylbw/DG4fdeT3+8z0f8MOSTtfipx05/QttHlAvODRxa5yDCvOz6n3f1BO0N/4MbGyOM27qW4x8mBVoryUZWfsmhfi0/GfNvB8+ylTGbYNX27OcEF5WZtNqbWEy7V5GbmZUSxspcg7FBZ3j3/3L7muvZXoZi1f5erq4y3lSH+oF7qPFnvieyLOCwubcQy+oyowj4cD0idRecnp+s+52D7+mzF1gpW+f+cJasoxigbxkOloS4yeiKvgM4nWSpGEj12RPx5o80EmziXN9t4zJzW1htA5hXJJeFympMYtyCUCevRWVmrRYktQ7UnCa6tL/AVPi1ni5XCXHfLOuqfK5KLYxjwVtdhhfmCLvpaKVNCEOCf4nNGHqD98wUb0ejWNwDYBnJGkBRw9RVspfwNdoWoVcn2qv8ywGUp7yc/sk2LpI6VziRyvWqbGNG1ZJqVVutRc46+sOE4VSeS2/CTNKNa6NMCf/hxxaUVsYfKdP+S1Lc4ianKp0sUugPpmtoydC0CP2RwjnHLcnzxCnvqxlLQWXMNPTsu2yBcHlWJLtcmdg/McLagy7l5kG6sN7ZxJLD/kAx8E6uqohP+K/jMSrYuQJnhr4tf4biWFY3Jzf/KpAYyVSq+pbfFKXMbL9nHEoZQT15Wyabd6DMAq3QZOthM5pUzdNqUXkcrI1PwjASAg4dVU/2o+98mTXnyhP7VlEkTnddpwyJF+YCCc01pqa6ZX99pnR+YOpIrb5DtPcKq0t5cFCDyQxN1lUuqZcYujvX/GC7+6IpRTz/NkEY/5cntOfZJyd6SkUx/JR5AJIRPCNeib0rcWnnFn0XK8s22x40wLjEtufXDO9+yraf4kHxlBI3SB0meMjepX50CJhAx7NekkRFnwoecLfqIyrjXrgAZsTMPiX94snjdrzisAXtBKanulilSxOfW2HSmfnk0L2GXHJJ3S+vCbgREml9dekfmHhmwEYtv3kfhth7ERIwVT8j69OtIBCmeTIJuA7o73PXc1LJ2whuZK7FZe6NTOs61MP7JVlIc6tbhtOBk3NXU8pBPOCY6Q55d63FtpUU/KCbQ0K2WQ9o5jNGwNNJ6Cw5FDCt+x5YJeP1d2QzDSiq+cSZqwqCZVcNoAftGGzgrRZ4vkpbP0xQZHCtWsUwHVTjrY4NfRDjHdVPRn8J9Xtooc/hIWkqzFaczYTlapu2RCJqe8aEo3jzWZCEJ7AiBzBYraYDgOfeiQyeJUsbbXHdGKiiw22Bdp4M+Yz671MZ0ie9EWRks0Q2DHGUTne9S4tAAJPGbUf4xtCMoJ40Q4QW05QLSZy3wNFAUFYmXE4ECDFB8+IEeNFFF/tgAnfh8GvFh/1xY+LE+ozMminjSRwhq09zQAEkNFyu0IKgvDkyOI+iccF7doWLTUasPxLH1xGPBSKkeWCnSoX/4C2c9du66BbRXamHqae+VCiS6BN953iYKOu8rMkneW+TFjV0CzAf8PNG9LBayUgY5Uw9x0Sap6zI4PpaZKH4d9ktkFU+91yBbN5SRtdd8/EQatJXmBNfw7kRrll+LjwZweWLEb0jT1RpGuDqHLdLTMMgowSczrp2zCsJdQH/seaYrpwkCFR8dRjM7JRUkVa1A0SvdZAX75vuBvvLBzN2c+Rbt5oqy93lqIBIjKb7w2UkRTb4pBWcNKNPUgt3az/PxQxEasaltdmuyvsXYbur3VPNn8JzN/6J5N+QCnz571TccapCmkPvFE+gayXU+UF1VKYyFcUvlev7lsQlmjqzK2QA8X7Q3TzFapqhvJKlt7dzBHraqZBgPgb9N4oKNayi8FZT71+kaYhIhFrESKYRHfHkj5n3kfhgLy+x+jLDbKxMggkgHnJz9RuEHZXBWtkYAQ==', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_verifier', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAMRdS21FeJfzQhM3s', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7396, 'output_tokens': 770, 'output_tokens_details': {'thinking_tokens': 739}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='skeptic', id='lc_run--01a0fe03-4ae9-76c0-89a6-4f0b114e3030-0', tool_calls=[{'name': 'transfer_to_verifier', 'args': {}, 'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7396, 'output_tokens': 770, 'total_tokens': 8166, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 739}}), ToolMessage(content='Successfully transferred to verifier', name='transfer_to_verifier', id='c064563a-974f-4f0f-ab4a-080e63c01cf5', tool_call_id='toolu_01H6fV8JvRJNvXy47eRnd3jH'), AIMessage(content=[{'signature': 'CAQS9gUKEAgSGAI4AUIIdGhpbmtpbmcSDGFyJ1MXoY+Ai6NkbxoMRd2TrUfUU7WrPpA2IjDgy8dUplcZKc7DnJwNLTBfH5UNR+nGnakSPQia8UVIzrjkOnJ2fLrWUsA2oEO8yRsqkwXkkDCsyhB97dwj57RMt91XzgWhL9luNEGO92yW0rNFGoMSUVOxPIrFc5RjyrhKEc3+1RnpxCkylq+LNlQ5NFxLn6v2cF5DzqpvBHNR7XAig+urJIU3gXydCkaGZZDetJtQOUOXgZi9z9100NpdAEkXDo2ICHCGTGpVc0J/pbXxWRBxb6B1/Gl0Yjn68V6HlesH7IoC7GR/3HwRu9NSDBG4ttfQjEWcg+PeAea4Rn7jzx+Tm9H7L1joTKVgDbQg5iKMLfjvPqlnksCLtXOt8jSIS5OYLN7a/7TX6/w5d34vnw8vPDmU1UYi0Q72qUWhUa2vw8OXNpcKL7Bp0lNfBS8rvHY+vBY8/haAoTL3wvLFU1fT+4z/vA3BcsigZDYlfJeDYcxXmRuqxzxEGpXCRe9s1HWtID+41pHu6lpZHkXaghpc6TZHLxNwcVhTb6fMQ7Qo+2L6Pvklh1m32peodxeg2YiPhdpl3w/xkOEu5/JEF7ZBxpdUL+u3wkO5pUlsqfgX7uhzv7HC2WpSWVimrwiQwQOUx0+JPrLtw+lYfq7I3jruo0GW0guWvnuf7JM3YLj84q+JgHomnSSZnnZmrL0UaRvrVyLo877F39ccLS7XoZnnui8J4P58ZTmiLkEJeGcx1Iuag2e33339O7Yew/59tLbQtIGh9VWMwHBnFx39S8/4LEFrK5HldPQhOrloyTtkRs66iQRKtxMKS0VI6QTKZDAbs7a7H1yBv+mJi9Xg4uKmyU6qnn7zEKjjY2bIlCwezx4LLGP5HD3Zr6fZsmdG1UHzDsBvYG2oCI0oqEKM+valsT6hU8MRjbQBD/JJ8JJN/iBGlhBAUHo0UmhhtR2euTVpZWlMnhmpXAdKDXSfPdaPohgB', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_red_team', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAsabDquegHi6YUpCn', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7509, 'output_tokens': 88, 'output_tokens_details': {'thinking_tokens': 55}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='verifier', id='lc_run--01a0fe03-6682-7d21-8c15-ffbce9422c9f-0', tool_calls=[{'name': 'transfer_to_red_team', 'args': {}, 'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7509, 'output_tokens': 88, 'total_tokens': 7597, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 55}}), ToolMessage(content='Successfully transferred to red_team', name='transfer_to_red_team', tool_call_id='toolu_01XEAuhQHCBdyCwtymZwFfDi')], 'active_agent': 'red_team'}, goto='red_team'))Traceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 707, in invoke\n input = context.run(step.invoke, input, config, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 3913, in invoke\n for chunk in self.stream(\n ~~~~~~~~~~~^\n input,\n ^^^^^^\n ...<11 lines>...\n **kwargs,\n ^^^^^^^^^\n ):\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 2967, in stream\n for _ in runner.tick(\n ~~~~~~~~~~~^\n [t for t in loop.tasks.values() if not t.writes],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<2 lines>...\n schedule_task=loop.accept_push,\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ):\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_runner.py\", line 207, in tick\n run_with_retry(\n ~~~~~~~~~~~~~~^\n t,\n ^^\n ...<10 lines>...\n },\n ^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_retry.py\", line 617, in run_with_retry\n return task.proc.invoke(task.input, config)\n ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 709, in invoke\n input = step.invoke(input, config)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 447, in invoke\n ret = self.func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 84, in _write\n self.do_write(\n ~~~~~~~~~~~~~^\n config,\n ^^^^^^^\n writes,\n ^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 126, in do_write\n write(_assemble_writes(writes))\n ~~~~~~~~~~~~~~~~^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 181, in _assemble_writes\n if ww := w.mapper(w.value):\n ~~~~~~~~^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/graph/state.py\", line 1762, in _control_branch\n raise ParentCommand(command)\n\n\nlanggraph.errors.ParentCommand: Command(graph='verifier:544dea40-e16b-458c-1464-128e1b9b10a9', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', id='389058d1-083f-4632-bbc5-09bfd3a0ffd2', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9'), AIMessage(content=[{'signature': 'CAQS1QgKEAgSGAI4AUIIdGhpbmtpbmcSDN6TfU6NDM971lEbzxoMvRUA1J9ox3fJQYT9IjArCQQGHBP4pRHPavXzRlbMbaYHC57Yj+Gbxgr0helvd2OIL/o6Rbt4PFA3FaIFUlgq8gc7KQI9FtGnvSAkLBRzIsUp/2d5Rd2zgEsfkFVeXsn8WYYZkinAQVVy6A7B3LaEJW0xb/x88Di67fedwx0Q+FlZtwcmT3Dcaa11JveyHRr3dm+zGSZP6CXeXNJ8nrTZ7W1YRTysD6w7o7Lp8h1Rwq4f4o8j+QUfY0vje+zJZsxHPmQNt3ykeKh99hgn6dbiz6/EHqmbHjSAu1M4SqEDfqIKThHYenamAZmcQ4XAOo6mdQ6+77VgpV1lC1/k52IiUoJjkGlQrnLCvutiI7KN+sgK8c2+2KCJrPRCvVV+BkStvP5jWWqzytpKQ+nHzhL448OrIA9gdJC39C8eDrXeRnI7t4FqlkXGoMlZAV9UMcrQHRxOMi8tOyTzi9oyBFDTSsQ+LJqer0tz1Ft9gek6vzg50SpVAFQdLERv8R1YqMaeI+xsvTAEW9MRgvRMkzLuzRT2guw3jD+Fjip37xgK5QI0R+CsOAmQPoTZizzcIyAUAuCJFCgq/gD/CibLxoymiVUI1an9Snt3KpNRoWdvWIne33V0pv8PHg9yW2JuLOqQGilLCsK/Q3oQr3upEMXxXGjCL7WUizvRZGFEEV+VplYt7v4tKZMj5pk3vdeM0MJ5XZg7PvUsRzycxcnrT3Knoc3qM+ttqZfTsb8d6U/pQifq7GuahFOHrvroqgJLj+/81Yy1pZFkjfJgY0XklzAnA6CFq/YT0gxbV+TH5IS+WaYOUzK36S8s5qqMwhngv4IzE3ctXXqnhIZXNznyVBqbYa/Gx2bCjcEj2n5RyGYRS/+MculgLBznQ9MkG2D7QP3Mq/nwHXKMIaLS8VK8hG3d0rAjlvnauXShNrJxtkj4qkwnxD4LrEjk1ue+PLcbLqYWwAr8n0s+41jrsIPVyyK9vFaEZrA9KaBxkJyiqx50GZNn7ckx+mkI18gH52iyVPtU8FKMXjugCl6mPLiHYiDQd+G63LudmOKys9itVnBDBNMP2b/I7+bIVRlheGaZWr6MATKl86B3y+0kuOrLRMyWcdibc2iQkZm+8f7dTUwbKbBJsKOFY73/8nlL/JiYpjz6nUIk+IqSjaFyK6XFlCcukWpWHpDJnSvbEzey7qHyCqJ+I+on8iP8GOtempQaS0/4kZGSwlrE6xOj6EwGpfjbDZKHkP/2RAdNYPQq3z7bdVJMJNnFMz6e37mzMpCBihwPdSFTRE3SCY66wvTgJzGCUyhHbSi5i1azzOXd8uHCSpT/6L1o6VU0pjoR8Kaa4wCWHGf7P/WEd+avyp02Jnu1cmpW0+Gq+vK9h0tdb2IClM/eNfdouQP+ep8MJUjraDRICmAJQY5obS71vniOo+reaFy0zRgB', 'thinking': '', 'type': 'thinking'}, {'signature': 'CAQS/REKEQgSGAI4AUIJbmFycmF0aW9uEgwFh3AVJZP3s8y1Hq0aDG76uhHONaScXAGShiIwGgjLXHUx1yZrkLPljjzTOQycpLJiKVwXcxm/EzKJm1s8M5WxuREWEldXcoaN0/msKpkRMT+eLb5ih9rAHULTomUNsUwJeu0jrJe4GLazy7sP5KtjEOEBKQsXUfoQ7fDkZOQUHZi9zzQN4hFNmYJUf3OVqf9TQNdC1+m//HAZPfWmAy6HtDvm7kgBJkCEoiFnm7tn/PX1hXGPRgWc6BTXUMr2WA8aKyqggRUbMH2+JYv5iWZddCc1up1Nbr3sF7WPcot9ZBhyHQXr692zeJk6SJgygIVrASbMly2bBxTWJB623gVgzpCmVSLZFpkKXgPcp8hGyDKOvF6SiQSHqGRxQofr7Vs1aPhfZixROJGD+MlrkggVAnGMyYIDUZpavtCeiU1JmBSz0084bXPtT/tnpsKOnSPYZQo3B0BXKbqA/WsKng8/V3Z8Uuajo6IVZ2hgA1WDduqQOQkExGU6pFuLhebIjuWxSNI/DffvR//LNqCfvdnf0hvoPtDxfx/SawIx4/spq9xbwFzHtLgoHg1F2wFw7clPDlg1PyD/v2yJG3V9T8GL6jGPE6rhfil/u4HNvf7BW0dQzCTTXcqZy4nByy0+p+S7ZxfMl4Kpxa2gOhhSNjfpfuX0QpL4kME8J1nliFN1d8KqEPuDI18BQHUhD/UfY29oA0x510FjM/BsLZ4n5sppkoIi7CFoQq2aAKVNfC1KilhIpRD+1dyDtZQklN8gZpOESHw6ML1TtU0yfAcbmqoxJ6hhjzwH6CMsTQEEy8MbRBM+QKKkIRdRCPHGFW2LkuR50+J0pF/tr6pqVvJeozfiMnrEilB27TZP4nKTJG8rd//vCWen0y7CA/8dBJ9gdp5tx7NVA45k1gdcxER1gYouNX3n1vG3/B1F4+93wXW9Mqo6Us59TQmrb9Nqa7pSQNSrH06w1GxyBAlkFfqAlafk16aMpq416TJYpGy43WCUiCcLWlhiWgz2sgOLAXP9a5weWAKRC8J6jkcUkDeXnW+HFSiPM6+9Bo6kVhLUhN5rc4mHZOZpF7ptPpsQN4XCqLPsqpRH/8o5XpyC6ZneTeF2ExtjhwZVjXDwMbIzVYIbw1y2PPGw8dC7pc4sIVSnI4oQAH0v650ghfhmMevWW4h4Pylbw/DG4fdeT3+8z0f8MOSTtfipx05/QttHlAvODRxa5yDCvOz6n3f1BO0N/4MbGyOM27qW4x8mBVoryUZWfsmhfi0/GfNvB8+ylTGbYNX27OcEF5WZtNqbWEy7V5GbmZUSxspcg7FBZ3j3/3L7muvZXoZi1f5erq4y3lSH+oF7qPFnvieyLOCwubcQy+oyowj4cD0idRecnp+s+52D7+mzF1gpW+f+cJasoxigbxkOloS4yeiKvgM4nWSpGEj12RPx5o80EmziXN9t4zJzW1htA5hXJJeFympMYtyCUCevRWVmrRYktQ7UnCa6tL/AVPi1ni5XCXHfLOuqfK5KLYxjwVtdhhfmCLvpaKVNCEOCf4nNGHqD98wUb0ejWNwDYBnJGkBRw9RVspfwNdoWoVcn2qv8ywGUp7yc/sk2LpI6VziRyvWqbGNG1ZJqVVutRc46+sOE4VSeS2/CTNKNa6NMCf/hxxaUVsYfKdP+S1Lc4ianKp0sUugPpmtoydC0CP2RwjnHLcnzxCnvqxlLQWXMNPTsu2yBcHlWJLtcmdg/McLagy7l5kG6sN7ZxJLD/kAx8E6uqohP+K/jMSrYuQJnhr4tf4biWFY3Jzf/KpAYyVSq+pbfFKXMbL9nHEoZQT15Wyabd6DMAq3QZOthM5pUzdNqUXkcrI1PwjASAg4dVU/2o+98mTXnyhP7VlEkTnddpwyJF+YCCc01pqa6ZX99pnR+YOpIrb5DtPcKq0t5cFCDyQxN1lUuqZcYujvX/GC7+6IpRTz/NkEY/5cntOfZJyd6SkUx/JR5AJIRPCNeib0rcWnnFn0XK8s22x40wLjEtufXDO9+yraf4kHxlBI3SB0meMjepX50CJhAx7NekkRFnwoecLfqIyrjXrgAZsTMPiX94snjdrzisAXtBKanulilSxOfW2HSmfnk0L2GXHJJ3S+vCbgREml9dekfmHhmwEYtv3kfhth7ERIwVT8j69OtIBCmeTIJuA7o73PXc1LJ2whuZK7FZe6NTOs61MP7JVlIc6tbhtOBk3NXU8pBPOCY6Q55d63FtpUU/KCbQ0K2WQ9o5jNGwNNJ6Cw5FDCt+x5YJeP1d2QzDSiq+cSZqwqCZVcNoAftGGzgrRZ4vkpbP0xQZHCtWsUwHVTjrY4NfRDjHdVPRn8J9Xtooc/hIWkqzFaczYTlapu2RCJqe8aEo3jzWZCEJ7AiBzBYraYDgOfeiQyeJUsbbXHdGKiiw22Bdp4M+Yz671MZ0ie9EWRks0Q2DHGUTne9S4tAAJPGbUf4xtCMoJ40Q4QW05QLSZy3wNFAUFYmXE4ECDFB8+IEeNFFF/tgAnfh8GvFh/1xY+LE+ozMminjSRwhq09zQAEkNFyu0IKgvDkyOI+iccF7doWLTUasPxLH1xGPBSKkeWCnSoX/4C2c9du66BbRXamHqae+VCiS6BN953iYKOu8rMkneW+TFjV0CzAf8PNG9LBayUgY5Uw9x0Sap6zI4PpaZKH4d9ktkFU+91yBbN5SRtdd8/EQatJXmBNfw7kRrll+LjwZweWLEb0jT1RpGuDqHLdLTMMgowSczrp2zCsJdQH/seaYrpwkCFR8dRjM7JRUkVa1A0SvdZAX75vuBvvLBzN2c+Rbt5oqy93lqIBIjKb7w2UkRTb4pBWcNKNPUgt3az/PxQxEasaltdmuyvsXYbur3VPNn8JzN/6J5N+QCnz571TccapCmkPvFE+gayXU+UF1VKYyFcUvlev7lsQlmjqzK2QA8X7Q3TzFapqhvJKlt7dzBHraqZBgPgb9N4oKNayi8FZT71+kaYhIhFrESKYRHfHkj5n3kfhgLy+x+jLDbKxMggkgHnJz9RuEHZXBWtkYAQ==', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_verifier', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAMRdS21FeJfzQhM3s', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7396, 'output_tokens': 770, 'output_tokens_details': {'thinking_tokens': 739}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='skeptic', id='lc_run--01a0fe03-4ae9-76c0-89a6-4f0b114e3030-0', tool_calls=[{'name': 'transfer_to_verifier', 'args': {}, 'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7396, 'output_tokens': 770, 'total_tokens': 8166, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 739}}), ToolMessage(content='Successfully transferred to verifier', name='transfer_to_verifier', id='c064563a-974f-4f0f-ab4a-080e63c01cf5', tool_call_id='toolu_01H6fV8JvRJNvXy47eRnd3jH'), AIMessage(content=[{'signature': 'CAQS9gUKEAgSGAI4AUIIdGhpbmtpbmcSDGFyJ1MXoY+Ai6NkbxoMRd2TrUfUU7WrPpA2IjDgy8dUplcZKc7DnJwNLTBfH5UNR+nGnakSPQia8UVIzrjkOnJ2fLrWUsA2oEO8yRsqkwXkkDCsyhB97dwj57RMt91XzgWhL9luNEGO92yW0rNFGoMSUVOxPIrFc5RjyrhKEc3+1RnpxCkylq+LNlQ5NFxLn6v2cF5DzqpvBHNR7XAig+urJIU3gXydCkaGZZDetJtQOUOXgZi9z9100NpdAEkXDo2ICHCGTGpVc0J/pbXxWRBxb6B1/Gl0Yjn68V6HlesH7IoC7GR/3HwRu9NSDBG4ttfQjEWcg+PeAea4Rn7jzx+Tm9H7L1joTKVgDbQg5iKMLfjvPqlnksCLtXOt8jSIS5OYLN7a/7TX6/w5d34vnw8vPDmU1UYi0Q72qUWhUa2vw8OXNpcKL7Bp0lNfBS8rvHY+vBY8/haAoTL3wvLFU1fT+4z/vA3BcsigZDYlfJeDYcxXmRuqxzxEGpXCRe9s1HWtID+41pHu6lpZHkXaghpc6TZHLxNwcVhTb6fMQ7Qo+2L6Pvklh1m32peodxeg2YiPhdpl3w/xkOEu5/JEF7ZBxpdUL+u3wkO5pUlsqfgX7uhzv7HC2WpSWVimrwiQwQOUx0+JPrLtw+lYfq7I3jruo0GW0guWvnuf7JM3YLj84q+JgHomnSSZnnZmrL0UaRvrVyLo877F39ccLS7XoZnnui8J4P58ZTmiLkEJeGcx1Iuag2e33339O7Yew/59tLbQtIGh9VWMwHBnFx39S8/4LEFrK5HldPQhOrloyTtkRs66iQRKtxMKS0VI6QTKZDAbs7a7H1yBv+mJi9Xg4uKmyU6qnn7zEKjjY2bIlCwezx4LLGP5HD3Zr6fZsmdG1UHzDsBvYG2oCI0oqEKM+valsT6hU8MRjbQBD/JJ8JJN/iBGlhBAUHo0UmhhtR2euTVpZWlMnhmpXAdKDXSfPdaPohgB', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_red_team', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAsabDquegHi6YUpCn', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7509, 'output_tokens': 88, 'output_tokens_details': {'thinking_tokens': 55}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='verifier', id='lc_run--01a0fe03-6682-7d21-8c15-ffbce9422c9f-0', tool_calls=[{'name': 'transfer_to_red_team', 'args': {}, 'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7509, 'output_tokens': 88, 'total_tokens': 7597, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 55}}), ToolMessage(content='Successfully transferred to red_team', name='transfer_to_red_team', tool_call_id='toolu_01XEAuhQHCBdyCwtymZwFfDi')], 'active_agent': 'red_team'}, goto='red_team')\n" + } + }, + { + "key": "exception.escaped", + "value": { + "stringValue": "False" + } + } + ] + } + ], + "status": { + "code": 1 + }, + "flags": 256 + } + ] + } + ] + }, + { + "resource": { + "attributes": [ + { + "key": "telemetry.sdk.language", + "value": { + "stringValue": "python" + } + }, + { + "key": "telemetry.sdk.name", + "value": { + "stringValue": "opentelemetry" + } + }, + { + "key": "telemetry.sdk.version", + "value": { + "stringValue": "1.45.0" + } + }, + { + "key": "service.instance.id", + "value": { + "stringValue": "b873df4c-5106-4097-a20b-b9380724f4d9" + } + }, + { + "key": "service.name", + "value": { + "stringValue": "deeplite" + } + } + ] + }, + "scopeSpans": [ + { + "scope": { + "name": "langsmith" + }, + "spans": [ + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "916b875f8575ff2d", + "parentSpanId": "59d91c7c0791b7de", + "name": "tools", + "kind": 1, + "startTimeUnixNano": "1790968035030605056", + "endTimeUnixNano": "1790968035036314112", + "attributes": [ + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "tools" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "2" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "tools" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"__pregel_push\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_push\",0,false]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "red_team:15ccef64-4162-9e02-e87c-4cb0eb6ea16b|tools:f216a445-733e-fcc8-6fba-de3530aa90b7" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "red_team" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "red_team:15ccef64-4162-9e02-e87c-4cb0eb6ea16b" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "graph:step:2" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJpbnB1dCI6W3sibmFtZSI6InRyYW5zZmVyX3RvX2VkaXRvciIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxVkJyV2RXRVdZZ2l0NGtOWWVoQk1VNSIsInR5cGUiOiJ0b29sX2NhbGwifV19" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "e30=" + } + } + ], + "events": [ + { + "timeUnixNano": "1790968035107514000", + "name": "exception", + "attributes": [ + { + "key": "exception.type", + "value": { + "stringValue": "Exception" + } + }, + { + "key": "exception.message", + "value": { + "stringValue": "ParentCommand(Command(graph='__parent__', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', id='389058d1-083f-4632-bbc5-09bfd3a0ffd2', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9'), AIMessage(content=[{'signature': 'CAQS1QgKEAgSGAI4AUIIdGhpbmtpbmcSDN6TfU6NDM971lEbzxoMvRUA1J9ox3fJQYT9IjArCQQGHBP4pRHPavXzRlbMbaYHC57Yj+Gbxgr0helvd2OIL/o6Rbt4PFA3FaIFUlgq8gc7KQI9FtGnvSAkLBRzIsUp/2d5Rd2zgEsfkFVeXsn8WYYZkinAQVVy6A7B3LaEJW0xb/x88Di67fedwx0Q+FlZtwcmT3Dcaa11JveyHRr3dm+zGSZP6CXeXNJ8nrTZ7W1YRTysD6w7o7Lp8h1Rwq4f4o8j+QUfY0vje+zJZsxHPmQNt3ykeKh99hgn6dbiz6/EHqmbHjSAu1M4SqEDfqIKThHYenamAZmcQ4XAOo6mdQ6+77VgpV1lC1/k52IiUoJjkGlQrnLCvutiI7KN+sgK8c2+2KCJrPRCvVV+BkStvP5jWWqzytpKQ+nHzhL448OrIA9gdJC39C8eDrXeRnI7t4FqlkXGoMlZAV9UMcrQHRxOMi8tOyTzi9oyBFDTSsQ+LJqer0tz1Ft9gek6vzg50SpVAFQdLERv8R1YqMaeI+xsvTAEW9MRgvRMkzLuzRT2guw3jD+Fjip37xgK5QI0R+CsOAmQPoTZizzcIyAUAuCJFCgq/gD/CibLxoymiVUI1an9Snt3KpNRoWdvWIne33V0pv8PHg9yW2JuLOqQGilLCsK/Q3oQr3upEMXxXGjCL7WUizvRZGFEEV+VplYt7v4tKZMj5pk3vdeM0MJ5XZg7PvUsRzycxcnrT3Knoc3qM+ttqZfTsb8d6U/pQifq7GuahFOHrvroqgJLj+/81Yy1pZFkjfJgY0XklzAnA6CFq/YT0gxbV+TH5IS+WaYOUzK36S8s5qqMwhngv4IzE3ctXXqnhIZXNznyVBqbYa/Gx2bCjcEj2n5RyGYRS/+MculgLBznQ9MkG2D7QP3Mq/nwHXKMIaLS8VK8hG3d0rAjlvnauXShNrJxtkj4qkwnxD4LrEjk1ue+PLcbLqYWwAr8n0s+41jrsIPVyyK9vFaEZrA9KaBxkJyiqx50GZNn7ckx+mkI18gH52iyVPtU8FKMXjugCl6mPLiHYiDQd+G63LudmOKys9itVnBDBNMP2b/I7+bIVRlheGaZWr6MATKl86B3y+0kuOrLRMyWcdibc2iQkZm+8f7dTUwbKbBJsKOFY73/8nlL/JiYpjz6nUIk+IqSjaFyK6XFlCcukWpWHpDJnSvbEzey7qHyCqJ+I+on8iP8GOtempQaS0/4kZGSwlrE6xOj6EwGpfjbDZKHkP/2RAdNYPQq3z7bdVJMJNnFMz6e37mzMpCBihwPdSFTRE3SCY66wvTgJzGCUyhHbSi5i1azzOXd8uHCSpT/6L1o6VU0pjoR8Kaa4wCWHGf7P/WEd+avyp02Jnu1cmpW0+Gq+vK9h0tdb2IClM/eNfdouQP+ep8MJUjraDRICmAJQY5obS71vniOo+reaFy0zRgB', 'thinking': '', 'type': 'thinking'}, {'signature': 'CAQS/REKEQgSGAI4AUIJbmFycmF0aW9uEgwFh3AVJZP3s8y1Hq0aDG76uhHONaScXAGShiIwGgjLXHUx1yZrkLPljjzTOQycpLJiKVwXcxm/EzKJm1s8M5WxuREWEldXcoaN0/msKpkRMT+eLb5ih9rAHULTomUNsUwJeu0jrJe4GLazy7sP5KtjEOEBKQsXUfoQ7fDkZOQUHZi9zzQN4hFNmYJUf3OVqf9TQNdC1+m//HAZPfWmAy6HtDvm7kgBJkCEoiFnm7tn/PX1hXGPRgWc6BTXUMr2WA8aKyqggRUbMH2+JYv5iWZddCc1up1Nbr3sF7WPcot9ZBhyHQXr692zeJk6SJgygIVrASbMly2bBxTWJB623gVgzpCmVSLZFpkKXgPcp8hGyDKOvF6SiQSHqGRxQofr7Vs1aPhfZixROJGD+MlrkggVAnGMyYIDUZpavtCeiU1JmBSz0084bXPtT/tnpsKOnSPYZQo3B0BXKbqA/WsKng8/V3Z8Uuajo6IVZ2hgA1WDduqQOQkExGU6pFuLhebIjuWxSNI/DffvR//LNqCfvdnf0hvoPtDxfx/SawIx4/spq9xbwFzHtLgoHg1F2wFw7clPDlg1PyD/v2yJG3V9T8GL6jGPE6rhfil/u4HNvf7BW0dQzCTTXcqZy4nByy0+p+S7ZxfMl4Kpxa2gOhhSNjfpfuX0QpL4kME8J1nliFN1d8KqEPuDI18BQHUhD/UfY29oA0x510FjM/BsLZ4n5sppkoIi7CFoQq2aAKVNfC1KilhIpRD+1dyDtZQklN8gZpOESHw6ML1TtU0yfAcbmqoxJ6hhjzwH6CMsTQEEy8MbRBM+QKKkIRdRCPHGFW2LkuR50+J0pF/tr6pqVvJeozfiMnrEilB27TZP4nKTJG8rd//vCWen0y7CA/8dBJ9gdp5tx7NVA45k1gdcxER1gYouNX3n1vG3/B1F4+93wXW9Mqo6Us59TQmrb9Nqa7pSQNSrH06w1GxyBAlkFfqAlafk16aMpq416TJYpGy43WCUiCcLWlhiWgz2sgOLAXP9a5weWAKRC8J6jkcUkDeXnW+HFSiPM6+9Bo6kVhLUhN5rc4mHZOZpF7ptPpsQN4XCqLPsqpRH/8o5XpyC6ZneTeF2ExtjhwZVjXDwMbIzVYIbw1y2PPGw8dC7pc4sIVSnI4oQAH0v650ghfhmMevWW4h4Pylbw/DG4fdeT3+8z0f8MOSTtfipx05/QttHlAvODRxa5yDCvOz6n3f1BO0N/4MbGyOM27qW4x8mBVoryUZWfsmhfi0/GfNvB8+ylTGbYNX27OcEF5WZtNqbWEy7V5GbmZUSxspcg7FBZ3j3/3L7muvZXoZi1f5erq4y3lSH+oF7qPFnvieyLOCwubcQy+oyowj4cD0idRecnp+s+52D7+mzF1gpW+f+cJasoxigbxkOloS4yeiKvgM4nWSpGEj12RPx5o80EmziXN9t4zJzW1htA5hXJJeFympMYtyCUCevRWVmrRYktQ7UnCa6tL/AVPi1ni5XCXHfLOuqfK5KLYxjwVtdhhfmCLvpaKVNCEOCf4nNGHqD98wUb0ejWNwDYBnJGkBRw9RVspfwNdoWoVcn2qv8ywGUp7yc/sk2LpI6VziRyvWqbGNG1ZJqVVutRc46+sOE4VSeS2/CTNKNa6NMCf/hxxaUVsYfKdP+S1Lc4ianKp0sUugPpmtoydC0CP2RwjnHLcnzxCnvqxlLQWXMNPTsu2yBcHlWJLtcmdg/McLagy7l5kG6sN7ZxJLD/kAx8E6uqohP+K/jMSrYuQJnhr4tf4biWFY3Jzf/KpAYyVSq+pbfFKXMbL9nHEoZQT15Wyabd6DMAq3QZOthM5pUzdNqUXkcrI1PwjASAg4dVU/2o+98mTXnyhP7VlEkTnddpwyJF+YCCc01pqa6ZX99pnR+YOpIrb5DtPcKq0t5cFCDyQxN1lUuqZcYujvX/GC7+6IpRTz/NkEY/5cntOfZJyd6SkUx/JR5AJIRPCNeib0rcWnnFn0XK8s22x40wLjEtufXDO9+yraf4kHxlBI3SB0meMjepX50CJhAx7NekkRFnwoecLfqIyrjXrgAZsTMPiX94snjdrzisAXtBKanulilSxOfW2HSmfnk0L2GXHJJ3S+vCbgREml9dekfmHhmwEYtv3kfhth7ERIwVT8j69OtIBCmeTIJuA7o73PXc1LJ2whuZK7FZe6NTOs61MP7JVlIc6tbhtOBk3NXU8pBPOCY6Q55d63FtpUU/KCbQ0K2WQ9o5jNGwNNJ6Cw5FDCt+x5YJeP1d2QzDSiq+cSZqwqCZVcNoAftGGzgrRZ4vkpbP0xQZHCtWsUwHVTjrY4NfRDjHdVPRn8J9Xtooc/hIWkqzFaczYTlapu2RCJqe8aEo3jzWZCEJ7AiBzBYraYDgOfeiQyeJUsbbXHdGKiiw22Bdp4M+Yz671MZ0ie9EWRks0Q2DHGUTne9S4tAAJPGbUf4xtCMoJ40Q4QW05QLSZy3wNFAUFYmXE4ECDFB8+IEeNFFF/tgAnfh8GvFh/1xY+LE+ozMminjSRwhq09zQAEkNFyu0IKgvDkyOI+iccF7doWLTUasPxLH1xGPBSKkeWCnSoX/4C2c9du66BbRXamHqae+VCiS6BN953iYKOu8rMkneW+TFjV0CzAf8PNG9LBayUgY5Uw9x0Sap6zI4PpaZKH4d9ktkFU+91yBbN5SRtdd8/EQatJXmBNfw7kRrll+LjwZweWLEb0jT1RpGuDqHLdLTMMgowSczrp2zCsJdQH/seaYrpwkCFR8dRjM7JRUkVa1A0SvdZAX75vuBvvLBzN2c+Rbt5oqy93lqIBIjKb7w2UkRTb4pBWcNKNPUgt3az/PxQxEasaltdmuyvsXYbur3VPNn8JzN/6J5N+QCnz571TccapCmkPvFE+gayXU+UF1VKYyFcUvlev7lsQlmjqzK2QA8X7Q3TzFapqhvJKlt7dzBHraqZBgPgb9N4oKNayi8FZT71+kaYhIhFrESKYRHfHkj5n3kfhgLy+x+jLDbKxMggkgHnJz9RuEHZXBWtkYAQ==', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_verifier', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAMRdS21FeJfzQhM3s', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7396, 'output_tokens': 770, 'output_tokens_details': {'thinking_tokens': 739}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='skeptic', id='lc_run--01a0fe03-4ae9-76c0-89a6-4f0b114e3030-0', tool_calls=[{'name': 'transfer_to_verifier', 'args': {}, 'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7396, 'output_tokens': 770, 'total_tokens': 8166, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 739}}), ToolMessage(content='Successfully transferred to verifier', name='transfer_to_verifier', id='c064563a-974f-4f0f-ab4a-080e63c01cf5', tool_call_id='toolu_01H6fV8JvRJNvXy47eRnd3jH'), AIMessage(content=[{'signature': 'CAQS9gUKEAgSGAI4AUIIdGhpbmtpbmcSDGFyJ1MXoY+Ai6NkbxoMRd2TrUfUU7WrPpA2IjDgy8dUplcZKc7DnJwNLTBfH5UNR+nGnakSPQia8UVIzrjkOnJ2fLrWUsA2oEO8yRsqkwXkkDCsyhB97dwj57RMt91XzgWhL9luNEGO92yW0rNFGoMSUVOxPIrFc5RjyrhKEc3+1RnpxCkylq+LNlQ5NFxLn6v2cF5DzqpvBHNR7XAig+urJIU3gXydCkaGZZDetJtQOUOXgZi9z9100NpdAEkXDo2ICHCGTGpVc0J/pbXxWRBxb6B1/Gl0Yjn68V6HlesH7IoC7GR/3HwRu9NSDBG4ttfQjEWcg+PeAea4Rn7jzx+Tm9H7L1joTKVgDbQg5iKMLfjvPqlnksCLtXOt8jSIS5OYLN7a/7TX6/w5d34vnw8vPDmU1UYi0Q72qUWhUa2vw8OXNpcKL7Bp0lNfBS8rvHY+vBY8/haAoTL3wvLFU1fT+4z/vA3BcsigZDYlfJeDYcxXmRuqxzxEGpXCRe9s1HWtID+41pHu6lpZHkXaghpc6TZHLxNwcVhTb6fMQ7Qo+2L6Pvklh1m32peodxeg2YiPhdpl3w/xkOEu5/JEF7ZBxpdUL+u3wkO5pUlsqfgX7uhzv7HC2WpSWVimrwiQwQOUx0+JPrLtw+lYfq7I3jruo0GW0guWvnuf7JM3YLj84q+JgHomnSSZnnZmrL0UaRvrVyLo877F39ccLS7XoZnnui8J4P58ZTmiLkEJeGcx1Iuag2e33339O7Yew/59tLbQtIGh9VWMwHBnFx39S8/4LEFrK5HldPQhOrloyTtkRs66iQRKtxMKS0VI6QTKZDAbs7a7H1yBv+mJi9Xg4uKmyU6qnn7zEKjjY2bIlCwezx4LLGP5HD3Zr6fZsmdG1UHzDsBvYG2oCI0oqEKM+valsT6hU8MRjbQBD/JJ8JJN/iBGlhBAUHo0UmhhtR2euTVpZWlMnhmpXAdKDXSfPdaPohgB', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_red_team', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAsabDquegHi6YUpCn', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7509, 'output_tokens': 88, 'output_tokens_details': {'thinking_tokens': 55}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='verifier', id='lc_run--01a0fe03-6682-7d21-8c15-ffbce9422c9f-0', tool_calls=[{'name': 'transfer_to_red_team', 'args': {}, 'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7509, 'output_tokens': 88, 'total_tokens': 7597, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 55}}), ToolMessage(content='Successfully transferred to red_team', name='transfer_to_red_team', id='982c0126-e6ab-458a-922e-7b266e83c2b2', tool_call_id='toolu_01XEAuhQHCBdyCwtymZwFfDi'), AIMessage(content=[{'signature': 'CAQSwgcKEAgSGAI4AUIIdGhpbmtpbmcSDORXge9uL1832RMQwBoMjQwAwAHb9iP1ojPGIjDDScal486beCJbtG8/D2Y9g9S5xIPfjoHg/dxwX5HypSQEBbjFiTSbPECXSFzV6YIq3wbLcmyPbvusGnDexFgx+3sIXzvqyFV/69xhihMnSpfTCHy0+0zSu1WpYU/WUpIfoaSXTcTa1qkM8x8Ek/y/1jkKMZUfDFa93uhUJxOC4+QgPWBl/+Xy2t82L2x/ebq7QLQTqtR0jXeaURQm3Co6JvOgWmDvMpEdTeTWE3fDh1tzzVKsflum4d89b6CaKtn8nlQd7RhN99vNQMQHG5V4OuVNdrHPZZpFOuhdejsMFWXyB+55slGAd8wEXgFKbI/knOXVGzL/NDF8Gty1tNZlu2fdSCAsYWMsFQP1cbwLeQGszyXzJMVZJ2I9xBPkyz1HOXCPyh5F+j2gRTPikVB3ARC6tfB/lDSUeKBVZPQS8pY/B4eCs+IjjaBNWruLzfqCsc6xL0CuQPSYyQXaYFjYrSRNcC70WQBWT5VVhp0mA9yZDGYbRZt9I5OK9QkQmuJ5zDFaMEOXzSrsC4lteP6MdimiMijvsH3iq+c6WUYolRj3neI88EaHIzLAGRq64BNCLzdAYL2pEU2sb5uG78PKVoUUCbVJL5jpDOhLOTO1bQsTuoUpnRRrLQD/kIzV+ljpVBwwfJcyq75TJA/CtPr/xr2SZ9B1b2sbjG6jD0lhtoCddeDgcqgl/LSSkcuaYY8xPWCy+LtNhcqThfmfvnHLJIpL6gjbjW/OQpRfVUwfw1jOb9bWVwcgHhNYfKYGPtM8oth5xwLYRRVgzqMKOvs7falXRhVzZ2SP6Iw0BMmAAZ6HjfCl9L+tPqJNlnQQOHpoWjum4B+XsYldFEKGQWQqsd59iNPvao9p/sCOt7rGqCozcmgiEm4w66ez/QIDyb46n0+tcNJPUcoBKvQ/jDwE6VwmkQ/nECjFp3UdDT+kWYQSNd8j8VBZOpG8LlqUYrEZCv0q7Bs1vcNrdYiaFoS32osaynWk+Ht3OHuFWbEF8JlsFTQAO05VvYkecf0V/oTHsG5hyOOs7UEaDFq7uaJuzWVhh5pAojIaBcR8fVln2PLRTX7is2nsA+QrdidePcZtFtgxAiKYe1i2sSFi4vm8EOkCbQ63wdQeY4Urr5PuoMEwxUULjgKagiAmsJDq4RrVm7sO4+6YuA9j6l2uylkOEc67vf/5fkXuCsiApNL6keb8c+vL0n4FXGdzj+apSDfFGBgB', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01VBrWdWEWYgit4kNYehBMU5', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_editor', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAzfjqUopoEmZLaqm1', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7416, 'output_tokens': 151, 'output_tokens_details': {'thinking_tokens': 119}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='red_team', id='lc_run--01a0fe03-6ce6-7251-b796-4f64333f67c6-0', tool_calls=[{'name': 'transfer_to_editor', 'args': {}, 'id': 'toolu_01VBrWdWEWYgit4kNYehBMU5', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7416, 'output_tokens': 151, 'total_tokens': 7567, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 119}}), ToolMessage(content='Successfully transferred to editor', name='transfer_to_editor', tool_call_id='toolu_01VBrWdWEWYgit4kNYehBMU5')], 'active_agent': 'editor'}, goto='editor'))Traceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 709, in invoke\n input = step.invoke(input, config)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 447, in invoke\n ret = self.func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 84, in _write\n self.do_write(\n ~~~~~~~~~~~~~^\n config,\n ^^^^^^^\n writes,\n ^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 126, in do_write\n write(_assemble_writes(writes))\n ~~~~~~~~~~~~~~~~^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 181, in _assemble_writes\n if ww := w.mapper(w.value):\n ~~~~~~~~^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/graph/state.py\", line 1762, in _control_branch\n raise ParentCommand(command)\n\n\nlanggraph.errors.ParentCommand: Command(graph='__parent__', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', id='389058d1-083f-4632-bbc5-09bfd3a0ffd2', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9'), AIMessage(content=[{'signature': 'CAQS1QgKEAgSGAI4AUIIdGhpbmtpbmcSDN6TfU6NDM971lEbzxoMvRUA1J9ox3fJQYT9IjArCQQGHBP4pRHPavXzRlbMbaYHC57Yj+Gbxgr0helvd2OIL/o6Rbt4PFA3FaIFUlgq8gc7KQI9FtGnvSAkLBRzIsUp/2d5Rd2zgEsfkFVeXsn8WYYZkinAQVVy6A7B3LaEJW0xb/x88Di67fedwx0Q+FlZtwcmT3Dcaa11JveyHRr3dm+zGSZP6CXeXNJ8nrTZ7W1YRTysD6w7o7Lp8h1Rwq4f4o8j+QUfY0vje+zJZsxHPmQNt3ykeKh99hgn6dbiz6/EHqmbHjSAu1M4SqEDfqIKThHYenamAZmcQ4XAOo6mdQ6+77VgpV1lC1/k52IiUoJjkGlQrnLCvutiI7KN+sgK8c2+2KCJrPRCvVV+BkStvP5jWWqzytpKQ+nHzhL448OrIA9gdJC39C8eDrXeRnI7t4FqlkXGoMlZAV9UMcrQHRxOMi8tOyTzi9oyBFDTSsQ+LJqer0tz1Ft9gek6vzg50SpVAFQdLERv8R1YqMaeI+xsvTAEW9MRgvRMkzLuzRT2guw3jD+Fjip37xgK5QI0R+CsOAmQPoTZizzcIyAUAuCJFCgq/gD/CibLxoymiVUI1an9Snt3KpNRoWdvWIne33V0pv8PHg9yW2JuLOqQGilLCsK/Q3oQr3upEMXxXGjCL7WUizvRZGFEEV+VplYt7v4tKZMj5pk3vdeM0MJ5XZg7PvUsRzycxcnrT3Knoc3qM+ttqZfTsb8d6U/pQifq7GuahFOHrvroqgJLj+/81Yy1pZFkjfJgY0XklzAnA6CFq/YT0gxbV+TH5IS+WaYOUzK36S8s5qqMwhngv4IzE3ctXXqnhIZXNznyVBqbYa/Gx2bCjcEj2n5RyGYRS/+MculgLBznQ9MkG2D7QP3Mq/nwHXKMIaLS8VK8hG3d0rAjlvnauXShNrJxtkj4qkwnxD4LrEjk1ue+PLcbLqYWwAr8n0s+41jrsIPVyyK9vFaEZrA9KaBxkJyiqx50GZNn7ckx+mkI18gH52iyVPtU8FKMXjugCl6mPLiHYiDQd+G63LudmOKys9itVnBDBNMP2b/I7+bIVRlheGaZWr6MATKl86B3y+0kuOrLRMyWcdibc2iQkZm+8f7dTUwbKbBJsKOFY73/8nlL/JiYpjz6nUIk+IqSjaFyK6XFlCcukWpWHpDJnSvbEzey7qHyCqJ+I+on8iP8GOtempQaS0/4kZGSwlrE6xOj6EwGpfjbDZKHkP/2RAdNYPQq3z7bdVJMJNnFMz6e37mzMpCBihwPdSFTRE3SCY66wvTgJzGCUyhHbSi5i1azzOXd8uHCSpT/6L1o6VU0pjoR8Kaa4wCWHGf7P/WEd+avyp02Jnu1cmpW0+Gq+vK9h0tdb2IClM/eNfdouQP+ep8MJUjraDRICmAJQY5obS71vniOo+reaFy0zRgB', 'thinking': '', 'type': 'thinking'}, {'signature': 'CAQS/REKEQgSGAI4AUIJbmFycmF0aW9uEgwFh3AVJZP3s8y1Hq0aDG76uhHONaScXAGShiIwGgjLXHUx1yZrkLPljjzTOQycpLJiKVwXcxm/EzKJm1s8M5WxuREWEldXcoaN0/msKpkRMT+eLb5ih9rAHULTomUNsUwJeu0jrJe4GLazy7sP5KtjEOEBKQsXUfoQ7fDkZOQUHZi9zzQN4hFNmYJUf3OVqf9TQNdC1+m//HAZPfWmAy6HtDvm7kgBJkCEoiFnm7tn/PX1hXGPRgWc6BTXUMr2WA8aKyqggRUbMH2+JYv5iWZddCc1up1Nbr3sF7WPcot9ZBhyHQXr692zeJk6SJgygIVrASbMly2bBxTWJB623gVgzpCmVSLZFpkKXgPcp8hGyDKOvF6SiQSHqGRxQofr7Vs1aPhfZixROJGD+MlrkggVAnGMyYIDUZpavtCeiU1JmBSz0084bXPtT/tnpsKOnSPYZQo3B0BXKbqA/WsKng8/V3Z8Uuajo6IVZ2hgA1WDduqQOQkExGU6pFuLhebIjuWxSNI/DffvR//LNqCfvdnf0hvoPtDxfx/SawIx4/spq9xbwFzHtLgoHg1F2wFw7clPDlg1PyD/v2yJG3V9T8GL6jGPE6rhfil/u4HNvf7BW0dQzCTTXcqZy4nByy0+p+S7ZxfMl4Kpxa2gOhhSNjfpfuX0QpL4kME8J1nliFN1d8KqEPuDI18BQHUhD/UfY29oA0x510FjM/BsLZ4n5sppkoIi7CFoQq2aAKVNfC1KilhIpRD+1dyDtZQklN8gZpOESHw6ML1TtU0yfAcbmqoxJ6hhjzwH6CMsTQEEy8MbRBM+QKKkIRdRCPHGFW2LkuR50+J0pF/tr6pqVvJeozfiMnrEilB27TZP4nKTJG8rd//vCWen0y7CA/8dBJ9gdp5tx7NVA45k1gdcxER1gYouNX3n1vG3/B1F4+93wXW9Mqo6Us59TQmrb9Nqa7pSQNSrH06w1GxyBAlkFfqAlafk16aMpq416TJYpGy43WCUiCcLWlhiWgz2sgOLAXP9a5weWAKRC8J6jkcUkDeXnW+HFSiPM6+9Bo6kVhLUhN5rc4mHZOZpF7ptPpsQN4XCqLPsqpRH/8o5XpyC6ZneTeF2ExtjhwZVjXDwMbIzVYIbw1y2PPGw8dC7pc4sIVSnI4oQAH0v650ghfhmMevWW4h4Pylbw/DG4fdeT3+8z0f8MOSTtfipx05/QttHlAvODRxa5yDCvOz6n3f1BO0N/4MbGyOM27qW4x8mBVoryUZWfsmhfi0/GfNvB8+ylTGbYNX27OcEF5WZtNqbWEy7V5GbmZUSxspcg7FBZ3j3/3L7muvZXoZi1f5erq4y3lSH+oF7qPFnvieyLOCwubcQy+oyowj4cD0idRecnp+s+52D7+mzF1gpW+f+cJasoxigbxkOloS4yeiKvgM4nWSpGEj12RPx5o80EmziXN9t4zJzW1htA5hXJJeFympMYtyCUCevRWVmrRYktQ7UnCa6tL/AVPi1ni5XCXHfLOuqfK5KLYxjwVtdhhfmCLvpaKVNCEOCf4nNGHqD98wUb0ejWNwDYBnJGkBRw9RVspfwNdoWoVcn2qv8ywGUp7yc/sk2LpI6VziRyvWqbGNG1ZJqVVutRc46+sOE4VSeS2/CTNKNa6NMCf/hxxaUVsYfKdP+S1Lc4ianKp0sUugPpmtoydC0CP2RwjnHLcnzxCnvqxlLQWXMNPTsu2yBcHlWJLtcmdg/McLagy7l5kG6sN7ZxJLD/kAx8E6uqohP+K/jMSrYuQJnhr4tf4biWFY3Jzf/KpAYyVSq+pbfFKXMbL9nHEoZQT15Wyabd6DMAq3QZOthM5pUzdNqUXkcrI1PwjASAg4dVU/2o+98mTXnyhP7VlEkTnddpwyJF+YCCc01pqa6ZX99pnR+YOpIrb5DtPcKq0t5cFCDyQxN1lUuqZcYujvX/GC7+6IpRTz/NkEY/5cntOfZJyd6SkUx/JR5AJIRPCNeib0rcWnnFn0XK8s22x40wLjEtufXDO9+yraf4kHxlBI3SB0meMjepX50CJhAx7NekkRFnwoecLfqIyrjXrgAZsTMPiX94snjdrzisAXtBKanulilSxOfW2HSmfnk0L2GXHJJ3S+vCbgREml9dekfmHhmwEYtv3kfhth7ERIwVT8j69OtIBCmeTIJuA7o73PXc1LJ2whuZK7FZe6NTOs61MP7JVlIc6tbhtOBk3NXU8pBPOCY6Q55d63FtpUU/KCbQ0K2WQ9o5jNGwNNJ6Cw5FDCt+x5YJeP1d2QzDSiq+cSZqwqCZVcNoAftGGzgrRZ4vkpbP0xQZHCtWsUwHVTjrY4NfRDjHdVPRn8J9Xtooc/hIWkqzFaczYTlapu2RCJqe8aEo3jzWZCEJ7AiBzBYraYDgOfeiQyeJUsbbXHdGKiiw22Bdp4M+Yz671MZ0ie9EWRks0Q2DHGUTne9S4tAAJPGbUf4xtCMoJ40Q4QW05QLSZy3wNFAUFYmXE4ECDFB8+IEeNFFF/tgAnfh8GvFh/1xY+LE+ozMminjSRwhq09zQAEkNFyu0IKgvDkyOI+iccF7doWLTUasPxLH1xGPBSKkeWCnSoX/4C2c9du66BbRXamHqae+VCiS6BN953iYKOu8rMkneW+TFjV0CzAf8PNG9LBayUgY5Uw9x0Sap6zI4PpaZKH4d9ktkFU+91yBbN5SRtdd8/EQatJXmBNfw7kRrll+LjwZweWLEb0jT1RpGuDqHLdLTMMgowSczrp2zCsJdQH/seaYrpwkCFR8dRjM7JRUkVa1A0SvdZAX75vuBvvLBzN2c+Rbt5oqy93lqIBIjKb7w2UkRTb4pBWcNKNPUgt3az/PxQxEasaltdmuyvsXYbur3VPNn8JzN/6J5N+QCnz571TccapCmkPvFE+gayXU+UF1VKYyFcUvlev7lsQlmjqzK2QA8X7Q3TzFapqhvJKlt7dzBHraqZBgPgb9N4oKNayi8FZT71+kaYhIhFrESKYRHfHkj5n3kfhgLy+x+jLDbKxMggkgHnJz9RuEHZXBWtkYAQ==', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_verifier', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAMRdS21FeJfzQhM3s', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7396, 'output_tokens': 770, 'output_tokens_details': {'thinking_tokens': 739}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='skeptic', id='lc_run--01a0fe03-4ae9-76c0-89a6-4f0b114e3030-0', tool_calls=[{'name': 'transfer_to_verifier', 'args': {}, 'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7396, 'output_tokens': 770, 'total_tokens': 8166, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 739}}), ToolMessage(content='Successfully transferred to verifier', name='transfer_to_verifier', id='c064563a-974f-4f0f-ab4a-080e63c01cf5', tool_call_id='toolu_01H6fV8JvRJNvXy47eRnd3jH'), AIMessage(content=[{'signature': 'CAQS9gUKEAgSGAI4AUIIdGhpbmtpbmcSDGFyJ1MXoY+Ai6NkbxoMRd2TrUfUU7WrPpA2IjDgy8dUplcZKc7DnJwNLTBfH5UNR+nGnakSPQia8UVIzrjkOnJ2fLrWUsA2oEO8yRsqkwXkkDCsyhB97dwj57RMt91XzgWhL9luNEGO92yW0rNFGoMSUVOxPIrFc5RjyrhKEc3+1RnpxCkylq+LNlQ5NFxLn6v2cF5DzqpvBHNR7XAig+urJIU3gXydCkaGZZDetJtQOUOXgZi9z9100NpdAEkXDo2ICHCGTGpVc0J/pbXxWRBxb6B1/Gl0Yjn68V6HlesH7IoC7GR/3HwRu9NSDBG4ttfQjEWcg+PeAea4Rn7jzx+Tm9H7L1joTKVgDbQg5iKMLfjvPqlnksCLtXOt8jSIS5OYLN7a/7TX6/w5d34vnw8vPDmU1UYi0Q72qUWhUa2vw8OXNpcKL7Bp0lNfBS8rvHY+vBY8/haAoTL3wvLFU1fT+4z/vA3BcsigZDYlfJeDYcxXmRuqxzxEGpXCRe9s1HWtID+41pHu6lpZHkXaghpc6TZHLxNwcVhTb6fMQ7Qo+2L6Pvklh1m32peodxeg2YiPhdpl3w/xkOEu5/JEF7ZBxpdUL+u3wkO5pUlsqfgX7uhzv7HC2WpSWVimrwiQwQOUx0+JPrLtw+lYfq7I3jruo0GW0guWvnuf7JM3YLj84q+JgHomnSSZnnZmrL0UaRvrVyLo877F39ccLS7XoZnnui8J4P58ZTmiLkEJeGcx1Iuag2e33339O7Yew/59tLbQtIGh9VWMwHBnFx39S8/4LEFrK5HldPQhOrloyTtkRs66iQRKtxMKS0VI6QTKZDAbs7a7H1yBv+mJi9Xg4uKmyU6qnn7zEKjjY2bIlCwezx4LLGP5HD3Zr6fZsmdG1UHzDsBvYG2oCI0oqEKM+valsT6hU8MRjbQBD/JJ8JJN/iBGlhBAUHo0UmhhtR2euTVpZWlMnhmpXAdKDXSfPdaPohgB', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_red_team', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAsabDquegHi6YUpCn', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7509, 'output_tokens': 88, 'output_tokens_details': {'thinking_tokens': 55}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='verifier', id='lc_run--01a0fe03-6682-7d21-8c15-ffbce9422c9f-0', tool_calls=[{'name': 'transfer_to_red_team', 'args': {}, 'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7509, 'output_tokens': 88, 'total_tokens': 7597, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 55}}), ToolMessage(content='Successfully transferred to red_team', name='transfer_to_red_team', id='982c0126-e6ab-458a-922e-7b266e83c2b2', tool_call_id='toolu_01XEAuhQHCBdyCwtymZwFfDi'), AIMessage(content=[{'signature': 'CAQSwgcKEAgSGAI4AUIIdGhpbmtpbmcSDORXge9uL1832RMQwBoMjQwAwAHb9iP1ojPGIjDDScal486beCJbtG8/D2Y9g9S5xIPfjoHg/dxwX5HypSQEBbjFiTSbPECXSFzV6YIq3wbLcmyPbvusGnDexFgx+3sIXzvqyFV/69xhihMnSpfTCHy0+0zSu1WpYU/WUpIfoaSXTcTa1qkM8x8Ek/y/1jkKMZUfDFa93uhUJxOC4+QgPWBl/+Xy2t82L2x/ebq7QLQTqtR0jXeaURQm3Co6JvOgWmDvMpEdTeTWE3fDh1tzzVKsflum4d89b6CaKtn8nlQd7RhN99vNQMQHG5V4OuVNdrHPZZpFOuhdejsMFWXyB+55slGAd8wEXgFKbI/knOXVGzL/NDF8Gty1tNZlu2fdSCAsYWMsFQP1cbwLeQGszyXzJMVZJ2I9xBPkyz1HOXCPyh5F+j2gRTPikVB3ARC6tfB/lDSUeKBVZPQS8pY/B4eCs+IjjaBNWruLzfqCsc6xL0CuQPSYyQXaYFjYrSRNcC70WQBWT5VVhp0mA9yZDGYbRZt9I5OK9QkQmuJ5zDFaMEOXzSrsC4lteP6MdimiMijvsH3iq+c6WUYolRj3neI88EaHIzLAGRq64BNCLzdAYL2pEU2sb5uG78PKVoUUCbVJL5jpDOhLOTO1bQsTuoUpnRRrLQD/kIzV+ljpVBwwfJcyq75TJA/CtPr/xr2SZ9B1b2sbjG6jD0lhtoCddeDgcqgl/LSSkcuaYY8xPWCy+LtNhcqThfmfvnHLJIpL6gjbjW/OQpRfVUwfw1jOb9bWVwcgHhNYfKYGPtM8oth5xwLYRRVgzqMKOvs7falXRhVzZ2SP6Iw0BMmAAZ6HjfCl9L+tPqJNlnQQOHpoWjum4B+XsYldFEKGQWQqsd59iNPvao9p/sCOt7rGqCozcmgiEm4w66ez/QIDyb46n0+tcNJPUcoBKvQ/jDwE6VwmkQ/nECjFp3UdDT+kWYQSNd8j8VBZOpG8LlqUYrEZCv0q7Bs1vcNrdYiaFoS32osaynWk+Ht3OHuFWbEF8JlsFTQAO05VvYkecf0V/oTHsG5hyOOs7UEaDFq7uaJuzWVhh5pAojIaBcR8fVln2PLRTX7is2nsA+QrdidePcZtFtgxAiKYe1i2sSFi4vm8EOkCbQ63wdQeY4Urr5PuoMEwxUULjgKagiAmsJDq4RrVm7sO4+6YuA9j6l2uylkOEc67vf/5fkXuCsiApNL6keb8c+vL0n4FXGdzj+apSDfFGBgB', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01VBrWdWEWYgit4kNYehBMU5', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_editor', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAzfjqUopoEmZLaqm1', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7416, 'output_tokens': 151, 'output_tokens_details': {'thinking_tokens': 119}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='red_team', id='lc_run--01a0fe03-6ce6-7251-b796-4f64333f67c6-0', tool_calls=[{'name': 'transfer_to_editor', 'args': {}, 'id': 'toolu_01VBrWdWEWYgit4kNYehBMU5', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7416, 'output_tokens': 151, 'total_tokens': 7567, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 119}}), ToolMessage(content='Successfully transferred to editor', name='transfer_to_editor', tool_call_id='toolu_01VBrWdWEWYgit4kNYehBMU5')], 'active_agent': 'editor'}, goto='editor')" + } + }, + { + "key": "exception.stacktrace", + "value": { + "stringValue": "Exception: ParentCommand(Command(graph='__parent__', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', id='389058d1-083f-4632-bbc5-09bfd3a0ffd2', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9'), AIMessage(content=[{'signature': 'CAQS1QgKEAgSGAI4AUIIdGhpbmtpbmcSDN6TfU6NDM971lEbzxoMvRUA1J9ox3fJQYT9IjArCQQGHBP4pRHPavXzRlbMbaYHC57Yj+Gbxgr0helvd2OIL/o6Rbt4PFA3FaIFUlgq8gc7KQI9FtGnvSAkLBRzIsUp/2d5Rd2zgEsfkFVeXsn8WYYZkinAQVVy6A7B3LaEJW0xb/x88Di67fedwx0Q+FlZtwcmT3Dcaa11JveyHRr3dm+zGSZP6CXeXNJ8nrTZ7W1YRTysD6w7o7Lp8h1Rwq4f4o8j+QUfY0vje+zJZsxHPmQNt3ykeKh99hgn6dbiz6/EHqmbHjSAu1M4SqEDfqIKThHYenamAZmcQ4XAOo6mdQ6+77VgpV1lC1/k52IiUoJjkGlQrnLCvutiI7KN+sgK8c2+2KCJrPRCvVV+BkStvP5jWWqzytpKQ+nHzhL448OrIA9gdJC39C8eDrXeRnI7t4FqlkXGoMlZAV9UMcrQHRxOMi8tOyTzi9oyBFDTSsQ+LJqer0tz1Ft9gek6vzg50SpVAFQdLERv8R1YqMaeI+xsvTAEW9MRgvRMkzLuzRT2guw3jD+Fjip37xgK5QI0R+CsOAmQPoTZizzcIyAUAuCJFCgq/gD/CibLxoymiVUI1an9Snt3KpNRoWdvWIne33V0pv8PHg9yW2JuLOqQGilLCsK/Q3oQr3upEMXxXGjCL7WUizvRZGFEEV+VplYt7v4tKZMj5pk3vdeM0MJ5XZg7PvUsRzycxcnrT3Knoc3qM+ttqZfTsb8d6U/pQifq7GuahFOHrvroqgJLj+/81Yy1pZFkjfJgY0XklzAnA6CFq/YT0gxbV+TH5IS+WaYOUzK36S8s5qqMwhngv4IzE3ctXXqnhIZXNznyVBqbYa/Gx2bCjcEj2n5RyGYRS/+MculgLBznQ9MkG2D7QP3Mq/nwHXKMIaLS8VK8hG3d0rAjlvnauXShNrJxtkj4qkwnxD4LrEjk1ue+PLcbLqYWwAr8n0s+41jrsIPVyyK9vFaEZrA9KaBxkJyiqx50GZNn7ckx+mkI18gH52iyVPtU8FKMXjugCl6mPLiHYiDQd+G63LudmOKys9itVnBDBNMP2b/I7+bIVRlheGaZWr6MATKl86B3y+0kuOrLRMyWcdibc2iQkZm+8f7dTUwbKbBJsKOFY73/8nlL/JiYpjz6nUIk+IqSjaFyK6XFlCcukWpWHpDJnSvbEzey7qHyCqJ+I+on8iP8GOtempQaS0/4kZGSwlrE6xOj6EwGpfjbDZKHkP/2RAdNYPQq3z7bdVJMJNnFMz6e37mzMpCBihwPdSFTRE3SCY66wvTgJzGCUyhHbSi5i1azzOXd8uHCSpT/6L1o6VU0pjoR8Kaa4wCWHGf7P/WEd+avyp02Jnu1cmpW0+Gq+vK9h0tdb2IClM/eNfdouQP+ep8MJUjraDRICmAJQY5obS71vniOo+reaFy0zRgB', 'thinking': '', 'type': 'thinking'}, {'signature': 'CAQS/REKEQgSGAI4AUIJbmFycmF0aW9uEgwFh3AVJZP3s8y1Hq0aDG76uhHONaScXAGShiIwGgjLXHUx1yZrkLPljjzTOQycpLJiKVwXcxm/EzKJm1s8M5WxuREWEldXcoaN0/msKpkRMT+eLb5ih9rAHULTomUNsUwJeu0jrJe4GLazy7sP5KtjEOEBKQsXUfoQ7fDkZOQUHZi9zzQN4hFNmYJUf3OVqf9TQNdC1+m//HAZPfWmAy6HtDvm7kgBJkCEoiFnm7tn/PX1hXGPRgWc6BTXUMr2WA8aKyqggRUbMH2+JYv5iWZddCc1up1Nbr3sF7WPcot9ZBhyHQXr692zeJk6SJgygIVrASbMly2bBxTWJB623gVgzpCmVSLZFpkKXgPcp8hGyDKOvF6SiQSHqGRxQofr7Vs1aPhfZixROJGD+MlrkggVAnGMyYIDUZpavtCeiU1JmBSz0084bXPtT/tnpsKOnSPYZQo3B0BXKbqA/WsKng8/V3Z8Uuajo6IVZ2hgA1WDduqQOQkExGU6pFuLhebIjuWxSNI/DffvR//LNqCfvdnf0hvoPtDxfx/SawIx4/spq9xbwFzHtLgoHg1F2wFw7clPDlg1PyD/v2yJG3V9T8GL6jGPE6rhfil/u4HNvf7BW0dQzCTTXcqZy4nByy0+p+S7ZxfMl4Kpxa2gOhhSNjfpfuX0QpL4kME8J1nliFN1d8KqEPuDI18BQHUhD/UfY29oA0x510FjM/BsLZ4n5sppkoIi7CFoQq2aAKVNfC1KilhIpRD+1dyDtZQklN8gZpOESHw6ML1TtU0yfAcbmqoxJ6hhjzwH6CMsTQEEy8MbRBM+QKKkIRdRCPHGFW2LkuR50+J0pF/tr6pqVvJeozfiMnrEilB27TZP4nKTJG8rd//vCWen0y7CA/8dBJ9gdp5tx7NVA45k1gdcxER1gYouNX3n1vG3/B1F4+93wXW9Mqo6Us59TQmrb9Nqa7pSQNSrH06w1GxyBAlkFfqAlafk16aMpq416TJYpGy43WCUiCcLWlhiWgz2sgOLAXP9a5weWAKRC8J6jkcUkDeXnW+HFSiPM6+9Bo6kVhLUhN5rc4mHZOZpF7ptPpsQN4XCqLPsqpRH/8o5XpyC6ZneTeF2ExtjhwZVjXDwMbIzVYIbw1y2PPGw8dC7pc4sIVSnI4oQAH0v650ghfhmMevWW4h4Pylbw/DG4fdeT3+8z0f8MOSTtfipx05/QttHlAvODRxa5yDCvOz6n3f1BO0N/4MbGyOM27qW4x8mBVoryUZWfsmhfi0/GfNvB8+ylTGbYNX27OcEF5WZtNqbWEy7V5GbmZUSxspcg7FBZ3j3/3L7muvZXoZi1f5erq4y3lSH+oF7qPFnvieyLOCwubcQy+oyowj4cD0idRecnp+s+52D7+mzF1gpW+f+cJasoxigbxkOloS4yeiKvgM4nWSpGEj12RPx5o80EmziXN9t4zJzW1htA5hXJJeFympMYtyCUCevRWVmrRYktQ7UnCa6tL/AVPi1ni5XCXHfLOuqfK5KLYxjwVtdhhfmCLvpaKVNCEOCf4nNGHqD98wUb0ejWNwDYBnJGkBRw9RVspfwNdoWoVcn2qv8ywGUp7yc/sk2LpI6VziRyvWqbGNG1ZJqVVutRc46+sOE4VSeS2/CTNKNa6NMCf/hxxaUVsYfKdP+S1Lc4ianKp0sUugPpmtoydC0CP2RwjnHLcnzxCnvqxlLQWXMNPTsu2yBcHlWJLtcmdg/McLagy7l5kG6sN7ZxJLD/kAx8E6uqohP+K/jMSrYuQJnhr4tf4biWFY3Jzf/KpAYyVSq+pbfFKXMbL9nHEoZQT15Wyabd6DMAq3QZOthM5pUzdNqUXkcrI1PwjASAg4dVU/2o+98mTXnyhP7VlEkTnddpwyJF+YCCc01pqa6ZX99pnR+YOpIrb5DtPcKq0t5cFCDyQxN1lUuqZcYujvX/GC7+6IpRTz/NkEY/5cntOfZJyd6SkUx/JR5AJIRPCNeib0rcWnnFn0XK8s22x40wLjEtufXDO9+yraf4kHxlBI3SB0meMjepX50CJhAx7NekkRFnwoecLfqIyrjXrgAZsTMPiX94snjdrzisAXtBKanulilSxOfW2HSmfnk0L2GXHJJ3S+vCbgREml9dekfmHhmwEYtv3kfhth7ERIwVT8j69OtIBCmeTIJuA7o73PXc1LJ2whuZK7FZe6NTOs61MP7JVlIc6tbhtOBk3NXU8pBPOCY6Q55d63FtpUU/KCbQ0K2WQ9o5jNGwNNJ6Cw5FDCt+x5YJeP1d2QzDSiq+cSZqwqCZVcNoAftGGzgrRZ4vkpbP0xQZHCtWsUwHVTjrY4NfRDjHdVPRn8J9Xtooc/hIWkqzFaczYTlapu2RCJqe8aEo3jzWZCEJ7AiBzBYraYDgOfeiQyeJUsbbXHdGKiiw22Bdp4M+Yz671MZ0ie9EWRks0Q2DHGUTne9S4tAAJPGbUf4xtCMoJ40Q4QW05QLSZy3wNFAUFYmXE4ECDFB8+IEeNFFF/tgAnfh8GvFh/1xY+LE+ozMminjSRwhq09zQAEkNFyu0IKgvDkyOI+iccF7doWLTUasPxLH1xGPBSKkeWCnSoX/4C2c9du66BbRXamHqae+VCiS6BN953iYKOu8rMkneW+TFjV0CzAf8PNG9LBayUgY5Uw9x0Sap6zI4PpaZKH4d9ktkFU+91yBbN5SRtdd8/EQatJXmBNfw7kRrll+LjwZweWLEb0jT1RpGuDqHLdLTMMgowSczrp2zCsJdQH/seaYrpwkCFR8dRjM7JRUkVa1A0SvdZAX75vuBvvLBzN2c+Rbt5oqy93lqIBIjKb7w2UkRTb4pBWcNKNPUgt3az/PxQxEasaltdmuyvsXYbur3VPNn8JzN/6J5N+QCnz571TccapCmkPvFE+gayXU+UF1VKYyFcUvlev7lsQlmjqzK2QA8X7Q3TzFapqhvJKlt7dzBHraqZBgPgb9N4oKNayi8FZT71+kaYhIhFrESKYRHfHkj5n3kfhgLy+x+jLDbKxMggkgHnJz9RuEHZXBWtkYAQ==', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_verifier', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAMRdS21FeJfzQhM3s', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7396, 'output_tokens': 770, 'output_tokens_details': {'thinking_tokens': 739}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='skeptic', id='lc_run--01a0fe03-4ae9-76c0-89a6-4f0b114e3030-0', tool_calls=[{'name': 'transfer_to_verifier', 'args': {}, 'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7396, 'output_tokens': 770, 'total_tokens': 8166, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 739}}), ToolMessage(content='Successfully transferred to verifier', name='transfer_to_verifier', id='c064563a-974f-4f0f-ab4a-080e63c01cf5', tool_call_id='toolu_01H6fV8JvRJNvXy47eRnd3jH'), AIMessage(content=[{'signature': 'CAQS9gUKEAgSGAI4AUIIdGhpbmtpbmcSDGFyJ1MXoY+Ai6NkbxoMRd2TrUfUU7WrPpA2IjDgy8dUplcZKc7DnJwNLTBfH5UNR+nGnakSPQia8UVIzrjkOnJ2fLrWUsA2oEO8yRsqkwXkkDCsyhB97dwj57RMt91XzgWhL9luNEGO92yW0rNFGoMSUVOxPIrFc5RjyrhKEc3+1RnpxCkylq+LNlQ5NFxLn6v2cF5DzqpvBHNR7XAig+urJIU3gXydCkaGZZDetJtQOUOXgZi9z9100NpdAEkXDo2ICHCGTGpVc0J/pbXxWRBxb6B1/Gl0Yjn68V6HlesH7IoC7GR/3HwRu9NSDBG4ttfQjEWcg+PeAea4Rn7jzx+Tm9H7L1joTKVgDbQg5iKMLfjvPqlnksCLtXOt8jSIS5OYLN7a/7TX6/w5d34vnw8vPDmU1UYi0Q72qUWhUa2vw8OXNpcKL7Bp0lNfBS8rvHY+vBY8/haAoTL3wvLFU1fT+4z/vA3BcsigZDYlfJeDYcxXmRuqxzxEGpXCRe9s1HWtID+41pHu6lpZHkXaghpc6TZHLxNwcVhTb6fMQ7Qo+2L6Pvklh1m32peodxeg2YiPhdpl3w/xkOEu5/JEF7ZBxpdUL+u3wkO5pUlsqfgX7uhzv7HC2WpSWVimrwiQwQOUx0+JPrLtw+lYfq7I3jruo0GW0guWvnuf7JM3YLj84q+JgHomnSSZnnZmrL0UaRvrVyLo877F39ccLS7XoZnnui8J4P58ZTmiLkEJeGcx1Iuag2e33339O7Yew/59tLbQtIGh9VWMwHBnFx39S8/4LEFrK5HldPQhOrloyTtkRs66iQRKtxMKS0VI6QTKZDAbs7a7H1yBv+mJi9Xg4uKmyU6qnn7zEKjjY2bIlCwezx4LLGP5HD3Zr6fZsmdG1UHzDsBvYG2oCI0oqEKM+valsT6hU8MRjbQBD/JJ8JJN/iBGlhBAUHo0UmhhtR2euTVpZWlMnhmpXAdKDXSfPdaPohgB', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_red_team', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAsabDquegHi6YUpCn', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7509, 'output_tokens': 88, 'output_tokens_details': {'thinking_tokens': 55}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='verifier', id='lc_run--01a0fe03-6682-7d21-8c15-ffbce9422c9f-0', tool_calls=[{'name': 'transfer_to_red_team', 'args': {}, 'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7509, 'output_tokens': 88, 'total_tokens': 7597, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 55}}), ToolMessage(content='Successfully transferred to red_team', name='transfer_to_red_team', id='982c0126-e6ab-458a-922e-7b266e83c2b2', tool_call_id='toolu_01XEAuhQHCBdyCwtymZwFfDi'), AIMessage(content=[{'signature': 'CAQSwgcKEAgSGAI4AUIIdGhpbmtpbmcSDORXge9uL1832RMQwBoMjQwAwAHb9iP1ojPGIjDDScal486beCJbtG8/D2Y9g9S5xIPfjoHg/dxwX5HypSQEBbjFiTSbPECXSFzV6YIq3wbLcmyPbvusGnDexFgx+3sIXzvqyFV/69xhihMnSpfTCHy0+0zSu1WpYU/WUpIfoaSXTcTa1qkM8x8Ek/y/1jkKMZUfDFa93uhUJxOC4+QgPWBl/+Xy2t82L2x/ebq7QLQTqtR0jXeaURQm3Co6JvOgWmDvMpEdTeTWE3fDh1tzzVKsflum4d89b6CaKtn8nlQd7RhN99vNQMQHG5V4OuVNdrHPZZpFOuhdejsMFWXyB+55slGAd8wEXgFKbI/knOXVGzL/NDF8Gty1tNZlu2fdSCAsYWMsFQP1cbwLeQGszyXzJMVZJ2I9xBPkyz1HOXCPyh5F+j2gRTPikVB3ARC6tfB/lDSUeKBVZPQS8pY/B4eCs+IjjaBNWruLzfqCsc6xL0CuQPSYyQXaYFjYrSRNcC70WQBWT5VVhp0mA9yZDGYbRZt9I5OK9QkQmuJ5zDFaMEOXzSrsC4lteP6MdimiMijvsH3iq+c6WUYolRj3neI88EaHIzLAGRq64BNCLzdAYL2pEU2sb5uG78PKVoUUCbVJL5jpDOhLOTO1bQsTuoUpnRRrLQD/kIzV+ljpVBwwfJcyq75TJA/CtPr/xr2SZ9B1b2sbjG6jD0lhtoCddeDgcqgl/LSSkcuaYY8xPWCy+LtNhcqThfmfvnHLJIpL6gjbjW/OQpRfVUwfw1jOb9bWVwcgHhNYfKYGPtM8oth5xwLYRRVgzqMKOvs7falXRhVzZ2SP6Iw0BMmAAZ6HjfCl9L+tPqJNlnQQOHpoWjum4B+XsYldFEKGQWQqsd59iNPvao9p/sCOt7rGqCozcmgiEm4w66ez/QIDyb46n0+tcNJPUcoBKvQ/jDwE6VwmkQ/nECjFp3UdDT+kWYQSNd8j8VBZOpG8LlqUYrEZCv0q7Bs1vcNrdYiaFoS32osaynWk+Ht3OHuFWbEF8JlsFTQAO05VvYkecf0V/oTHsG5hyOOs7UEaDFq7uaJuzWVhh5pAojIaBcR8fVln2PLRTX7is2nsA+QrdidePcZtFtgxAiKYe1i2sSFi4vm8EOkCbQ63wdQeY4Urr5PuoMEwxUULjgKagiAmsJDq4RrVm7sO4+6YuA9j6l2uylkOEc67vf/5fkXuCsiApNL6keb8c+vL0n4FXGdzj+apSDfFGBgB', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01VBrWdWEWYgit4kNYehBMU5', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_editor', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAzfjqUopoEmZLaqm1', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7416, 'output_tokens': 151, 'output_tokens_details': {'thinking_tokens': 119}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='red_team', id='lc_run--01a0fe03-6ce6-7251-b796-4f64333f67c6-0', tool_calls=[{'name': 'transfer_to_editor', 'args': {}, 'id': 'toolu_01VBrWdWEWYgit4kNYehBMU5', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7416, 'output_tokens': 151, 'total_tokens': 7567, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 119}}), ToolMessage(content='Successfully transferred to editor', name='transfer_to_editor', tool_call_id='toolu_01VBrWdWEWYgit4kNYehBMU5')], 'active_agent': 'editor'}, goto='editor'))Traceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 709, in invoke\n input = step.invoke(input, config)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 447, in invoke\n ret = self.func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 84, in _write\n self.do_write(\n ~~~~~~~~~~~~~^\n config,\n ^^^^^^^\n writes,\n ^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 126, in do_write\n write(_assemble_writes(writes))\n ~~~~~~~~~~~~~~~~^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 181, in _assemble_writes\n if ww := w.mapper(w.value):\n ~~~~~~~~^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/graph/state.py\", line 1762, in _control_branch\n raise ParentCommand(command)\n\n\nlanggraph.errors.ParentCommand: Command(graph='__parent__', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', id='389058d1-083f-4632-bbc5-09bfd3a0ffd2', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9'), AIMessage(content=[{'signature': 'CAQS1QgKEAgSGAI4AUIIdGhpbmtpbmcSDN6TfU6NDM971lEbzxoMvRUA1J9ox3fJQYT9IjArCQQGHBP4pRHPavXzRlbMbaYHC57Yj+Gbxgr0helvd2OIL/o6Rbt4PFA3FaIFUlgq8gc7KQI9FtGnvSAkLBRzIsUp/2d5Rd2zgEsfkFVeXsn8WYYZkinAQVVy6A7B3LaEJW0xb/x88Di67fedwx0Q+FlZtwcmT3Dcaa11JveyHRr3dm+zGSZP6CXeXNJ8nrTZ7W1YRTysD6w7o7Lp8h1Rwq4f4o8j+QUfY0vje+zJZsxHPmQNt3ykeKh99hgn6dbiz6/EHqmbHjSAu1M4SqEDfqIKThHYenamAZmcQ4XAOo6mdQ6+77VgpV1lC1/k52IiUoJjkGlQrnLCvutiI7KN+sgK8c2+2KCJrPRCvVV+BkStvP5jWWqzytpKQ+nHzhL448OrIA9gdJC39C8eDrXeRnI7t4FqlkXGoMlZAV9UMcrQHRxOMi8tOyTzi9oyBFDTSsQ+LJqer0tz1Ft9gek6vzg50SpVAFQdLERv8R1YqMaeI+xsvTAEW9MRgvRMkzLuzRT2guw3jD+Fjip37xgK5QI0R+CsOAmQPoTZizzcIyAUAuCJFCgq/gD/CibLxoymiVUI1an9Snt3KpNRoWdvWIne33V0pv8PHg9yW2JuLOqQGilLCsK/Q3oQr3upEMXxXGjCL7WUizvRZGFEEV+VplYt7v4tKZMj5pk3vdeM0MJ5XZg7PvUsRzycxcnrT3Knoc3qM+ttqZfTsb8d6U/pQifq7GuahFOHrvroqgJLj+/81Yy1pZFkjfJgY0XklzAnA6CFq/YT0gxbV+TH5IS+WaYOUzK36S8s5qqMwhngv4IzE3ctXXqnhIZXNznyVBqbYa/Gx2bCjcEj2n5RyGYRS/+MculgLBznQ9MkG2D7QP3Mq/nwHXKMIaLS8VK8hG3d0rAjlvnauXShNrJxtkj4qkwnxD4LrEjk1ue+PLcbLqYWwAr8n0s+41jrsIPVyyK9vFaEZrA9KaBxkJyiqx50GZNn7ckx+mkI18gH52iyVPtU8FKMXjugCl6mPLiHYiDQd+G63LudmOKys9itVnBDBNMP2b/I7+bIVRlheGaZWr6MATKl86B3y+0kuOrLRMyWcdibc2iQkZm+8f7dTUwbKbBJsKOFY73/8nlL/JiYpjz6nUIk+IqSjaFyK6XFlCcukWpWHpDJnSvbEzey7qHyCqJ+I+on8iP8GOtempQaS0/4kZGSwlrE6xOj6EwGpfjbDZKHkP/2RAdNYPQq3z7bdVJMJNnFMz6e37mzMpCBihwPdSFTRE3SCY66wvTgJzGCUyhHbSi5i1azzOXd8uHCSpT/6L1o6VU0pjoR8Kaa4wCWHGf7P/WEd+avyp02Jnu1cmpW0+Gq+vK9h0tdb2IClM/eNfdouQP+ep8MJUjraDRICmAJQY5obS71vniOo+reaFy0zRgB', 'thinking': '', 'type': 'thinking'}, {'signature': 'CAQS/REKEQgSGAI4AUIJbmFycmF0aW9uEgwFh3AVJZP3s8y1Hq0aDG76uhHONaScXAGShiIwGgjLXHUx1yZrkLPljjzTOQycpLJiKVwXcxm/EzKJm1s8M5WxuREWEldXcoaN0/msKpkRMT+eLb5ih9rAHULTomUNsUwJeu0jrJe4GLazy7sP5KtjEOEBKQsXUfoQ7fDkZOQUHZi9zzQN4hFNmYJUf3OVqf9TQNdC1+m//HAZPfWmAy6HtDvm7kgBJkCEoiFnm7tn/PX1hXGPRgWc6BTXUMr2WA8aKyqggRUbMH2+JYv5iWZddCc1up1Nbr3sF7WPcot9ZBhyHQXr692zeJk6SJgygIVrASbMly2bBxTWJB623gVgzpCmVSLZFpkKXgPcp8hGyDKOvF6SiQSHqGRxQofr7Vs1aPhfZixROJGD+MlrkggVAnGMyYIDUZpavtCeiU1JmBSz0084bXPtT/tnpsKOnSPYZQo3B0BXKbqA/WsKng8/V3Z8Uuajo6IVZ2hgA1WDduqQOQkExGU6pFuLhebIjuWxSNI/DffvR//LNqCfvdnf0hvoPtDxfx/SawIx4/spq9xbwFzHtLgoHg1F2wFw7clPDlg1PyD/v2yJG3V9T8GL6jGPE6rhfil/u4HNvf7BW0dQzCTTXcqZy4nByy0+p+S7ZxfMl4Kpxa2gOhhSNjfpfuX0QpL4kME8J1nliFN1d8KqEPuDI18BQHUhD/UfY29oA0x510FjM/BsLZ4n5sppkoIi7CFoQq2aAKVNfC1KilhIpRD+1dyDtZQklN8gZpOESHw6ML1TtU0yfAcbmqoxJ6hhjzwH6CMsTQEEy8MbRBM+QKKkIRdRCPHGFW2LkuR50+J0pF/tr6pqVvJeozfiMnrEilB27TZP4nKTJG8rd//vCWen0y7CA/8dBJ9gdp5tx7NVA45k1gdcxER1gYouNX3n1vG3/B1F4+93wXW9Mqo6Us59TQmrb9Nqa7pSQNSrH06w1GxyBAlkFfqAlafk16aMpq416TJYpGy43WCUiCcLWlhiWgz2sgOLAXP9a5weWAKRC8J6jkcUkDeXnW+HFSiPM6+9Bo6kVhLUhN5rc4mHZOZpF7ptPpsQN4XCqLPsqpRH/8o5XpyC6ZneTeF2ExtjhwZVjXDwMbIzVYIbw1y2PPGw8dC7pc4sIVSnI4oQAH0v650ghfhmMevWW4h4Pylbw/DG4fdeT3+8z0f8MOSTtfipx05/QttHlAvODRxa5yDCvOz6n3f1BO0N/4MbGyOM27qW4x8mBVoryUZWfsmhfi0/GfNvB8+ylTGbYNX27OcEF5WZtNqbWEy7V5GbmZUSxspcg7FBZ3j3/3L7muvZXoZi1f5erq4y3lSH+oF7qPFnvieyLOCwubcQy+oyowj4cD0idRecnp+s+52D7+mzF1gpW+f+cJasoxigbxkOloS4yeiKvgM4nWSpGEj12RPx5o80EmziXN9t4zJzW1htA5hXJJeFympMYtyCUCevRWVmrRYktQ7UnCa6tL/AVPi1ni5XCXHfLOuqfK5KLYxjwVtdhhfmCLvpaKVNCEOCf4nNGHqD98wUb0ejWNwDYBnJGkBRw9RVspfwNdoWoVcn2qv8ywGUp7yc/sk2LpI6VziRyvWqbGNG1ZJqVVutRc46+sOE4VSeS2/CTNKNa6NMCf/hxxaUVsYfKdP+S1Lc4ianKp0sUugPpmtoydC0CP2RwjnHLcnzxCnvqxlLQWXMNPTsu2yBcHlWJLtcmdg/McLagy7l5kG6sN7ZxJLD/kAx8E6uqohP+K/jMSrYuQJnhr4tf4biWFY3Jzf/KpAYyVSq+pbfFKXMbL9nHEoZQT15Wyabd6DMAq3QZOthM5pUzdNqUXkcrI1PwjASAg4dVU/2o+98mTXnyhP7VlEkTnddpwyJF+YCCc01pqa6ZX99pnR+YOpIrb5DtPcKq0t5cFCDyQxN1lUuqZcYujvX/GC7+6IpRTz/NkEY/5cntOfZJyd6SkUx/JR5AJIRPCNeib0rcWnnFn0XK8s22x40wLjEtufXDO9+yraf4kHxlBI3SB0meMjepX50CJhAx7NekkRFnwoecLfqIyrjXrgAZsTMPiX94snjdrzisAXtBKanulilSxOfW2HSmfnk0L2GXHJJ3S+vCbgREml9dekfmHhmwEYtv3kfhth7ERIwVT8j69OtIBCmeTIJuA7o73PXc1LJ2whuZK7FZe6NTOs61MP7JVlIc6tbhtOBk3NXU8pBPOCY6Q55d63FtpUU/KCbQ0K2WQ9o5jNGwNNJ6Cw5FDCt+x5YJeP1d2QzDSiq+cSZqwqCZVcNoAftGGzgrRZ4vkpbP0xQZHCtWsUwHVTjrY4NfRDjHdVPRn8J9Xtooc/hIWkqzFaczYTlapu2RCJqe8aEo3jzWZCEJ7AiBzBYraYDgOfeiQyeJUsbbXHdGKiiw22Bdp4M+Yz671MZ0ie9EWRks0Q2DHGUTne9S4tAAJPGbUf4xtCMoJ40Q4QW05QLSZy3wNFAUFYmXE4ECDFB8+IEeNFFF/tgAnfh8GvFh/1xY+LE+ozMminjSRwhq09zQAEkNFyu0IKgvDkyOI+iccF7doWLTUasPxLH1xGPBSKkeWCnSoX/4C2c9du66BbRXamHqae+VCiS6BN953iYKOu8rMkneW+TFjV0CzAf8PNG9LBayUgY5Uw9x0Sap6zI4PpaZKH4d9ktkFU+91yBbN5SRtdd8/EQatJXmBNfw7kRrll+LjwZweWLEb0jT1RpGuDqHLdLTMMgowSczrp2zCsJdQH/seaYrpwkCFR8dRjM7JRUkVa1A0SvdZAX75vuBvvLBzN2c+Rbt5oqy93lqIBIjKb7w2UkRTb4pBWcNKNPUgt3az/PxQxEasaltdmuyvsXYbur3VPNn8JzN/6J5N+QCnz571TccapCmkPvFE+gayXU+UF1VKYyFcUvlev7lsQlmjqzK2QA8X7Q3TzFapqhvJKlt7dzBHraqZBgPgb9N4oKNayi8FZT71+kaYhIhFrESKYRHfHkj5n3kfhgLy+x+jLDbKxMggkgHnJz9RuEHZXBWtkYAQ==', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_verifier', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAMRdS21FeJfzQhM3s', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7396, 'output_tokens': 770, 'output_tokens_details': {'thinking_tokens': 739}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='skeptic', id='lc_run--01a0fe03-4ae9-76c0-89a6-4f0b114e3030-0', tool_calls=[{'name': 'transfer_to_verifier', 'args': {}, 'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7396, 'output_tokens': 770, 'total_tokens': 8166, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 739}}), ToolMessage(content='Successfully transferred to verifier', name='transfer_to_verifier', id='c064563a-974f-4f0f-ab4a-080e63c01cf5', tool_call_id='toolu_01H6fV8JvRJNvXy47eRnd3jH'), AIMessage(content=[{'signature': 'CAQS9gUKEAgSGAI4AUIIdGhpbmtpbmcSDGFyJ1MXoY+Ai6NkbxoMRd2TrUfUU7WrPpA2IjDgy8dUplcZKc7DnJwNLTBfH5UNR+nGnakSPQia8UVIzrjkOnJ2fLrWUsA2oEO8yRsqkwXkkDCsyhB97dwj57RMt91XzgWhL9luNEGO92yW0rNFGoMSUVOxPIrFc5RjyrhKEc3+1RnpxCkylq+LNlQ5NFxLn6v2cF5DzqpvBHNR7XAig+urJIU3gXydCkaGZZDetJtQOUOXgZi9z9100NpdAEkXDo2ICHCGTGpVc0J/pbXxWRBxb6B1/Gl0Yjn68V6HlesH7IoC7GR/3HwRu9NSDBG4ttfQjEWcg+PeAea4Rn7jzx+Tm9H7L1joTKVgDbQg5iKMLfjvPqlnksCLtXOt8jSIS5OYLN7a/7TX6/w5d34vnw8vPDmU1UYi0Q72qUWhUa2vw8OXNpcKL7Bp0lNfBS8rvHY+vBY8/haAoTL3wvLFU1fT+4z/vA3BcsigZDYlfJeDYcxXmRuqxzxEGpXCRe9s1HWtID+41pHu6lpZHkXaghpc6TZHLxNwcVhTb6fMQ7Qo+2L6Pvklh1m32peodxeg2YiPhdpl3w/xkOEu5/JEF7ZBxpdUL+u3wkO5pUlsqfgX7uhzv7HC2WpSWVimrwiQwQOUx0+JPrLtw+lYfq7I3jruo0GW0guWvnuf7JM3YLj84q+JgHomnSSZnnZmrL0UaRvrVyLo877F39ccLS7XoZnnui8J4P58ZTmiLkEJeGcx1Iuag2e33339O7Yew/59tLbQtIGh9VWMwHBnFx39S8/4LEFrK5HldPQhOrloyTtkRs66iQRKtxMKS0VI6QTKZDAbs7a7H1yBv+mJi9Xg4uKmyU6qnn7zEKjjY2bIlCwezx4LLGP5HD3Zr6fZsmdG1UHzDsBvYG2oCI0oqEKM+valsT6hU8MRjbQBD/JJ8JJN/iBGlhBAUHo0UmhhtR2euTVpZWlMnhmpXAdKDXSfPdaPohgB', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_red_team', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAsabDquegHi6YUpCn', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7509, 'output_tokens': 88, 'output_tokens_details': {'thinking_tokens': 55}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='verifier', id='lc_run--01a0fe03-6682-7d21-8c15-ffbce9422c9f-0', tool_calls=[{'name': 'transfer_to_red_team', 'args': {}, 'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7509, 'output_tokens': 88, 'total_tokens': 7597, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 55}}), ToolMessage(content='Successfully transferred to red_team', name='transfer_to_red_team', id='982c0126-e6ab-458a-922e-7b266e83c2b2', tool_call_id='toolu_01XEAuhQHCBdyCwtymZwFfDi'), AIMessage(content=[{'signature': 'CAQSwgcKEAgSGAI4AUIIdGhpbmtpbmcSDORXge9uL1832RMQwBoMjQwAwAHb9iP1ojPGIjDDScal486beCJbtG8/D2Y9g9S5xIPfjoHg/dxwX5HypSQEBbjFiTSbPECXSFzV6YIq3wbLcmyPbvusGnDexFgx+3sIXzvqyFV/69xhihMnSpfTCHy0+0zSu1WpYU/WUpIfoaSXTcTa1qkM8x8Ek/y/1jkKMZUfDFa93uhUJxOC4+QgPWBl/+Xy2t82L2x/ebq7QLQTqtR0jXeaURQm3Co6JvOgWmDvMpEdTeTWE3fDh1tzzVKsflum4d89b6CaKtn8nlQd7RhN99vNQMQHG5V4OuVNdrHPZZpFOuhdejsMFWXyB+55slGAd8wEXgFKbI/knOXVGzL/NDF8Gty1tNZlu2fdSCAsYWMsFQP1cbwLeQGszyXzJMVZJ2I9xBPkyz1HOXCPyh5F+j2gRTPikVB3ARC6tfB/lDSUeKBVZPQS8pY/B4eCs+IjjaBNWruLzfqCsc6xL0CuQPSYyQXaYFjYrSRNcC70WQBWT5VVhp0mA9yZDGYbRZt9I5OK9QkQmuJ5zDFaMEOXzSrsC4lteP6MdimiMijvsH3iq+c6WUYolRj3neI88EaHIzLAGRq64BNCLzdAYL2pEU2sb5uG78PKVoUUCbVJL5jpDOhLOTO1bQsTuoUpnRRrLQD/kIzV+ljpVBwwfJcyq75TJA/CtPr/xr2SZ9B1b2sbjG6jD0lhtoCddeDgcqgl/LSSkcuaYY8xPWCy+LtNhcqThfmfvnHLJIpL6gjbjW/OQpRfVUwfw1jOb9bWVwcgHhNYfKYGPtM8oth5xwLYRRVgzqMKOvs7falXRhVzZ2SP6Iw0BMmAAZ6HjfCl9L+tPqJNlnQQOHpoWjum4B+XsYldFEKGQWQqsd59iNPvao9p/sCOt7rGqCozcmgiEm4w66ez/QIDyb46n0+tcNJPUcoBKvQ/jDwE6VwmkQ/nECjFp3UdDT+kWYQSNd8j8VBZOpG8LlqUYrEZCv0q7Bs1vcNrdYiaFoS32osaynWk+Ht3OHuFWbEF8JlsFTQAO05VvYkecf0V/oTHsG5hyOOs7UEaDFq7uaJuzWVhh5pAojIaBcR8fVln2PLRTX7is2nsA+QrdidePcZtFtgxAiKYe1i2sSFi4vm8EOkCbQ63wdQeY4Urr5PuoMEwxUULjgKagiAmsJDq4RrVm7sO4+6YuA9j6l2uylkOEc67vf/5fkXuCsiApNL6keb8c+vL0n4FXGdzj+apSDfFGBgB', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01VBrWdWEWYgit4kNYehBMU5', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_editor', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAzfjqUopoEmZLaqm1', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7416, 'output_tokens': 151, 'output_tokens_details': {'thinking_tokens': 119}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='red_team', id='lc_run--01a0fe03-6ce6-7251-b796-4f64333f67c6-0', tool_calls=[{'name': 'transfer_to_editor', 'args': {}, 'id': 'toolu_01VBrWdWEWYgit4kNYehBMU5', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7416, 'output_tokens': 151, 'total_tokens': 7567, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 119}}), ToolMessage(content='Successfully transferred to editor', name='transfer_to_editor', tool_call_id='toolu_01VBrWdWEWYgit4kNYehBMU5')], 'active_agent': 'editor'}, goto='editor')\n" + } + }, + { + "key": "exception.escaped", + "value": { + "stringValue": "False" + } + } + ] + } + ], + "status": { + "code": 2 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "783b8087e857fd50", + "parentSpanId": "916b875f8575ff2d", + "name": "FilesystemMiddleware.wrap_tool_call", + "kind": 1, + "startTimeUnixNano": "1790968035031665920", + "endTimeUnixNano": "1790968035033541888", + "attributes": [ + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "FilesystemMiddleware.wrap_tool_call" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "2" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "tools" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"__pregel_push\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_push\",0,false]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "red_team:15ccef64-4162-9e02-e87c-4cb0eb6ea16b|tools:f216a445-733e-fcc8-6fba-de3530aa90b7" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "red_team" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "red_team:15ccef64-4162-9e02-e87c-4cb0eb6ea16b" + } + }, + { + "key": "langsmith.metadata.ls_method", + "value": { + "stringValue": "traceable" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "e30=" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJvdXRwdXQiOnsiZ3JhcGgiOiJfX3BhcmVudF9fIiwidXBkYXRlIjp7Im1lc3NhZ2VzIjpbeyJjb250ZW50IjoiRm9yIGEgc3ludGhldGljIHRyYWNpbmcgZml4dHVyZSwgZXhwbGFpbiBQeXRob24gdHVwbGUgdmVyc3VzIGxpc3QgaW1tdXRhYmlsaXR5IGluIGF0IG1vc3QgMTIwIHdvcmRzLiBVc2Ugb25seSBvZmZpY2lhbCBQeXRob24gZG9jdW1lbnRhdGlvbiBpZiB5b3Ugc2VhcmNoLiBIYXZlIHRoZSBza2VwdGljIGFuZCB2ZXJpZmllciBjaGVjayBpdCwgdGhlbiBoYXZlIHRoZSBlZGl0b3Igd3JpdGUgL2Fuc3dlci5tZC4gS2VlcCB0aGUgcnVuIHNob3J0IGFuZCBkbyBub3QgYXNrIGZvbGxvdy11cCBxdWVzdGlvbnMuIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJodW1hbiIsImlkIjoiNDYyMDQzMDUtZjhlZS00Mzg0LTljZTgtZmNkODBiZmNkZjc1In0seyJjb250ZW50IjpbeyJpZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7InF1ZXJ5IjoiZG9jcy5weXRob24ub3JnIHR1cGxlcyBpbW11dGFibGUgc2VxdWVuY2VzIGxpc3RzIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIHR1dG9yaWFsIn0sIm5hbWUiOiJ3ZWJfc2VhcmNoIiwidHlwZSI6InRvb2xfdXNlIiwidG9vbHNldF9uYW1lIjpudWxsfV0sImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2ZkdzlhVzlicmVuUHFneWJmTndQIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjI4NDUsIm91dHB1dF90b2tlbnMiOjczLCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjowfSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCIsIm1vZGVsX3Byb3ZpZGVyIjoiYW50aHJvcGljIn0sInR5cGUiOiJhaSIsIm5hbWUiOiJyZXNlYXJjaGVyIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTIxYTMtNzAwMC1hMzgxLTI4NGE2YTBjMDNmNy0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6IndlYl9zZWFyY2giLCJhcmdzIjp7InF1ZXJ5IjoiZG9jcy5weXRob24ub3JnIHR1cGxlcyBpbW11dGFibGUgc2VxdWVuY2VzIGxpc3RzIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIHR1dG9yaWFsIn0sImlkIjoidG9vbHVfMDFCMkxmSEdEallTRkpobWlYTmo0Q3VRIiwidHlwZSI6InRvb2xfY2FsbCJ9XSwiaW52YWxpZF90b29sX2NhbGxzIjpbXSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjoyODQ1LCJvdXRwdXRfdG9rZW5zIjo3MywidG90YWxfdG9rZW5zIjoyOTE4LCJpbnB1dF90b2tlbl9kZXRhaWxzIjp7ImNhY2hlX3JlYWQiOjAsImNhY2hlX2NyZWF0aW9uIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VuX2RldGFpbHMiOnsicmVhc29uaW5nIjowfX19LHsiY29udGVudCI6Ilt7XCJ0aXRsZVwiOiBcIjUuIERhdGEgU3RydWN0dXJlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiWW91IG1pZ2h0IGhhdmUgbm90aWNlZCB0aGF0IG1ldGhvZHMgbGlrZWBpbnNlcnRgLGByZW1vdmVgIG9yYHNvcnRgIHRoYXQgb25seSBtb2RpZnkgdGhlIGxpc3QgaGF2ZSBubyByZXR1cm4gdmFsdWUgcHJpbnRlZCBcXHUyMDEzIHRoZXkgcmV0dXJuIHRoZSBkZWZhdWx0YE5vbmVgLiBbMV0gVGhpcyBpcyBhIGRlc2lnbiBwcmluY2lwbGUgZm9yIGFsbCBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyBpbiBQeXRob24uXFxuLi4uXFxuIyMgNS4zLiBUdXBsZXMgYW5kIFNlcXVlbmNlc1xcdTAwYjZcXG5cXG5XZSBzYXcgdGhhdCBsaXN0cyBhbmQgc3RyaW5ncyBoYXZlIG1hbnkgY29tbW9uIHByb3BlcnRpZXMsIHN1Y2ggYXMgaW5kZXhpbmcgYW5kIHNsaWNpbmcgb3BlcmF0aW9ucy4gVGhleSBhcmUgdHdvIGV4YW1wbGVzIG9mIHNlcXVlbmNlIGRhdGEgdHlwZXMgKHNlZSBTZXF1ZW5jZSBUeXBlcyBcXHUyMDE0IGxpc3QsIHR1cGxlLCByYW5nZSkuIFNpbmNlIFB5dGhvbiBpcyBhbiBldm9sdmluZyBsYW5ndWFnZSwgb3RoZXIgc2VxdWVuY2UgZGF0YSB0eXBlcyBtYXkgYmUgYWRkZWQuIFRoZXJlIGlzIGFsc28gYW5vdGhlciBzdGFuZGFyZCBzZXF1ZW5jZSBkYXRhIHR5cGU6IHRoZSB0dXBsZS5cXG5cXG5BIHR1cGxlIGNvbnNpc3RzIG9mIGEgbnVtYmVyIG9mIHZhbHVlcyBzZXBhcmF0ZWQgYnkgY29tbWFzLCBmb3IgaW5zdGFuY2U6XFxuLi4uXFxuPj4+ICMgVHVwbGVzIGFyZSBpbW11dGFibGU6XFxuPj4+IHRbMF0gPSA4ODg4OFxcblRyYWNlYmFjayAobW9zdCByZWNlbnQgY2FsbCBsYXN0KTpcXG4gIEZpbGUgXFxcIjxzdGRpbj5cXFwiLCBsaW5lIDEsIGluIDxtb2R1bGU+XFxuVHlwZUVycm9yOiAndHVwbGUnIG9iamVjdCBkb2VzIG5vdCBzdXBwb3J0IGl0ZW0gYXNzaWdubWVudFxcbj4+PiAjIGJ1dCB0aGV5IGNhbiBjb250YWluIG11dGFibGUgb2JqZWN0czpcXG4+Pj4gdiA9IChbMSwgMiwgM10sIFszLCAyLCAxXSlcXG4+Pj4gdlxcbihbMSwgMiwgM10sIFszLCAyLCAxXSlcXG4uLi5cXG5BcyB5b3Ugc2VlLCBvbiBvdXRwdXQgdHVwbGVzIGFyZSBhbHdheXMgZW5jbG9zZWQgaW4gcGFyZW50aGVzZXMsIHNvIHRoYXQgbmVzdGVkIHR1cGxlcyBhcmUgaW50ZXJwcmV0ZWQgY29ycmVjdGx5OyB0aGV5IG1heSBiZSBpbnB1dCB3aXRoIG9yIHdpdGhvdXQgc3Vycm91bmRpbmcgcGFyZW50aGVzZXMsIGFsdGhvdWdoIG9mdGVuIHBhcmVudGhlc2VzIGFyZSBuZWNlc3NhcnkgYW55d2F5IChpZiB0aGUgdHVwbGUgaXMgcGFydCBvZiBhIGxhcmdlciBleHByZXNzaW9uKS4gSXQgaXMgbm90IHBvc3NpYmxlIHRvIGFzc2lnbiB0byB0aGUgaW5kaXZpZHVhbCBpdGVtcyBvZiBhIHR1cGxlLCBob3dldmVyIGl0IGlzIHBvc3NpYmxlIHRvIGNyZWF0ZSB0dXBsZXMgd2hpY2ggY29udGFpbiBtdXRhYmxlIG9iamVjdHMsIHN1Y2ggYXMgbGlzdHMuXFxuXFxuVGhvdWdoIHR1cGxlcyBtYXkgc2VlbSBzaW1pbGFyIHRvIGxpc3RzLCB0aGV5IGFyZSBvZnRlbiB1c2VkIGluIGRpZmZlcmVudCBzaXR1YXRpb25zIGFuZCBmb3IgZGlmZmVyZW50IHB1cnBvc2VzLiBUdXBsZXMgYXJlIGltbXV0YWJsZSwgYW5kIHVzdWFsbHkgY29udGFpbiBhIGhldGVyb2dlbmVvdXMgc2VxdWVuY2Ugb2YgZWxlbWVudHMgdGhhdCBhcmUgYWNjZXNzZWQgdmlhIHVucGFja2luZyAoc2VlIGxhdGVyIGluIHRoaXMgc2VjdGlvbikgb3IgaW5kZXhpbmcgKG9yIGV2ZW4gYnkgYXR0cmlidXRlIGluIHRoZSBjYXNlIG9mIG5hbWVkdHVwbGVzKS4gTGlzdHMgYXJlIG11dGFibGUsIGFuZCB0aGVpciBlbGVtZW50cyBhcmUgdXN1YWxseSBob21vZ2VuZW91cyBhbmQgYXJlIGFjY2Vzc2VkIGJ5IGl0ZXJhdGluZyBvdmVyIHRoZSBsaXN0Llxcbi4uLlxcbkFub3RoZXIgdXNlZnVsIGRhdGEgdHlwZSBidWlsdCBpbnRvIFB5dGhvbiBpcyB0aGUgZGljdGlvbmFyeSAoc2VlIE1hcHBpbmcgVHlwZXMgXFx1MjAxNCBkaWN0KS4gRGljdGlvbmFyaWVzIGFyZSBzb21ldGltZXMgZm91bmQgaW4gb3RoZXIgbGFuZ3VhZ2VzIGFzIFxcdTIwMWNhc3NvY2lhdGl2ZSBtZW1vcmllc1xcdTIwMWQgb3IgXFx1MjAxY2Fzc29jaWF0aXZlIGFycmF5c1xcdTIwMWQuIFVubGlrZSBzZXF1ZW5jZXMsIHdoaWNoIGFyZSBpbmRleGVkIGJ5IGEgcmFuZ2Ugb2YgbnVtYmVycywgZGljdGlvbmFyaWVzIGFyZSBpbmRleGVkIGJ5IGtleXMsIHdoaWNoIGNhbiBiZSBhbnkgaW1tdXRhYmxlIHR5cGU7IHN0cmluZ3MgYW5kIG51bWJlcnMgY2FuIGFsd2F5cyBiZSBrZXlzLiBUdXBsZXMgY2FuIGJlIHVzZWQgYXMga2V5cyBpZiB0aGV5IGNvbnRhaW4gb25seSBzdHJpbmdzLCBudW1iZXJzLCBvciB0dXBsZXM7IGlmIGEgdHVwbGUgY29udGFpbnMgYW55IG11dGFibGUgb2JqZWN0IGVpdGhlciBkaXJlY3RseSBvciBpbmRpcmVjdGx5LCBpdCBjYW5ub3QgYmUgdXNlZCBhcyBhIGtleS4gWW91IGNhblxcdTIwMTl0IHVzZSBsaXN0cyBhcyBrZXlzLCBzaW5jZSBsaXN0cyBjYW4gYmUgbW9kaWZpZWQgaW4gcGxhY2UgdXNpbmcgaW5kZXggYXNzaWdubWVudHMsIHNsaWNlIGFzc2lnbm1lbnRzLCBvciBtZXRob2RzIGxpa2UgYXBwZW5kKCkgYW5kIGV4dGVuZCgpLlwiXX0sIHtcInRpdGxlXCI6IFwiMy4gRGF0YSBtb2RlbCBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvcmVmZXJlbmNlL2RhdGFtb2RlbC5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJUaGUgdmFsdWUgb2Ygc29tZSBvYmplY3RzIGNhbiBjaGFuZ2UuIE9iamVjdHMgd2hvc2UgdmFsdWUgY2FuIGNoYW5nZSBhcmUgc2FpZCB0byBiZSBtdXRhYmxlOyBvYmplY3RzIHdob3NlIHZhbHVlIGlzIHVuY2hhbmdlYWJsZSBvbmNlIHRoZXkgYXJlIGNyZWF0ZWQgYXJlIGNhbGxlZCBpbW11dGFibGUuIChUaGUgdmFsdWUgb2YgYW4gaW1tdXRhYmxlIGNvbnRhaW5lciBvYmplY3QgdGhhdCBjb250YWlucyBhIHJlZmVyZW5jZSB0byBhIG11dGFibGUgb2JqZWN0IGNhbiBjaGFuZ2Ugd2hlbiB0aGUgbGF0dGVyXFx1MjAxOXMgdmFsdWUgaXMgY2hhbmdlZDsgaG93ZXZlciB0aGUgY29udGFpbmVyIGlzIHN0aWxsIGNvbnNpZGVyZWQgaW1tdXRhYmxlLCBiZWNhdXNlIHRoZSBjb2xsZWN0aW9uIG9mIG9iamVjdHMgaXQgY29udGFpbnMgY2Fubm90IGJlIGNoYW5nZWQuIFNvLCBpbW11dGFiaWxpdHkgaXMgbm90IHN0cmljdGx5IHRoZSBzYW1lIGFzIGhhdmluZyBhbiB1bmNoYW5nZWFibGUgdmFsdWUsIGl0IGlzIG1vcmUgc3VidGxlLikgQW4gb2JqZWN0XFx1MjAxOXMgbXV0YWJpbGl0eSBpcyBkZXRlcm1pbmVkIGJ5IGl0cyB0eXBlOyBmb3IgaW5zdGFuY2UsIG51bWJlcnMsIHN0cmluZ3MgYW5kIHR1cGxlcyBhcmUgaW1tdXRhYmxlLCB3aGlsZSBkaWN0aW9uYXJpZXMgYW5kIGxpc3RzIGFyZSBtdXRhYmxlLlxcbi4uLlxcblNvbWUgb2JqZWN0cyBjb250YWluIHJlZmVyZW5jZXMgdG8gb3RoZXIgb2JqZWN0czsgdGhlc2UgYXJlIGNhbGxlZCBjb250YWluZXJzLiBFeGFtcGxlcyBvZiBjb250YWluZXJzIGFyZSB0dXBsZXMsIGxpc3RzIGFuZCBkaWN0aW9uYXJpZXMuIFRoZSByZWZlcmVuY2VzIGFyZSBwYXJ0IG9mIGEgY29udGFpbmVyXFx1MjAxOXMgdmFsdWUuIEluIG1vc3QgY2FzZXMsIHdoZW4gd2UgdGFsayBhYm91dCB0aGUgdmFsdWUgb2YgYSBjb250YWluZXIsIHdlIGltcGx5IHRoZSB2YWx1ZXMsIG5vdCB0aGUgaWRlbnRpdGllcyBvZiB0aGUgY29udGFpbmVkIG9iamVjdHM7IGhvd2V2ZXIsIHdoZW4gd2UgdGFsayBhYm91dCB0aGUgbXV0YWJpbGl0eSBvZiBhIGNvbnRhaW5lciwgb25seSB0aGUgaWRlbnRpdGllcyBvZiB0aGUgaW1tZWRpYXRlbHkgY29udGFpbmVkIG9iamVjdHMgYXJlIGltcGxpZWQuIFNvLCBpZiBhbiBpbW11dGFibGUgY29udGFpbmVyIChsaWtlIGEgdHVwbGUpIGNvbnRhaW5zIGEgcmVmZXJlbmNlIHRvIGEgbXV0YWJsZSBvYmplY3QsIGl0cyB2YWx1ZSBjaGFuZ2VzIGlmIHRoYXQgbXV0YWJsZSBvYmplY3QgaXMgY2hhbmdlZC5cXG4uLi5cXG4jIyMgMy4yLjUuIFNlcXVlbmNlc1xcdTAwYjZcXG4uLi5cXG4jIyMjIDMuMi41LjEuIEltbXV0YWJsZSBzZXF1ZW5jZXNcXHUwMGI2XFxuXFxuQW4gb2JqZWN0IG9mIGFuIGltbXV0YWJsZSBzZXF1ZW5jZSB0eXBlIGNhbm5vdCBjaGFuZ2Ugb25jZSBpdCBpcyBjcmVhdGVkLiAoSWYgdGhlIG9iamVjdCBjb250YWlucyByZWZlcmVuY2VzIHRvIG90aGVyIG9iamVjdHMsIHRoZXNlIG90aGVyIG9iamVjdHMgbWF5IGJlIG11dGFibGUgYW5kIG1heSBiZSBjaGFuZ2VkOyBob3dldmVyLCB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGRpcmVjdGx5IHJlZmVyZW5jZWQgYnkgYW4gaW1tdXRhYmxlIG9iamVjdCBjYW5ub3QgY2hhbmdlLilcXG5cXG5UaGUgZm9sbG93aW5nIHR5cGVzIGFyZSBpbW11dGFibGUgc2VxdWVuY2VzOlxcbi4uLlxcblR1cGxlc1xcbjogVGhlIGl0ZW1zIG9mIGEgYHR1cGxlYCBhcmUgYXJiaXRyYXJ5IFB5dGhvbiBvYmplY3RzLiBUdXBsZXMgb2YgdHdvIG9yIG1vcmUgaXRlbXMgYXJlIGZvcm1lZCBieSBjb21tYS1zZXBhcmF0ZWQgbGlzdHMgb2YgZXhwcmVzc2lvbnMuIEEgdHVwbGUgb2Ygb25lIGl0ZW0gKGEgXFx1MjAxOHNpbmdsZXRvblxcdTIwMTkpIGNhbiBiZSBmb3JtZWQgYnkgYWZmaXhpbmcgYSBjb21tYSB0byBhbiBleHByZXNzaW9uIChhbiBleHByZXNzaW9uIGJ5IGl0c2VsZiBkb2VzIG5vdCBjcmVhdGUgYSB0dXBsZSwgc2luY2UgcGFyZW50aGVzZXMgbXVzdCBiZSB1c2FibGUgZm9yIGdyb3VwaW5nIG9mIGV4cHJlc3Npb25zKS4gQW4gZW1wdHkgdHVwbGUgY2FuIGJlIGZvcm1lZCBieSBhbiBlbXB0eSBwYWlyIG9mIHBhcmVudGhlc2VzLlxcbi4uLlxcbiMjIyMgMy4yLjUuMi4gTXV0YWJsZSBzZXF1ZW5jZXNcXHUwMGI2XFxuXFxuTXV0YWJsZSBzZXF1ZW5jZXMgY2FuIGJlIGNoYW5nZWQgYWZ0ZXIgdGhleSBhcmUgY3JlYXRlZC4gVGhlIHN1YnNjcmlwdGlvbiBhbmQgc2xpY2luZyBub3RhdGlvbnMgY2FuIGJlIHVzZWQgYXMgdGhlIHRhcmdldCBvZiBhc3NpZ25tZW50IGFuZCBgZGVsYCAoZGVsZXRlKSBzdGF0ZW1lbnRzLlxcbi4uLlxcblRoZXJlIGFyZSBjdXJyZW50bHkgdHdvIGludHJpbnNpYyBtdXRhYmxlIHNlcXVlbmNlIHR5cGVzOlxcblxcbkxpc3RzXFxuOiBUaGUgaXRlbXMgb2YgYSBsaXN0IGFyZSBhcmJpdHJhcnkgUHl0aG9uIG9iamVjdHMuIExpc3RzIGFyZSBmb3JtZWQgYnkgcGxhY2luZyBhIGNvbW1hLXNlcGFyYXRlZCBsaXN0IG9mIGV4cHJlc3Npb25zIGluIHNxdWFyZSBicmFja2V0cy4gKE5vdGUgdGhhdCB0aGVyZSBhcmUgbm8gc3BlY2lhbCBjYXNlcyBuZWVkZWQgdG8gZm9ybSBsaXN0cyBvZiBsZW5ndGggMCBvciAxLilcIl19LCB7XCJ0aXRsZVwiOiBcIkJ1aWx0LWluIFR5cGVzIFxcdTIwMTQgUHl0aG9uIDMuMTQuNyBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9idWlsdGlucy9zdGR0eXBlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJjb2xsZWN0aW9uIGNsYXNzZXMgYXJlIG11dGFibGUuIFRoZSBtZXRob2RzIHRoYXQgYWRkLCBzdWJ0cmFjdCwgb3IgcmVhcnJhbmdlIHRoZWlyIC4uLiBpbiBwbGFjZSwgYW5kIGRvblxcdTIwMTl0IHJldHVybiBhIC4uLiAsIG5ldmVyIHJldHVybiB0aGUgY29sbGVjdGlvbiBpbnN0YW5jZSBpdHNlbGYgYnV0IGBOb25lYC5cXG4uLi5cXG4jIyBTZXF1ZW5jZSBUeXBlcyBcXHUyMDE0IGBsaXN0YCwgYHR1cGxlYCwgYHJhbmdlYFxcdTAwYjZcXG5cXG5UaGVyZSBhcmUgdGhyZWUgYmFzaWMgc2VxdWVuY2UgdHlwZXM6IGxpc3RzLCB0dXBsZXMsIGFuZCByYW5nZSBvYmplY3RzLiBBZGRpdGlvbmFsIHNlcXVlbmNlIHR5cGVzIHRhaWxvcmVkIGZvciBwcm9jZXNzaW5nIG9mIGJpbmFyeSBkYXRhIGFuZCB0ZXh0IHN0cmluZ3MgYXJlIGRlc2NyaWJlZCBpbiBkZWRpY2F0ZWQgc2VjdGlvbnMuXFxuLi4uXFxuVGhlIG9wZXJhdGlvbnMgaW4gdGhlIGZvbGxvd2luZyB0YWJsZSAuLi4gbXV0YWJsZSBhbmQgaW1tdXRhYmxlLiBUaGUgYGNvbGxlY3Rpb25zLiAuLi4gaXMgcHJvdmlkZWQgdG8gbWFrZSAuLi4gZWFzaWVyIHRvIGNvcnJlY3RseSBpbXBsZW1lbnQgdGhlc2Ugb3BlcmF0aW9ucyBvbiBjdXN0b20gc2VxdWVuY2UgdHlwZXNcXG4uLi5cXG4jIyMgSW1tdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbm11dGFibGUgc2VxdWVuY2UgdHlwZXMgaXNcXG4uLi5cXG5zdXBwb3J0IGFsbG93cyBpbW11dGFibGUgc2VxdWVuY2VzLCAuLi4gLCB0byBiZSB1c2VkIGFzIGBkaWN0YCBrZXlzIGFuZCBzdG9yZWQgaW4gLi4uIGVuc2V0YCBpbnN0YW5jZXNcXG4uLi5cXG4jIyMgTXV0YWJsZSBTZXF1ZW5jZSBUeXBlc1xcdTAwYjZcXG4uLi5cXG4jIyMgTGlzdHNcXHUwMGI2XFxuXFxuTGlzdHMgYXJlIG11dGFibGUgc2VxdWVuY2VzLCB0eXBpY2FsbHkgdXNlZCB0byBzdG9yZSBjb2xsZWN0aW9ucyBvZiBob21vZ2VuZW91cyBpdGVtcyAod2hlcmUgdGhlIHByZWNpc2UgZGVncmVlIG9mIHNpbWlsYXJpdHkgd2lsbCB2YXJ5IGJ5IGFwcGxpY2F0aW9uKS5cXG4uLi5cXG4jIyMgVHVwbGVzXFx1MDBiNlxcblxcblR1cGxlcyBhcmUgaW1tdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaGV0ZXJvZ2VuZW91cyBkYXRhIChzdWNoIGFzIHRoZSAyLXR1cGxlcyBwcm9kdWNlZCBieSB0aGUgYGVudW1lcmF0ZSgpYCBidWlsdC1pbikuIFR1cGxlcyBhcmUgYWxzbyB1c2VkIGZvciBjYXNlcyB3aGVyZSBhbiBpbW11dGFibGUgc2VxdWVuY2Ugb2YgaG9tb2dlbmVvdXMgZGF0YSBpcyBuZWVkZWQgKHN1Y2ggYXMgYWxsb3dpbmcgc3RvcmFnZSBpbiBhIGBzZXRgIG9yIGBkaWN0YCBpbnN0YW5jZSkuXFxuLi4uXFxudHVwbGUoaXRlcmFibGU9XFxuLi4uXFxuVGhlIGNvbnN0cnVjdG9yIGJ1aWxkcyBhIHR1cGxlIHdob3NlIGl0ZW1zIGFyZSB0aGUgc2FtZSBhbmQgaW4gdGhlIHNhbWUgb3JkZXIgYXMgaXRlcmFibGVcXHUyMDE5cyBpdGVtcy4gaXRlcmFibGUgbWF5IGJlIGVpdGhlciBhIHNlcXVlbmNlLCBhIGNvbnRhaW5lciB0aGF0IHN1cHBvcnRzIGl0ZXJhdGlvbiwgb3IgYW4gaXRlcmF0b3Igb2JqZWN0LiBJZiBpdGVyYWJsZSBpcyBhbHJlYWR5IGEgdHVwbGUsIGl0IGlzIHJldHVybmVkIHVuY2hhbmdlZC4gRm9yIGV4YW1wbGUsIGB0dXBsZSgnYWJjJylgIHJldHVybnMgYCgnYScsICdiJywgJ2MnKWAgYW5kIGB0dXBsZSggWzEsIDIsIDNdIClgIHJldHVybnMgYCgxLCAyLCAzKWAuIElmIG5vIGFyZ3VtZW50IGlzIGdpdmVuLCB0aGUgY29uc3RydWN0b3IgY3JlYXRlcyBhIG5ldyBlbXB0eSB0dXBsZSwgYCgpYC5cXG4uLi5cXG5UdXBsZXMgaW1wbGVtZW50IGFsbCBvZiB0aGUgY29tbW9uIHNlcXVlbmNlIG9wZXJhdGlvbnMuXFxuLi4uXFxuRm9yIGhldGVyb2dlbmVvdXMgY29sbGVjdGlvbnMgb2YgZGF0YSB3aGVyZSBhY2Nlc3MgYnkgbmFtZSBpcyBjbGVhcmVyIHRoYW4gYWNjZXNzIGJ5IGluZGV4LCBgY29sbGVjdGlvbnMubmFtZWR0dXBsZSgpYCBtYXkgYmUgYSBtb3JlIGFwcHJvcHJpYXRlIGNob2ljZSB0aGFuIGEgc2ltcGxlIHR1cGxlIG9iamVjdC5cIl19LCB7XCJ0aXRsZVwiOiBcIkJ1aWx0LWluIFR5cGVzIFxcdTIwMTQgUHl0aG9uIDMuMTQuNSBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9saWJyYXJ5L3N0ZHR5cGVzLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcIlNvbWUgY29sbGVjdGlvbiBjbGFzc2VzIGFyZSBtdXRhYmxlLiBUaGUgbWV0aG9kcyB0aGF0IGFkZCwgc3VidHJhY3QsIG9yIHJlYXJyYW5nZSB0aGVpciAuLi4gaW4gcGxhY2UsIGFuZCBkb25cXHUyMDE5dCByZXR1cm4gYSAuLi4gLCBuZXZlciByZXR1cm4gdGhlIGNvbGxlY3Rpb24gaW5zdGFuY2UgaXRzZWxmIGJ1dCBgTm9uZWAuXFxuLi4uXFxuIyMgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBgbGlzdGAsIGB0dXBsZWAsIGByYW5nZWBcXHUwMGI2XFxuXFxuVGhlcmUgYXJlIHRocmVlIGJhc2ljIHNlcXVlbmNlIHR5cGVzOiBsaXN0cywgdHVwbGVzLCBhbmQgLi4uIG9iamVjdHMuIEFkZGl0aW9uYWwgc2VxdWVuY2UgdHlwZXMgdGFpbG9yZWQgZm9yIHByb2Nlc3Npbmcgb2YgYmluYXJ5IGRhdGEgYW5kIHRleHQgc3RyaW5ncyBhcmUgZGVzY3JpYmVkIGluIGRlZGljYXRlZCBzZWN0aW9ucy5cXG4uLi5cXG5UaGUgb3BlcmF0aW9ucyBpbiB0aGUgZm9sbG93aW5nIHRhYmxlIC4uLiBpbW11dGFibGUuIFRoZSBgIC4uLiBpcyBwcm92aWRlZCB0byBtYWtlIC4uLiBlYXNpZXIgdG8gY29ycmVjdGx5IGltcGxlbWVudCB0aGVzZSBvcGVyYXRpb25zIG9uIGN1c3RvbSBzZXF1ZW5jZSB0eXBlc1xcbi4uLlxcbiMjIyBJbW11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxub3BlcmF0aW9uIHRoYXQgaW1tdXRhYmxlIHNlcXVlbmNlIC4uLiBieSBtdXRhYmxlIHNlcXVlbmNlIHR5cGVzIGlzIC4uLiBgaGFzaCgpYFxcbi4uLlxcblRoaXMgc3VwcG9ydCBhbGxvd3MgaW1tdXRhYmxlIHNlcXVlbmNlcywgc3VjaCBhcyBgdHVwbGVgIGluc3RhbmNlcywgdG8gYmUgdXNlZCBhcyBgZGljdGAga2V5cyBhbmQgc3RvcmVkIGluIGBzZXRgIGFuZCBgZnJvemVuc2V0YCBpbnN0YW5jZXMuXFxuLi4uXFxuIyMjIE11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxuIyMjIExpc3RzXFx1MDBiNlxcblxcbkxpc3RzIGFyZSBtdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaG9tb2dlbmVvdXMgaXRlbXMgKHdoZXJlIHRoZSBwcmVjaXNlIGRlZ3JlZSBvZiBzaW1pbGFyaXR5IHdpbGwgdmFyeSBieSBhcHBsaWNhdGlvbikuXFxuLi4uXFxuIyMjIFR1cGxlc1xcdTAwYjZcXG5cXG5UdXBsZXMgYXJlIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhldGVyb2dlbmVvdXMgZGF0YSAoc3VjaCBhcyB0aGUgMi10dXBsZXMgcHJvZHVjZWQgYnkgdGhlIGBlbnVtZXJhdGUoKWAgYnVpbHQtaW4pLiBUdXBsZXMgYXJlIGFsc28gdXNlZCBmb3IgY2FzZXMgd2hlcmUgYW4gaW1tdXRhYmxlIHNlcXVlbmNlIG9mIGhvbW9nZW5lb3VzIGRhdGEgaXMgbmVlZGVkIChzdWNoIGFzIGFsbG93aW5nIHN0b3JhZ2UgaW4gYSBgc2V0YCBvciBgZGljdGAgaW5zdGFuY2UpLlxcbi4uLlxcblRoZSBjb25zdHJ1Y3RvciBidWlsZHMgYSB0dXBsZSB3aG9zZSBpdGVtcyBhcmUgdGhlIHNhbWUgYW5kIGluIHRoZSBzYW1lIG9yZGVyIGFzIGl0ZXJhYmxlXFx1MjAxOXMgaXRlbXMuIGl0ZXJhYmxlIG1heSBiZSBlaXRoZXIgYSBzZXF1ZW5jZSwgYSBjb250YWluZXIgdGhhdCBzdXBwb3J0cyBpdGVyYXRpb24sIG9yIGFuIGl0ZXJhdG9yIG9iamVjdC4gSWYgaXRlcmFibGUgaXMgYWxyZWFkeSBhIHR1cGxlLCBpdCBpcyByZXR1cm5lZCB1bmNoYW5nZWQuIEZvciBleGFtcGxlLCBgdHVwbGUoJ2FiYycpYCByZXR1cm5zIGAoJ2EnLCAnYicsICdjJylgIGFuZCBgdHVwbGUoIFsxLCAyLCAzXSApYCByZXR1cm5zIGAoMSwgMiwgMylgLiBJZiBubyBhcmd1bWVudCBpcyBnaXZlbiwgdGhlIGNvbnN0cnVjdG9yIGNyZWF0ZXMgYSBuZXcgZW1wdHkgdHVwbGUsIGAoKWAuXFxuLi4uXFxuVHVwbGVzIGltcGxlbWVudCBhbGwgb2YgdGhlIGNvbW1vbiBzZXF1ZW5jZSBvcGVyYXRpb25zLlxcbi4uLlxcbkZvciBoZXRlcm9nZW5lb3VzIGNvbGxlY3Rpb25zIG9mIGRhdGEgd2hlcmUgYWNjZXNzIGJ5IG5hbWUgaXMgY2xlYXJlciB0aGFuIGFjY2VzcyBieSBpbmRleCwgYGNvbGxlY3Rpb25zLm5hbWVkdHVwbGUoKWAgbWF5IGJlIGEgbW9yZSBhcHByb3ByaWF0ZSBjaG9pY2UgdGhhbiBhIHNpbXBsZSB0dXBsZSBvYmplY3QuXCJdfSwge1widGl0bGVcIjogXCI1LiBEYXRhIFN0cnVjdHVyZXMgXFx1MjAxNCBQeXRob24gMy4xMC4yMCBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy4xMC90dXRvcmlhbC9kYXRhc3RydWN0dXJlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJZb3UgbWlnaHQgaGF2ZSBub3RpY2VkIHRoYXQgbWV0aG9kcyBsaWtlIGBpbnNlcnRgLCBgcmVtb3ZlYCBvciBgc29ydGAgdGhhdCBvbmx5IG1vZGlmeSB0aGUgbGlzdCBoYXZlIG5vIHJldHVybiB2YWx1ZSBwcmludGVkIFxcdTIwMTMgdGhleSByZXR1cm4gdGhlIGRlZmF1bHQgYE5vbmVgLiAxIFRoaXMgaXMgYSBkZXNpZ24gcHJpbmNpcGxlIGZvciBhbGwgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgaW4gUHl0aG9uLlxcbi4uLlxcbiMjIDUuMy4gVHVwbGVzIGFuZCBTZXF1ZW5jZXNcXHUwMGI2XFxuXFxuV2Ugc2F3IHRoYXQgbGlzdHMgYW5kIHN0cmluZ3MgaGF2ZSBtYW55IGNvbW1vbiBwcm9wZXJ0aWVzLCBzdWNoIGFzIGluZGV4aW5nIGFuZCBzbGljaW5nIG9wZXJhdGlvbnMuIFRoZXkgYXJlIHR3byBleGFtcGxlcyBvZiBzZXF1ZW5jZSBkYXRhIHR5cGVzIChzZWUgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBsaXN0LCB0dXBsZSwgcmFuZ2UpLiBTaW5jZSBQeXRob24gaXMgYW4gZXZvbHZpbmcgbGFuZ3VhZ2UsIG90aGVyIHNlcXVlbmNlIGRhdGEgdHlwZXMgbWF5IGJlIGFkZGVkLiBUaGVyZSBpcyBhbHNvIGFub3RoZXIgc3RhbmRhcmQgc2VxdWVuY2UgZGF0YSB0eXBlOiB0aGUgdHVwbGUuXFxuXFxuQSB0dXBsZSBjb25zaXN0cyBvZiBhIG51bWJlciBvZiB2YWx1ZXMgc2VwYXJhdGVkIGJ5IGNvbW1hcywgZm9yIGluc3RhbmNlOlxcbi4uLlxcbj4+PiAjIFR1cGxlcyBhcmUgaW1tdXRhYmxlOlxcbi4uLiB0WzBdID0gODg4ODhcXG5UcmFjZWJhY2sgKG1vc3QgcmVjZW50IGNhbGwgbGFzdCk6XFxuICBGaWxlIFxcXCI8c3RkaW4+XFxcIiwgbGluZSAxLCBpbiA8bW9kdWxlPlxcblR5cGVFcnJvcjogJ3R1cGxlJyBvYmplY3QgZG9lcyBub3Qgc3VwcG9ydCBpdGVtIGFzc2lnbm1lbnRcXG4+Pj4gIyBidXQgdGhleSBjYW4gY29udGFpbiBtdXRhYmxlIG9iamVjdHM6XFxuLi4uIHYgPSAoWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuPj4+IHZcXG4oWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuLi4uXFxuQXMgeW91IHNlZSwgb24gb3V0cHV0IHR1cGxlcyBhcmUgYWx3YXlzIGVuY2xvc2VkIGluIHBhcmVudGhlc2VzLCBzbyB0aGF0IG5lc3RlZCB0dXBsZXMgYXJlIGludGVycHJldGVkIGNvcnJlY3RseTsgdGhleSBtYXkgYmUgaW5wdXQgd2l0aCBvciB3aXRob3V0IHN1cnJvdW5kaW5nIHBhcmVudGhlc2VzLCBhbHRob3VnaCBvZnRlbiBwYXJlbnRoZXNlcyBhcmUgbmVjZXNzYXJ5IGFueXdheSAoaWYgdGhlIHR1cGxlIGlzIHBhcnQgb2YgYSBsYXJnZXIgZXhwcmVzc2lvbikuIEl0IGlzIG5vdCBwb3NzaWJsZSB0byBhc3NpZ24gdG8gdGhlIGluZGl2aWR1YWwgaXRlbXMgb2YgYSB0dXBsZSwgaG93ZXZlciBpdCBpcyBwb3NzaWJsZSB0byBjcmVhdGUgdHVwbGVzIHdoaWNoIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzLCBzdWNoIGFzIGxpc3RzLlxcblxcblRob3VnaCB0dXBsZXMgbWF5IHNlZW0gc2ltaWxhciB0byBsaXN0cywgdGhleSBhcmUgb2Z0ZW4gdXNlZCBpbiBkaWZmZXJlbnQgc2l0dWF0aW9ucyBhbmQgZm9yIGRpZmZlcmVudCBwdXJwb3Nlcy4gVHVwbGVzIGFyZSBpbW11dGFibGUsIGFuZCB1c3VhbGx5IGNvbnRhaW4gYSBoZXRlcm9nZW5lb3VzIHNlcXVlbmNlIG9mIGVsZW1lbnRzIHRoYXQgYXJlIGFjY2Vzc2VkIHZpYSB1bnBhY2tpbmcgKHNlZSBsYXRlciBpbiB0aGlzIHNlY3Rpb24pIG9yIGluZGV4aW5nIChvciBldmVuIGJ5IGF0dHJpYnV0ZSBpbiB0aGUgY2FzZSBvZiBgbmFtZWR0dXBsZXNgKS4gTGlzdHMgYXJlIG11dGFibGUsIGFuZCB0aGVpciBlbGVtZW50cyBhcmUgdXN1YWxseSBob21vZ2VuZW91cyBhbmQgYXJlIGFjY2Vzc2VkIGJ5IGl0ZXJhdGluZyBvdmVyIHRoZSBsaXN0Llxcbi4uLlxcbiMjIDUuNC5cXG4uLi5cXG4uIFNldCBvYmplY3RzXFxuLi4uXFxuIyMgNS41LiBEaWN0aW9uYXJpZXNcXHUwMGI2XFxuXFxuQW5vdGhlciB1c2VmdWwgZGF0YSB0eXBlIGJ1aWx0IGludG8gUHl0aG9uIGlzIHRoZSBkaWN0aW9uYXJ5IChzZWUgTWFwcGluZyBUeXBlcyBcXHUyMDE0IGRpY3QpLiBEaWN0aW9uYXJpZXMgYXJlIHNvbWV0aW1lcyBmb3VuZCBpbiBvdGhlciBsYW5ndWFnZXMgYXMgXFx1MjAxY2Fzc29jaWF0aXZlIG1lbW9yaWVzXFx1MjAxZCBvciBcXHUyMDFjYXNzb2NpYXRpdmUgYXJyYXlzXFx1MjAxZC4gVW5saWtlIHNlcXVlbmNlcywgd2hpY2ggYXJlIGluZGV4ZWQgYnkgYSByYW5nZSBvZiBudW1iZXJzLCBkaWN0aW9uYXJpZXMgYXJlIGluZGV4ZWQgYnkga2V5cywgd2hpY2ggY2FuIGJlIGFueSBpbW11dGFibGUgdHlwZTsgc3RyaW5ncyBhbmQgbnVtYmVycyBjYW4gYWx3YXlzIGJlIGtleXMuIFR1cGxlcyBjYW4gYmUgdXNlZCBhcyBrZXlzIGlmIHRoZXkgY29udGFpbiBvbmx5IHN0cmluZ3MsIG51bWJlcnMsIG9yIHR1cGxlczsgaWYgYSB0dXBsZSBjb250YWlucyBhbnkgbXV0YWJsZSBvYmplY3QgZWl0aGVyIGRpcmVjdGx5IG9yIGluZGlyZWN0bHksIGl0IGNhbm5vdCBiZSB1c2VkIGFzIGEga2V5LiBZb3UgY2FuXFx1MjAxOXQgdXNlIGxpc3RzIGFzIGtleXMsIHNpbmNlIGxpc3RzIGNhbiBiZSBtb2RpZmllZCBpbiBwbGFjZSB1c2luZyBpbmRleCBhc3NpZ25tZW50cywgc2xpY2UgYXNzaWdubWVudHMsIG9yIG1ldGhvZHMgbGlrZSBgYXBwZW5kKClgIGFuZCBgZXh0ZW5kKClgLlwiXX1dIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJ0b29sIiwibmFtZSI6IndlYl9zZWFyY2giLCJpZCI6IjZlNTEyNWFjLTMxNjQtNGE3Ni05YzUyLWFjMzQ2MGNlMzZlNCIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsInN0YXR1cyI6InN1Y2Nlc3MifSx7ImNvbnRlbnQiOlt7ImlkIjoidG9vbHVfMDFYamozYVJkV3loQkpmUG1USkJ4UWE5IiwiY2FsbGVyIjp7InR5cGUiOiJkaXJlY3QifSwiaW5wdXQiOnt9LCJuYW1lIjoidHJhbnNmZXJfdG9fc2tlcHRpYyIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHdBNkY0Y0NONXloRDRDcnM5TSIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo3Mzc4LCJvdXRwdXRfdG9rZW5zIjozMywib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6MH0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoicmVzZWFyY2hlciIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy0zY2Q4LTc5ZjEtODhhMS1jZTZjZWUyOTRlNzktMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ0cmFuc2Zlcl90b19za2VwdGljIiwiYXJncyI6e30sImlkIjoidG9vbHVfMDFYamozYVJkV3loQkpmUG1USkJ4UWE5IiwidHlwZSI6InRvb2xfY2FsbCJ9XSwiaW52YWxpZF90b29sX2NhbGxzIjpbXSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjo3Mzc4LCJvdXRwdXRfdG9rZW5zIjozMywidG90YWxfdG9rZW5zIjo3NDExLCJpbnB1dF90b2tlbl9kZXRhaWxzIjp7ImNhY2hlX3JlYWQiOjAsImNhY2hlX2NyZWF0aW9uIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VuX2RldGFpbHMiOnsicmVhc29uaW5nIjowfX19LHsiY29udGVudCI6IlN1Y2Nlc3NmdWxseSB0cmFuc2ZlcnJlZCB0byBza2VwdGljIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJ0b29sIiwibmFtZSI6InRyYW5zZmVyX3RvX3NrZXB0aWMiLCJpZCI6IjM4OTA1OGQxLTA4M2YtNDYzMi1iYmM1LTA5YmZkM2EwZmZkMiIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsInN0YXR1cyI6InN1Y2Nlc3MifSx7ImNvbnRlbnQiOlt7InNpZ25hdHVyZSI6IkNBUVMxUWdLRUFnU0dBSTRBVUlJZEdocGJtdHBibWNTRE42VGZVNk5ETTk3MWxFYnp4b012UlVBMUo5b3gzZkpRWVQ5SWpBckNRUUdIQlA0cFJIUGF2WHpSbGJNYmFZSEM1N1lqK0dieGdyMGhlbHZkMk9JTC9vNlJidDRQRkEzRmFJRlVsZ3E4Z2M3S1FJOUZ0R252U0FrTEJSeklzVXAvMmQ1UmQyemdFc2ZrRlZlWHNuOFdZWVpraW5BUVZWeTZBN0IzTGFFSlcweGIveDg4RGk2N2ZlZHd4MFErRmxadHdjbVQzRGNhYTExSnZleUhScjNkbSt6R1NaUDZDWGVYTko4bnJUWjdXMVlSVHlzRDZ3N283THA4aDFSd3E0ZjRvOGorUVVmWTB2amUrekpac3hIUG1RTnQzeWtlS2g5OWhnbjZkYml6Ni9FSHFtYkhqU0F1MU00U3FFRGZxSUtUaEhZZW5hbUFabWNRNFhBT282bWRRNis3N1ZncFYxbEMxL2s1MklpVW9KamtHbFFybkxDdnV0aUk3S04rc2dLOGMyKzJLQ0pyUFJDdlZWK0JrU3R2UDVqV1dxenl0cEtRK25IemhMNDQ4T3JJQTlnZEpDMzlDOGVEclhlUm5JN3Q0RnFsa1hHb01sWkFWOVVNY3JRSFJ4T01pOHRPeVR6aTlveUJGRFRTc1ErTEpxZXIwdHoxRnQ5Z2VrNnZ6ZzUwU3BWQUZRZExFUnY4UjFZcU1hZUkreHN2VEFFVzlNUmd2Uk1rekx1elJUMmd1dzNqRCtGamlwMzd4Z0s1UUkwUitDc09BbVFQb1RaaXp6Y0l5QVVBdUNKRkNncS9nRC9DaWJMeG95bWlWVUkxYW45U250M0twTlJvV2R2V0luZTMzVjBwdjhQSGc5eVcySnVMT3FRR2lsTENzSy9RM29RcjN1cEVNWHhYR2pDTDdXVWl6dlJaR0ZFRVYrVnBsWXQ3djR0S1pNajVwazN2ZGVNME1KNVhaZzdQdlVzUnp5Y3hjbnJUM0tub2MzcU0rdHRxWmZUc2I4ZDZVL3BRaWZxN0d1YWhGT0hydnJvcWdKTGorLzgxWXkxcFpGa2pmSmdZMFhrbHpBbkE2Q0ZxL1lUMGd4YlYrVEg1SVMrV2FZT1V6SzM2UzhzNXFxTXdobmd2NEl6RTNjdFhYcW5oSVpYTnpueVZCcWJZYS9HeDJiQ2pjRWoybjVSeUdZUlMvK01jdWxnTEJ6blE5TWtHMkQ3UVAzTXEvbndIWEtNSWFMUzhWSzhoRzNkMHJBamx2bmF1WFNoTnJKeHRrajRxa3dueEQ0THJFamsxdWUrUExjYkxxWVd3QXI4bjBzKzQxanJzSVBWeXlLOXZGYUVackE5S2FCeGtKeWlxeDUwR1pObjdja3grbWtJMThnSDUyaXlWUHRVOEZLTVhqdWdDbDZtUExpSFlpRFFkK0c2M0x1ZG1PS3lzOWl0Vm5CREJOTVAyYi9JNytiSVZSbGhlR2FaV3I2TUFUS2w4NkIzeSswa3VPckxSTXlXY2RpYmMyaVFrWm0rOGY3ZFRVd2JLYkJKc0tPRlk3My84bmxML0ppWXBqejZuVUlrK0lxU2phRnlLNlhGbENjdWtXcFdIcERKblN2YkV6ZXk3cUh5Q3FKK0krb244aVA4R090ZW1wUWFTMC80a1pHU3dsckU2eE9qNkV3R3BmamJEWktIa1AvMlJBZE5ZUFFxM3o3YmRWSk1KTm5GTXo2ZTM3bXpNcENCaWh3UGRTRlRSRTNTQ1k2Nnd2VGdKekdDVXloSGJTaTVpMWF6ek9YZDh1SENTcFQvNkwxbzZWVTBwam9SOEthYTR3Q1dIR2Y3UC9XRWQrYXZ5cDAySm51MWNtcFcwK0dxK3ZLOWgwdGRiMklDbE0vZU5mZG91UVArZXA4TUpVanJhRFJJQ21BSlFZNW9iUzcxdm5pT28rcmVhRnkwelJnQiIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJzaWduYXR1cmUiOiJDQVFTL1JFS0VRZ1NHQUk0QVVJSmJtRnljbUYwYVc5dUVnd0ZoM0FWSlpQM3M4eTFIcTBhREc3NnVoSE9OYVNjWEFHU2hpSXdHZ2pMWEhVeDF5WnJrTFBsamp6VE9ReWNwTEppS1Z3WGN4bS9FektKbTFzOE01V3h1UkVXRWxkWGNvYU4wL21zS3BrUk1UK2VMYjVpaDlyQUhVTFRvbVVOc1V3SmV1MGpySmU0R0xhenk3c1A1S3RqRU9FQktRc1hVZm9RN2ZEa1pPUVVIWmk5enpRTjRoRk5tWUpVZjNPVnFmOVRRTmRDMSttLy9IQVpQZldtQXk2SHREdm03a2dCSmtDRW9pRm5tN3RuL1BYMWhYR1BSZ1djNkJUWFVNcjJXQThhS3lxZ2dSVWJNSDIrSll2NWlXWmRkQ2MxdXAxTmJyM3NGN1dQY290OVpCaHlIUVhyNjkyemVKazZTSmd5Z0lWckFTYk1seTJiQnhUV0pCNjIzZ1ZnenBDbVZTTFpGcGtLWGdQY3A4aEd5REtPdkY2U2lRU0hxR1J4UW9mcjdWczFhUGhmWml4Uk9KR0QrTWxya2dnVkFuR015WUlEVVpwYXZ0Q2VpVTFKbUJTejAwODRiWFB0VC90bnBzS09uU1BZWlFvM0IwQlhLYnFBL1dzS25nOC9WM1o4VXVham82SVZaMmhnQTFXRGR1cVFPUWtFeEdVNnBGdUxoZWJJanVXeFNOSS9EZmZ2Ui8vTE5xQ2Z2ZG5mMGh2b1B0RHhmeC9TYXdJeDQvc3BxOXhid0Z6SHRMZ29IZzFGMndGdzdjbFBEbGcxUHlEL3YyeUpHM1Y5VDhHTDZqR1BFNnJoZmlsL3U0SE52ZjdCVzBkUXpDVFRYY3FaeTRuQnl5MCtwK1M3WnhmTWw0S3B4YTJnT2hoU05qZnBmdVgwUXBMNGtNRThKMW5saUZOMWQ4S3FFUHVESTE4QlFIVWhEL1VmWTI5b0EweDUxMEZqTS9Cc0xaNG41c3Bwa29JaTdDRm9RcTJhQUtWTmZDMUtpbGhJcFJEKzFkeUR0WlFrbE44Z1pwT0VTSHc2TUwxVHRVMHlmQWNibXFveEo2aGhqendINkNNc1RRRUV5OE1iUkJNK1FLS2tJUmRSQ1BIR0ZXMkxrdVI1MCtKMHBGL3RyNnBxVnZKZW96ZmlNbnJFaWxCMjdUWlA0bktUSkc4cmQvL3ZDV2VuMHk3Q0EvOGRCSjlnZHA1dHg3TlZBNDVrMWdkY3hFUjFnWW91TlgzbjF2RzMvQjFGNCs5M3dYVzlNcW82VXM1OVRRbXJiOU5xYTdwU1FOU3JIMDZ3MUd4eUJBbGtGZnFBbGFmazE2YU1wcTQxNlRKWXBHeTQzV0NVaUNjTFdsaGlXZ3oyc2dPTEFYUDlhNXdlV0FLUkM4SjZqa2NVa0RlWG5XK0hGU2lQTTYrOUJvNmtWaExVaE41cmM0bUhaT1pwRjdwdFBwc1FONFhDcUxQc3FwUkgvOG81WHB5QzZabmVUZUYyRXh0amh3WlZqWER3TWJJelZZSWJ3MXkyUFBHdzhkQzdwYzRzSVZTbkk0b1FBSDB2NjUwZ2hmaG1NZXZXVzRoNFB5bGJ3L0RHNGZkZVQzKzh6MGY4TU9TVHRmaXB4MDUvUXR0SGxBdk9EUnhhNXlEQ3ZPejZuM2YxQk8wTi80TWJHeU9NMjdxVzR4OG1CVm9yeVVaV2ZzbWhmaTAvR2ZOdkI4K3lsVEdiWU5YMjdPY0VGNVdadE5xYldFeTdWNUdibVpVU3hzcGNnN0ZCWjNqMy8zTDdtdXZaWG9aaTFmNWVycTR5M2xTSCtvRjdxUEZudmlleUxPQ3d1YmNReStveW93ajRjRDBpZFJlY25wK3MrNTJENyttekYxZ3BXK2YrY0phc294aWdieGtPbG9TNHllaUt2Z000bldTcEdFajEyUlB4NW84MEVtemlYTjl0NHpKelcxaHRBNWhYSkplRnltcE1ZdHlDVUNldlJXVm1yUllrdFE3VW5DYTZ0TC9BVlBpMW5pNVhDWEhmTE91cWZLNUtMWXhqd1Z0ZGhoZm1DTHZwYUtWTkNFT0NmNG5OR0hxRDk4d1ViMGVqV053RFlCbkpHa0JSdzlSVnNwZndOZG9Xb1ZjbjJxdjh5d0dVcDd5Yy9zazJMcEk2VnppUnl2V3FiR05HMVpKcVZWdXRSYzQ2K3NPRTRWU2VTMi9DVE5LTmE2Tk1DZi9oeHhhVVZzWWZLZFArUzFMYzRpYW5LcDBzVXVnUHBtdG95ZEMwQ1AyUndqbkhMY256eENudnF4bExRV1hNTlBUc3UyeUJjSGxXSkx0Y21kZy9NY0xhZ3k3bDVrRzZzTjdaeEpMRC9rQXg4RTZ1cW9oUCtLL2pNU3JZdVFKbmhyNHRmNGJpV0ZZM0p6Zi9LcEFZeVZTcStwYmZGS1hNYkw5bkhFb1pRVDE1V3lhYmQ2RE1BcTNRWk90aE01cFV6ZE5xVVhrY3JJMVB3akFTQWc0ZFZVLzJvKzk4bVRYbnloUDdWbEVrVG5kZHB3eUpGK1lDQ2MwMXBxYTZaWDk5cG5SK1lPcElyYjVEdFBjS3EwdDVjRkNEeVF4TjFsVXVxWmNZdWp2WC9HQzcrNklwUlR6L05rRVkvNWNudE9mWkp5ZDZTa1V4L0pSNUFKSVJQQ05laWIwcmNXbm5GbjBYSzhzMjJ4NDB3TGpFdHVmWERPOSt5cmFmNGtIeGxCSTNTQjBtZU1qZXBYNTBDSmhBeDdOZWtrUkZud29lY0xmcUl5cmpYcmdBWnNUTVBpWDk0c25qZHJ6aXNBWHRCS2FudWxpbFN4T2ZXMkhTbWZuazBMMkdYSEpKM1MrdkNiZ1JFbWw5ZGVrZm1IaG13RVl0djNrZmh0aDdFUkl3VlQ4ajY5T3RJQkNtZVRJSnVBN283M1BYYzFMSjJ3aHVaSzdGWmU2TlRPczYxTVA3SlZsSWM2dGJodE9CazNOWFU4cEJQT0NZNlE1NWQ2M0Z0cFVVL0tDYlEwSzJXUTlvNWpOR3dOTko2Q3c1RkRDdCt4NVlKZVAxZDJRekRTaXErY1NacXdxQ1pWY05vQWZ0R0d6Z3JSWjR2a3BiUDB4UVpIQ3RXc1V3SFZUanJZNE5mUkRqSGRWUFJuOEo5WHRvb2MvaElXa3F6RmFjellUbGFwdTJSQ0pxZThhRW8zanpXWkNFSjdBaUJ6QllyYVlEZ09mZWlReWVKVXNiYlhIZEdLaWl3MjJCZHA0TStZejY3MU1aMGllOUVXUmtzMFEyREhHVVRuZTlTNHRBQUpQR2JVZjR4dENNb0o0MFE0UVcwNVFMU1p5M3dORkFVRlltWEU0RUNERkI4K0lFZU5GRkYvdGdBbmZoOEd2RmgvMXhZK0xFK296TW1pbmpTUndocTA5elFBRWtORnl1MElLZ3ZEa3lPSStpY2NGN2RvV0xUVWFzUHhMSDF4R1BCU0trZVdDblNvWC80QzJjOWR1NjZCYlJYYW1IcWFlK1ZDaVM2Qk45NTNpWUtPdThyTWtuZVcrVEZqVjBDekFmOFBORzlMQmF5VWdZNVV3OXgwU2FwNnpJNFBwYVpLSDRkOWt0a0ZVKzkxeUJiTjVTUnRkZDgvRVFhdEpYbUJOZnc3a1JybGwrTGp3WndlV0xFYjBqVDFScEd1RHFITGRMVE1NZ293U2N6cnAyekNzSmRRSC9zZWFZcnB3a0NGUjhkUmpNN0pSVWtWYTFBMFN2ZFpBWDc1dnVCdnZMQnpOMmMrUmJ0NW9xeTkzbHFJQklqS2I3dzJVa1JUYjRwQldjTktOUFVndDNhei9QeFF4RWFzYWx0ZG11eXZzWFlidXIzVlBObjhKek4vNko1TitRQ256NTcxVGNjYXBDbWtQdkZFK2dheVhVK1VGMVZLWXlGY1V2bGV2N2xzUWxtanF6SzJRQThYN1EzVHpGYXBxaHZKS2x0N2R6QkhyYXFaQmdQZ2I5TjRvS05heWk4RlpUNzEra2FZaEloRnJFU0tZUkhmSGtqNW4za2ZoZ0x5K3grakxEYkt4TWdna2dIbkp6OVJ1RUhaWEJXdGtZQVE9PSIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJpZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3ZlcmlmaWVyIiwidHlwZSI6InRvb2xfdXNlIiwidG9vbHNldF9uYW1lIjpudWxsfV0sImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2Zkd0FNUmRTMjFGZUpmelFoTTNzIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjczOTYsIm91dHB1dF90b2tlbnMiOjc3MCwib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6NzM5fSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCIsIm1vZGVsX3Byb3ZpZGVyIjoiYW50aHJvcGljIn0sInR5cGUiOiJhaSIsIm5hbWUiOiJza2VwdGljIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTRhZTktNzZjMC04OWE2LTRmMGIxMTRlMzAzMC0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6InRyYW5zZmVyX3RvX3ZlcmlmaWVyIiwiYXJncyI6e30sImlkIjoidG9vbHVfMDFINmZWOEp2UkpOdlh5NDdlUm5kM2pIIiwidHlwZSI6InRvb2xfY2FsbCJ9XSwiaW52YWxpZF90b29sX2NhbGxzIjpbXSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjo3Mzk2LCJvdXRwdXRfdG9rZW5zIjo3NzAsInRvdGFsX3Rva2VucyI6ODE2NiwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6NzM5fX19LHsiY29udGVudCI6IlN1Y2Nlc3NmdWxseSB0cmFuc2ZlcnJlZCB0byB2ZXJpZmllciIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsImlkIjoiYzA2NDU2M2EtOTc0Zi00ZjBmLWFiNGEtMDgwZTYzYzAxY2Y1IiwidG9vbF9jYWxsX2lkIjoidG9vbHVfMDFINmZWOEp2UkpOdlh5NDdlUm5kM2pIIiwic3RhdHVzIjoic3VjY2VzcyJ9LHsiY29udGVudCI6W3sic2lnbmF0dXJlIjoiQ0FRUzlnVUtFQWdTR0FJNEFVSUlkR2hwYm10cGJtY1NER0Z5SjFNWG9ZK0FpNk5rYnhvTVJkMlRyVWZVVTdXclBwQTJJakRneThkVXBsY1pLYzdEbkp3TkxUQmZINVVOUituR25ha1NQUWlhOFVWSXpyamtPbkoyZkxyV1VzQTJvRU84eVJzcWt3WGtrRENzeWhCOTdkd2o1N1JNdDkxWHpnV2hMOWx1TkVHTzkyeVcwck5GR29NU1VWT3hQSXJGYzVSanlyaEtFYzMrMVJucHhDa3lscStMTmxRNU5GeExuNnYyY0Y1RHpxcHZCSE5SN1hBaWcrdXJKSVUzZ1h5ZENrYUdaWkRldEp0UU9VT1hnWmk5ejkxMDBOcGRBRWtYRG8ySUNIQ0dUR3BWYzBKL3BiWHhXUkJ4YjZCMS9HbDBZam42OFY2SGxlc0g3SW9DN0dSLzNId1J1OU5TREJHNHR0ZlFqRVdjZytQZUFlYTRSbjdqengrVG05SDdMMWpvVEtWZ0RiUWc1aUtNTGZqdlBxbG5rc0NMdFhPdDhqU0lTNU9ZTE43YS83VFg2L3c1ZDM0dm53OHZQRG1VMVVZaTBRNzJxVVdoVWEydnc4T1hOcGNLTDdCcDBsTmZCUzhydkhZK3ZCWTgvaGFBb1RMM3d2TEZVMWZUKzR6L3ZBM0Jjc2lnWkRZbGZKZURZY3hYbVJ1cXh6eEVHcFhDUmU5czFIV3RJRCs0MXBIdTZscFpIa1hhZ2hwYzZUWkhMeE53Y1ZoVGI2Zk1RN1FvKzJMNlB2a2xoMW0zMnBlb2R4ZWcyWWlQaGRwbDN3L3hrT0V1NS9KRUY3WkJ4cGRVTCt1M3drTzVwVWxzcWZnWDd1aHp2N0hDMldwU1dWaW1yd2lRd1FPVXgwK0pQckx0dytsWWZxN0kzanJ1bzBHVzBndVd2bnVmN0pNM1lMajg0cStKZ0hvbW5TU1publptckwwVWFSdnJWeUxvODc3RjM5Y2NMUzdYb1pubnVpOEo0UDU4WlRtaUxrRUplR2N4MUl1YWcyZTMzMzM5TzdZZXcvNTl0TGJRdElHaDlWV013SEJuRngzOVM4LzRMRUZySzVIbGRQUWhPcmxveVR0a1JzNjZpUVJLdHhNS1MwVkk2UVRLWkRBYnM3YTdIMXlCdittSmk5WGc0dUtteVU2cW5uN3pFS2pqWTJiSWxDd2V6eDRMTEdQNUhEM1pyNmZac21kRzFVSHpEc0J2WUcyb0NJMG9xRUtNK3ZhbHNUNmhVOE1SamJRQkQvSko4SkpOL2lCR2xoQkFVSG8wVW1oaHRSMmV1VFZwWldsTW5obXBYQWRLRFhTZlBkYVBvaGdCIiwidGhpbmtpbmciOiIiLCJ0eXBlIjoidGhpbmtpbmcifSx7ImlkIjoidG9vbHVfMDFYRUF1aFFIQ0JkeUN3dHltWndGZkRpIiwiY2FsbGVyIjp7InR5cGUiOiJkaXJlY3QifSwiaW5wdXQiOnt9LCJuYW1lIjoidHJhbnNmZXJfdG9fcmVkX3RlYW0iLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QXNhYkRxdWVnSGk2WVVwQ24iLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzUwOSwib3V0cHV0X3Rva2VucyI6ODgsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjU1fSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCIsIm1vZGVsX3Byb3ZpZGVyIjoiYW50aHJvcGljIn0sInR5cGUiOiJhaSIsIm5hbWUiOiJ2ZXJpZmllciIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy02NjgyLTdkMjEtOGMxNS1mZmJjZTk0MjJjOWYtMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ0cmFuc2Zlcl90b19yZWRfdGVhbSIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxWEVBdWhRSENCZHlDd3R5bVp3RmZEaSIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzUwOSwib3V0cHV0X3Rva2VucyI6ODgsInRvdGFsX3Rva2VucyI6NzU5NywiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6NTV9fX0seyJjb250ZW50IjoiU3VjY2Vzc2Z1bGx5IHRyYW5zZmVycmVkIHRvIHJlZF90ZWFtIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJ0b29sIiwibmFtZSI6InRyYW5zZmVyX3RvX3JlZF90ZWFtIiwiaWQiOiI5ODJjMDEyNi1lNmFiLTQ1OGEtOTIyZS03YjI2NmU4M2MyYjIiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMVhFQXVoUUhDQmR5Q3d0eW1ad0ZmRGkiLCJzdGF0dXMiOiJzdWNjZXNzIn0seyJjb250ZW50IjpbeyJzaWduYXR1cmUiOiJDQVFTd2djS0VBZ1NHQUk0QVVJSWRHaHBibXRwYm1jU0RPUlhnZTl1TDE4MzJSTVF3Qm9NalF3QXdBSGI5aVAxb2pQR0lqRERTY2FsNDg2YmVDSmJ0RzgvRDJZOWc5UzV4SVBmam9IZy9keHdYNUh5cFNRRUJiakZpVFNiUEVDWFNGelY2WUlxM3diTGNteVBidnVzR25EZXhGZ3grM3NJWHp2cXlGVi82OXhoaWhNblNwZlRDSHkwKzB6U3UxV3BZVS9XVXBJZm9hU1hUY1RhMXFrTTh4OEVrL3kvMWprS01aVWZERmE5M3VoVUp4T0M0K1FnUFdCbC8rWHkydDgyTDJ4L2VicTdRTFFUcXRSMGpYZWFVUlFtM0NvNkp2T2dXbUR2TXBFZFRlVFdFM2ZEaDF0enpWS3NmbHVtNGQ4OWI2Q2FLdG44bmxRZDdSaE45OXZOUU1RSEc1VjRPdVZOZHJIUFpacEZPdWhkZWpzTUZXWHlCKzU1c2xHQWQ4d0VYZ0ZLYkkva25PWFZHekwvTkRGOEd0eTF0TlpsdTJmZFNDQXNZV01zRlFQMWNid0xlUUdzenlYekpNVlpKMkk5eEJQa3l6MUhPWENQeWg1RitqMmdSVFBpa1ZCM0FSQzZ0ZkIvbERTVWVLQlZaUFFTOHBZL0I0ZUNzK0lqamFCTldydUx6ZnFDc2M2eEwwQ3VRUFNZeVFYYVlGallyU1JOY0M3MFdRQldUNVZWaHAwbUE5eVpER1liUlp0OUk1T0s5UWtRbXVKNXpERmFNRU9YelNyc0M0bHRlUDZNZGltaU1panZzSDNpcStjNldVWW9sUmozbmVJODhFYUhJekxBR1JxNjRCTkNMemRBWUwycEVVMnNiNXVHNzhQS1ZvVVVDYlZKTDVqcERPaExPVE8xYlFzVHVvVXBuUlJyTFFEL2tJelYrbGpwVkJ3d2ZKY3lxNzVUSkEvQ3RQci94cjJTWjlCMWIyc2JqRzZqRDBsaHRvQ2RkZURnY3FnbC9MU1NrY3VhWVk4eFBXQ3krTHROaGNxVGhmbWZ2bkhMSklwTDZnamJqVy9PUXBSZlZVd2Z3MWpPYjliV1Z3Y2dIaE5ZZktZR1B0TThvdGg1eHdMWVJSVmd6cU1LT3ZzN2ZhbFhSaFZ6WjJTUDZJdzBCTW1BQVo2SGpmQ2w5TCt0UHFKTmxuUVFPSHBvV2p1bTRCK1hzWWxkRkVLR1FXUXFzZDU5aU5QdmFvOXAvc0NPdDdyR3FDb3pjbWdpRW00dzY2ZXovUUlEeWI0Nm4wK3RjTkpQVWNvQkt2US9qRHdFNlZ3bWtRL25FQ2pGcDNVZERUK2tXWVFTTmQ4ajhWQlpPcEc4TGxxVVlyRVpDdjBxN0JzMXZjTnJkWWlhRm9TMzJvc2F5bldrK0h0M09IdUZXYkVGOEpsc0ZUUUFPMDVWdllrZWNmMFYvb1RIc0c1aHlPT3M3VUVhREZxN3VhSnV6V1ZoaDVwQW9qSWFCY1I4ZlZsbjJQTFJUWDdpczJuc0ErUXJkaWRlUGNadEZ0Z3hBaUtZZTFpMnNTRmk0dm04RU9rQ2JRNjN3ZFFlWTRVcnI1UHVvTUV3eFVVTGpnS2FnaUFtc0pEcTRSclZtN3NPNCs2WXVBOWo2bDJ1eWxrT0VjNjd2Zi81ZmtYdUNzaUFwTkw2a2ViOGMrdkwwbjRGWEdkemorYXBTRGZGR0JnQiIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJpZCI6InRvb2x1XzAxVkJyV2RXRVdZZ2l0NGtOWWVoQk1VNSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX2VkaXRvciIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHdBemZqcVVvcG9FbVpMYXFtMSIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo3NDE2LCJvdXRwdXRfdG9rZW5zIjoxNTEsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjExOX0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoicmVkX3RlYW0iLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtNmNlNi03MjUxLWI3OTYtNGY2NDMzM2Y2N2M2LTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoidHJhbnNmZXJfdG9fZWRpdG9yIiwiYXJncyI6e30sImlkIjoidG9vbHVfMDFWQnJXZFdFV1lnaXQ0a05ZZWhCTVU1IiwidHlwZSI6InRvb2xfY2FsbCJ9XSwiaW52YWxpZF90b29sX2NhbGxzIjpbXSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjo3NDE2LCJvdXRwdXRfdG9rZW5zIjoxNTEsInRvdGFsX3Rva2VucyI6NzU2NywiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MTE5fX19LHsiY29udGVudCI6IlN1Y2Nlc3NmdWxseSB0cmFuc2ZlcnJlZCB0byBlZGl0b3IiLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7fSwidHlwZSI6InRvb2wiLCJuYW1lIjoidHJhbnNmZXJfdG9fZWRpdG9yIiwidG9vbF9jYWxsX2lkIjoidG9vbHVfMDFWQnJXZFdFV1lnaXQ0a05ZZWhCTVU1Iiwic3RhdHVzIjoic3VjY2VzcyJ9XSwiYWN0aXZlX2FnZW50IjoiZWRpdG9yIn0sInJlc3VtZSI6bnVsbCwiZ290byI6ImVkaXRvciJ9fQ==" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "54441d892eb866e9", + "parentSpanId": "783b8087e857fd50", + "name": "transfer_to_editor", + "kind": 1, + "startTimeUnixNano": "1790968035032363008", + "endTimeUnixNano": "1790968035033051904", + "attributes": [ + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "tool" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "transfer_to_editor" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "execute_tool" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "gen_ai.tool.name", + "value": { + "stringValue": "transfer_to_editor" + } + }, + { + "key": "gen_ai.tool.call.id", + "value": { + "stringValue": "toolu_01VBrWdWEWYgit4kNYehBMU5" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "2" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "tools" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"__pregel_push\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_push\",0,false]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "red_team:15ccef64-4162-9e02-e87c-4cb0eb6ea16b|tools:f216a445-733e-fcc8-6fba-de3530aa90b7" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "red_team" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "red_team:15ccef64-4162-9e02-e87c-4cb0eb6ea16b" + } + }, + { + "key": "langsmith.metadata.__handoff_destination", + "value": { + "stringValue": "editor" + } + }, + { + "key": "langsmith.metadata.ls_method", + "value": { + "stringValue": "traceable" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "seq:step:1" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "e30=" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJvdXRwdXQiOnsiZ3JhcGgiOiJfX3BhcmVudF9fIiwidXBkYXRlIjp7Im1lc3NhZ2VzIjpbeyJjb250ZW50IjoiRm9yIGEgc3ludGhldGljIHRyYWNpbmcgZml4dHVyZSwgZXhwbGFpbiBQeXRob24gdHVwbGUgdmVyc3VzIGxpc3QgaW1tdXRhYmlsaXR5IGluIGF0IG1vc3QgMTIwIHdvcmRzLiBVc2Ugb25seSBvZmZpY2lhbCBQeXRob24gZG9jdW1lbnRhdGlvbiBpZiB5b3Ugc2VhcmNoLiBIYXZlIHRoZSBza2VwdGljIGFuZCB2ZXJpZmllciBjaGVjayBpdCwgdGhlbiBoYXZlIHRoZSBlZGl0b3Igd3JpdGUgL2Fuc3dlci5tZC4gS2VlcCB0aGUgcnVuIHNob3J0IGFuZCBkbyBub3QgYXNrIGZvbGxvdy11cCBxdWVzdGlvbnMuIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJodW1hbiIsImlkIjoiNDYyMDQzMDUtZjhlZS00Mzg0LTljZTgtZmNkODBiZmNkZjc1In0seyJjb250ZW50IjpbeyJpZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7InF1ZXJ5IjoiZG9jcy5weXRob24ub3JnIHR1cGxlcyBpbW11dGFibGUgc2VxdWVuY2VzIGxpc3RzIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIHR1dG9yaWFsIn0sIm5hbWUiOiJ3ZWJfc2VhcmNoIiwidHlwZSI6InRvb2xfdXNlIiwidG9vbHNldF9uYW1lIjpudWxsfV0sImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2ZkdzlhVzlicmVuUHFneWJmTndQIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjI4NDUsIm91dHB1dF90b2tlbnMiOjczLCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjowfSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCIsIm1vZGVsX3Byb3ZpZGVyIjoiYW50aHJvcGljIn0sInR5cGUiOiJhaSIsIm5hbWUiOiJyZXNlYXJjaGVyIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTIxYTMtNzAwMC1hMzgxLTI4NGE2YTBjMDNmNy0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6IndlYl9zZWFyY2giLCJhcmdzIjp7InF1ZXJ5IjoiZG9jcy5weXRob24ub3JnIHR1cGxlcyBpbW11dGFibGUgc2VxdWVuY2VzIGxpc3RzIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIHR1dG9yaWFsIn0sImlkIjoidG9vbHVfMDFCMkxmSEdEallTRkpobWlYTmo0Q3VRIiwidHlwZSI6InRvb2xfY2FsbCJ9XSwiaW52YWxpZF90b29sX2NhbGxzIjpbXSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjoyODQ1LCJvdXRwdXRfdG9rZW5zIjo3MywidG90YWxfdG9rZW5zIjoyOTE4LCJpbnB1dF90b2tlbl9kZXRhaWxzIjp7ImNhY2hlX3JlYWQiOjAsImNhY2hlX2NyZWF0aW9uIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VuX2RldGFpbHMiOnsicmVhc29uaW5nIjowfX19LHsiY29udGVudCI6Ilt7XCJ0aXRsZVwiOiBcIjUuIERhdGEgU3RydWN0dXJlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiWW91IG1pZ2h0IGhhdmUgbm90aWNlZCB0aGF0IG1ldGhvZHMgbGlrZWBpbnNlcnRgLGByZW1vdmVgIG9yYHNvcnRgIHRoYXQgb25seSBtb2RpZnkgdGhlIGxpc3QgaGF2ZSBubyByZXR1cm4gdmFsdWUgcHJpbnRlZCBcXHUyMDEzIHRoZXkgcmV0dXJuIHRoZSBkZWZhdWx0YE5vbmVgLiBbMV0gVGhpcyBpcyBhIGRlc2lnbiBwcmluY2lwbGUgZm9yIGFsbCBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyBpbiBQeXRob24uXFxuLi4uXFxuIyMgNS4zLiBUdXBsZXMgYW5kIFNlcXVlbmNlc1xcdTAwYjZcXG5cXG5XZSBzYXcgdGhhdCBsaXN0cyBhbmQgc3RyaW5ncyBoYXZlIG1hbnkgY29tbW9uIHByb3BlcnRpZXMsIHN1Y2ggYXMgaW5kZXhpbmcgYW5kIHNsaWNpbmcgb3BlcmF0aW9ucy4gVGhleSBhcmUgdHdvIGV4YW1wbGVzIG9mIHNlcXVlbmNlIGRhdGEgdHlwZXMgKHNlZSBTZXF1ZW5jZSBUeXBlcyBcXHUyMDE0IGxpc3QsIHR1cGxlLCByYW5nZSkuIFNpbmNlIFB5dGhvbiBpcyBhbiBldm9sdmluZyBsYW5ndWFnZSwgb3RoZXIgc2VxdWVuY2UgZGF0YSB0eXBlcyBtYXkgYmUgYWRkZWQuIFRoZXJlIGlzIGFsc28gYW5vdGhlciBzdGFuZGFyZCBzZXF1ZW5jZSBkYXRhIHR5cGU6IHRoZSB0dXBsZS5cXG5cXG5BIHR1cGxlIGNvbnNpc3RzIG9mIGEgbnVtYmVyIG9mIHZhbHVlcyBzZXBhcmF0ZWQgYnkgY29tbWFzLCBmb3IgaW5zdGFuY2U6XFxuLi4uXFxuPj4+ICMgVHVwbGVzIGFyZSBpbW11dGFibGU6XFxuPj4+IHRbMF0gPSA4ODg4OFxcblRyYWNlYmFjayAobW9zdCByZWNlbnQgY2FsbCBsYXN0KTpcXG4gIEZpbGUgXFxcIjxzdGRpbj5cXFwiLCBsaW5lIDEsIGluIDxtb2R1bGU+XFxuVHlwZUVycm9yOiAndHVwbGUnIG9iamVjdCBkb2VzIG5vdCBzdXBwb3J0IGl0ZW0gYXNzaWdubWVudFxcbj4+PiAjIGJ1dCB0aGV5IGNhbiBjb250YWluIG11dGFibGUgb2JqZWN0czpcXG4+Pj4gdiA9IChbMSwgMiwgM10sIFszLCAyLCAxXSlcXG4+Pj4gdlxcbihbMSwgMiwgM10sIFszLCAyLCAxXSlcXG4uLi5cXG5BcyB5b3Ugc2VlLCBvbiBvdXRwdXQgdHVwbGVzIGFyZSBhbHdheXMgZW5jbG9zZWQgaW4gcGFyZW50aGVzZXMsIHNvIHRoYXQgbmVzdGVkIHR1cGxlcyBhcmUgaW50ZXJwcmV0ZWQgY29ycmVjdGx5OyB0aGV5IG1heSBiZSBpbnB1dCB3aXRoIG9yIHdpdGhvdXQgc3Vycm91bmRpbmcgcGFyZW50aGVzZXMsIGFsdGhvdWdoIG9mdGVuIHBhcmVudGhlc2VzIGFyZSBuZWNlc3NhcnkgYW55d2F5IChpZiB0aGUgdHVwbGUgaXMgcGFydCBvZiBhIGxhcmdlciBleHByZXNzaW9uKS4gSXQgaXMgbm90IHBvc3NpYmxlIHRvIGFzc2lnbiB0byB0aGUgaW5kaXZpZHVhbCBpdGVtcyBvZiBhIHR1cGxlLCBob3dldmVyIGl0IGlzIHBvc3NpYmxlIHRvIGNyZWF0ZSB0dXBsZXMgd2hpY2ggY29udGFpbiBtdXRhYmxlIG9iamVjdHMsIHN1Y2ggYXMgbGlzdHMuXFxuXFxuVGhvdWdoIHR1cGxlcyBtYXkgc2VlbSBzaW1pbGFyIHRvIGxpc3RzLCB0aGV5IGFyZSBvZnRlbiB1c2VkIGluIGRpZmZlcmVudCBzaXR1YXRpb25zIGFuZCBmb3IgZGlmZmVyZW50IHB1cnBvc2VzLiBUdXBsZXMgYXJlIGltbXV0YWJsZSwgYW5kIHVzdWFsbHkgY29udGFpbiBhIGhldGVyb2dlbmVvdXMgc2VxdWVuY2Ugb2YgZWxlbWVudHMgdGhhdCBhcmUgYWNjZXNzZWQgdmlhIHVucGFja2luZyAoc2VlIGxhdGVyIGluIHRoaXMgc2VjdGlvbikgb3IgaW5kZXhpbmcgKG9yIGV2ZW4gYnkgYXR0cmlidXRlIGluIHRoZSBjYXNlIG9mIG5hbWVkdHVwbGVzKS4gTGlzdHMgYXJlIG11dGFibGUsIGFuZCB0aGVpciBlbGVtZW50cyBhcmUgdXN1YWxseSBob21vZ2VuZW91cyBhbmQgYXJlIGFjY2Vzc2VkIGJ5IGl0ZXJhdGluZyBvdmVyIHRoZSBsaXN0Llxcbi4uLlxcbkFub3RoZXIgdXNlZnVsIGRhdGEgdHlwZSBidWlsdCBpbnRvIFB5dGhvbiBpcyB0aGUgZGljdGlvbmFyeSAoc2VlIE1hcHBpbmcgVHlwZXMgXFx1MjAxNCBkaWN0KS4gRGljdGlvbmFyaWVzIGFyZSBzb21ldGltZXMgZm91bmQgaW4gb3RoZXIgbGFuZ3VhZ2VzIGFzIFxcdTIwMWNhc3NvY2lhdGl2ZSBtZW1vcmllc1xcdTIwMWQgb3IgXFx1MjAxY2Fzc29jaWF0aXZlIGFycmF5c1xcdTIwMWQuIFVubGlrZSBzZXF1ZW5jZXMsIHdoaWNoIGFyZSBpbmRleGVkIGJ5IGEgcmFuZ2Ugb2YgbnVtYmVycywgZGljdGlvbmFyaWVzIGFyZSBpbmRleGVkIGJ5IGtleXMsIHdoaWNoIGNhbiBiZSBhbnkgaW1tdXRhYmxlIHR5cGU7IHN0cmluZ3MgYW5kIG51bWJlcnMgY2FuIGFsd2F5cyBiZSBrZXlzLiBUdXBsZXMgY2FuIGJlIHVzZWQgYXMga2V5cyBpZiB0aGV5IGNvbnRhaW4gb25seSBzdHJpbmdzLCBudW1iZXJzLCBvciB0dXBsZXM7IGlmIGEgdHVwbGUgY29udGFpbnMgYW55IG11dGFibGUgb2JqZWN0IGVpdGhlciBkaXJlY3RseSBvciBpbmRpcmVjdGx5LCBpdCBjYW5ub3QgYmUgdXNlZCBhcyBhIGtleS4gWW91IGNhblxcdTIwMTl0IHVzZSBsaXN0cyBhcyBrZXlzLCBzaW5jZSBsaXN0cyBjYW4gYmUgbW9kaWZpZWQgaW4gcGxhY2UgdXNpbmcgaW5kZXggYXNzaWdubWVudHMsIHNsaWNlIGFzc2lnbm1lbnRzLCBvciBtZXRob2RzIGxpa2UgYXBwZW5kKCkgYW5kIGV4dGVuZCgpLlwiXX0sIHtcInRpdGxlXCI6IFwiMy4gRGF0YSBtb2RlbCBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvcmVmZXJlbmNlL2RhdGFtb2RlbC5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJUaGUgdmFsdWUgb2Ygc29tZSBvYmplY3RzIGNhbiBjaGFuZ2UuIE9iamVjdHMgd2hvc2UgdmFsdWUgY2FuIGNoYW5nZSBhcmUgc2FpZCB0byBiZSBtdXRhYmxlOyBvYmplY3RzIHdob3NlIHZhbHVlIGlzIHVuY2hhbmdlYWJsZSBvbmNlIHRoZXkgYXJlIGNyZWF0ZWQgYXJlIGNhbGxlZCBpbW11dGFibGUuIChUaGUgdmFsdWUgb2YgYW4gaW1tdXRhYmxlIGNvbnRhaW5lciBvYmplY3QgdGhhdCBjb250YWlucyBhIHJlZmVyZW5jZSB0byBhIG11dGFibGUgb2JqZWN0IGNhbiBjaGFuZ2Ugd2hlbiB0aGUgbGF0dGVyXFx1MjAxOXMgdmFsdWUgaXMgY2hhbmdlZDsgaG93ZXZlciB0aGUgY29udGFpbmVyIGlzIHN0aWxsIGNvbnNpZGVyZWQgaW1tdXRhYmxlLCBiZWNhdXNlIHRoZSBjb2xsZWN0aW9uIG9mIG9iamVjdHMgaXQgY29udGFpbnMgY2Fubm90IGJlIGNoYW5nZWQuIFNvLCBpbW11dGFiaWxpdHkgaXMgbm90IHN0cmljdGx5IHRoZSBzYW1lIGFzIGhhdmluZyBhbiB1bmNoYW5nZWFibGUgdmFsdWUsIGl0IGlzIG1vcmUgc3VidGxlLikgQW4gb2JqZWN0XFx1MjAxOXMgbXV0YWJpbGl0eSBpcyBkZXRlcm1pbmVkIGJ5IGl0cyB0eXBlOyBmb3IgaW5zdGFuY2UsIG51bWJlcnMsIHN0cmluZ3MgYW5kIHR1cGxlcyBhcmUgaW1tdXRhYmxlLCB3aGlsZSBkaWN0aW9uYXJpZXMgYW5kIGxpc3RzIGFyZSBtdXRhYmxlLlxcbi4uLlxcblNvbWUgb2JqZWN0cyBjb250YWluIHJlZmVyZW5jZXMgdG8gb3RoZXIgb2JqZWN0czsgdGhlc2UgYXJlIGNhbGxlZCBjb250YWluZXJzLiBFeGFtcGxlcyBvZiBjb250YWluZXJzIGFyZSB0dXBsZXMsIGxpc3RzIGFuZCBkaWN0aW9uYXJpZXMuIFRoZSByZWZlcmVuY2VzIGFyZSBwYXJ0IG9mIGEgY29udGFpbmVyXFx1MjAxOXMgdmFsdWUuIEluIG1vc3QgY2FzZXMsIHdoZW4gd2UgdGFsayBhYm91dCB0aGUgdmFsdWUgb2YgYSBjb250YWluZXIsIHdlIGltcGx5IHRoZSB2YWx1ZXMsIG5vdCB0aGUgaWRlbnRpdGllcyBvZiB0aGUgY29udGFpbmVkIG9iamVjdHM7IGhvd2V2ZXIsIHdoZW4gd2UgdGFsayBhYm91dCB0aGUgbXV0YWJpbGl0eSBvZiBhIGNvbnRhaW5lciwgb25seSB0aGUgaWRlbnRpdGllcyBvZiB0aGUgaW1tZWRpYXRlbHkgY29udGFpbmVkIG9iamVjdHMgYXJlIGltcGxpZWQuIFNvLCBpZiBhbiBpbW11dGFibGUgY29udGFpbmVyIChsaWtlIGEgdHVwbGUpIGNvbnRhaW5zIGEgcmVmZXJlbmNlIHRvIGEgbXV0YWJsZSBvYmplY3QsIGl0cyB2YWx1ZSBjaGFuZ2VzIGlmIHRoYXQgbXV0YWJsZSBvYmplY3QgaXMgY2hhbmdlZC5cXG4uLi5cXG4jIyMgMy4yLjUuIFNlcXVlbmNlc1xcdTAwYjZcXG4uLi5cXG4jIyMjIDMuMi41LjEuIEltbXV0YWJsZSBzZXF1ZW5jZXNcXHUwMGI2XFxuXFxuQW4gb2JqZWN0IG9mIGFuIGltbXV0YWJsZSBzZXF1ZW5jZSB0eXBlIGNhbm5vdCBjaGFuZ2Ugb25jZSBpdCBpcyBjcmVhdGVkLiAoSWYgdGhlIG9iamVjdCBjb250YWlucyByZWZlcmVuY2VzIHRvIG90aGVyIG9iamVjdHMsIHRoZXNlIG90aGVyIG9iamVjdHMgbWF5IGJlIG11dGFibGUgYW5kIG1heSBiZSBjaGFuZ2VkOyBob3dldmVyLCB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGRpcmVjdGx5IHJlZmVyZW5jZWQgYnkgYW4gaW1tdXRhYmxlIG9iamVjdCBjYW5ub3QgY2hhbmdlLilcXG5cXG5UaGUgZm9sbG93aW5nIHR5cGVzIGFyZSBpbW11dGFibGUgc2VxdWVuY2VzOlxcbi4uLlxcblR1cGxlc1xcbjogVGhlIGl0ZW1zIG9mIGEgYHR1cGxlYCBhcmUgYXJiaXRyYXJ5IFB5dGhvbiBvYmplY3RzLiBUdXBsZXMgb2YgdHdvIG9yIG1vcmUgaXRlbXMgYXJlIGZvcm1lZCBieSBjb21tYS1zZXBhcmF0ZWQgbGlzdHMgb2YgZXhwcmVzc2lvbnMuIEEgdHVwbGUgb2Ygb25lIGl0ZW0gKGEgXFx1MjAxOHNpbmdsZXRvblxcdTIwMTkpIGNhbiBiZSBmb3JtZWQgYnkgYWZmaXhpbmcgYSBjb21tYSB0byBhbiBleHByZXNzaW9uIChhbiBleHByZXNzaW9uIGJ5IGl0c2VsZiBkb2VzIG5vdCBjcmVhdGUgYSB0dXBsZSwgc2luY2UgcGFyZW50aGVzZXMgbXVzdCBiZSB1c2FibGUgZm9yIGdyb3VwaW5nIG9mIGV4cHJlc3Npb25zKS4gQW4gZW1wdHkgdHVwbGUgY2FuIGJlIGZvcm1lZCBieSBhbiBlbXB0eSBwYWlyIG9mIHBhcmVudGhlc2VzLlxcbi4uLlxcbiMjIyMgMy4yLjUuMi4gTXV0YWJsZSBzZXF1ZW5jZXNcXHUwMGI2XFxuXFxuTXV0YWJsZSBzZXF1ZW5jZXMgY2FuIGJlIGNoYW5nZWQgYWZ0ZXIgdGhleSBhcmUgY3JlYXRlZC4gVGhlIHN1YnNjcmlwdGlvbiBhbmQgc2xpY2luZyBub3RhdGlvbnMgY2FuIGJlIHVzZWQgYXMgdGhlIHRhcmdldCBvZiBhc3NpZ25tZW50IGFuZCBgZGVsYCAoZGVsZXRlKSBzdGF0ZW1lbnRzLlxcbi4uLlxcblRoZXJlIGFyZSBjdXJyZW50bHkgdHdvIGludHJpbnNpYyBtdXRhYmxlIHNlcXVlbmNlIHR5cGVzOlxcblxcbkxpc3RzXFxuOiBUaGUgaXRlbXMgb2YgYSBsaXN0IGFyZSBhcmJpdHJhcnkgUHl0aG9uIG9iamVjdHMuIExpc3RzIGFyZSBmb3JtZWQgYnkgcGxhY2luZyBhIGNvbW1hLXNlcGFyYXRlZCBsaXN0IG9mIGV4cHJlc3Npb25zIGluIHNxdWFyZSBicmFja2V0cy4gKE5vdGUgdGhhdCB0aGVyZSBhcmUgbm8gc3BlY2lhbCBjYXNlcyBuZWVkZWQgdG8gZm9ybSBsaXN0cyBvZiBsZW5ndGggMCBvciAxLilcIl19LCB7XCJ0aXRsZVwiOiBcIkJ1aWx0LWluIFR5cGVzIFxcdTIwMTQgUHl0aG9uIDMuMTQuNyBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9idWlsdGlucy9zdGR0eXBlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJjb2xsZWN0aW9uIGNsYXNzZXMgYXJlIG11dGFibGUuIFRoZSBtZXRob2RzIHRoYXQgYWRkLCBzdWJ0cmFjdCwgb3IgcmVhcnJhbmdlIHRoZWlyIC4uLiBpbiBwbGFjZSwgYW5kIGRvblxcdTIwMTl0IHJldHVybiBhIC4uLiAsIG5ldmVyIHJldHVybiB0aGUgY29sbGVjdGlvbiBpbnN0YW5jZSBpdHNlbGYgYnV0IGBOb25lYC5cXG4uLi5cXG4jIyBTZXF1ZW5jZSBUeXBlcyBcXHUyMDE0IGBsaXN0YCwgYHR1cGxlYCwgYHJhbmdlYFxcdTAwYjZcXG5cXG5UaGVyZSBhcmUgdGhyZWUgYmFzaWMgc2VxdWVuY2UgdHlwZXM6IGxpc3RzLCB0dXBsZXMsIGFuZCByYW5nZSBvYmplY3RzLiBBZGRpdGlvbmFsIHNlcXVlbmNlIHR5cGVzIHRhaWxvcmVkIGZvciBwcm9jZXNzaW5nIG9mIGJpbmFyeSBkYXRhIGFuZCB0ZXh0IHN0cmluZ3MgYXJlIGRlc2NyaWJlZCBpbiBkZWRpY2F0ZWQgc2VjdGlvbnMuXFxuLi4uXFxuVGhlIG9wZXJhdGlvbnMgaW4gdGhlIGZvbGxvd2luZyB0YWJsZSAuLi4gbXV0YWJsZSBhbmQgaW1tdXRhYmxlLiBUaGUgYGNvbGxlY3Rpb25zLiAuLi4gaXMgcHJvdmlkZWQgdG8gbWFrZSAuLi4gZWFzaWVyIHRvIGNvcnJlY3RseSBpbXBsZW1lbnQgdGhlc2Ugb3BlcmF0aW9ucyBvbiBjdXN0b20gc2VxdWVuY2UgdHlwZXNcXG4uLi5cXG4jIyMgSW1tdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbm11dGFibGUgc2VxdWVuY2UgdHlwZXMgaXNcXG4uLi5cXG5zdXBwb3J0IGFsbG93cyBpbW11dGFibGUgc2VxdWVuY2VzLCAuLi4gLCB0byBiZSB1c2VkIGFzIGBkaWN0YCBrZXlzIGFuZCBzdG9yZWQgaW4gLi4uIGVuc2V0YCBpbnN0YW5jZXNcXG4uLi5cXG4jIyMgTXV0YWJsZSBTZXF1ZW5jZSBUeXBlc1xcdTAwYjZcXG4uLi5cXG4jIyMgTGlzdHNcXHUwMGI2XFxuXFxuTGlzdHMgYXJlIG11dGFibGUgc2VxdWVuY2VzLCB0eXBpY2FsbHkgdXNlZCB0byBzdG9yZSBjb2xsZWN0aW9ucyBvZiBob21vZ2VuZW91cyBpdGVtcyAod2hlcmUgdGhlIHByZWNpc2UgZGVncmVlIG9mIHNpbWlsYXJpdHkgd2lsbCB2YXJ5IGJ5IGFwcGxpY2F0aW9uKS5cXG4uLi5cXG4jIyMgVHVwbGVzXFx1MDBiNlxcblxcblR1cGxlcyBhcmUgaW1tdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaGV0ZXJvZ2VuZW91cyBkYXRhIChzdWNoIGFzIHRoZSAyLXR1cGxlcyBwcm9kdWNlZCBieSB0aGUgYGVudW1lcmF0ZSgpYCBidWlsdC1pbikuIFR1cGxlcyBhcmUgYWxzbyB1c2VkIGZvciBjYXNlcyB3aGVyZSBhbiBpbW11dGFibGUgc2VxdWVuY2Ugb2YgaG9tb2dlbmVvdXMgZGF0YSBpcyBuZWVkZWQgKHN1Y2ggYXMgYWxsb3dpbmcgc3RvcmFnZSBpbiBhIGBzZXRgIG9yIGBkaWN0YCBpbnN0YW5jZSkuXFxuLi4uXFxudHVwbGUoaXRlcmFibGU9XFxuLi4uXFxuVGhlIGNvbnN0cnVjdG9yIGJ1aWxkcyBhIHR1cGxlIHdob3NlIGl0ZW1zIGFyZSB0aGUgc2FtZSBhbmQgaW4gdGhlIHNhbWUgb3JkZXIgYXMgaXRlcmFibGVcXHUyMDE5cyBpdGVtcy4gaXRlcmFibGUgbWF5IGJlIGVpdGhlciBhIHNlcXVlbmNlLCBhIGNvbnRhaW5lciB0aGF0IHN1cHBvcnRzIGl0ZXJhdGlvbiwgb3IgYW4gaXRlcmF0b3Igb2JqZWN0LiBJZiBpdGVyYWJsZSBpcyBhbHJlYWR5IGEgdHVwbGUsIGl0IGlzIHJldHVybmVkIHVuY2hhbmdlZC4gRm9yIGV4YW1wbGUsIGB0dXBsZSgnYWJjJylgIHJldHVybnMgYCgnYScsICdiJywgJ2MnKWAgYW5kIGB0dXBsZSggWzEsIDIsIDNdIClgIHJldHVybnMgYCgxLCAyLCAzKWAuIElmIG5vIGFyZ3VtZW50IGlzIGdpdmVuLCB0aGUgY29uc3RydWN0b3IgY3JlYXRlcyBhIG5ldyBlbXB0eSB0dXBsZSwgYCgpYC5cXG4uLi5cXG5UdXBsZXMgaW1wbGVtZW50IGFsbCBvZiB0aGUgY29tbW9uIHNlcXVlbmNlIG9wZXJhdGlvbnMuXFxuLi4uXFxuRm9yIGhldGVyb2dlbmVvdXMgY29sbGVjdGlvbnMgb2YgZGF0YSB3aGVyZSBhY2Nlc3MgYnkgbmFtZSBpcyBjbGVhcmVyIHRoYW4gYWNjZXNzIGJ5IGluZGV4LCBgY29sbGVjdGlvbnMubmFtZWR0dXBsZSgpYCBtYXkgYmUgYSBtb3JlIGFwcHJvcHJpYXRlIGNob2ljZSB0aGFuIGEgc2ltcGxlIHR1cGxlIG9iamVjdC5cIl19LCB7XCJ0aXRsZVwiOiBcIkJ1aWx0LWluIFR5cGVzIFxcdTIwMTQgUHl0aG9uIDMuMTQuNSBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9saWJyYXJ5L3N0ZHR5cGVzLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcIlNvbWUgY29sbGVjdGlvbiBjbGFzc2VzIGFyZSBtdXRhYmxlLiBUaGUgbWV0aG9kcyB0aGF0IGFkZCwgc3VidHJhY3QsIG9yIHJlYXJyYW5nZSB0aGVpciAuLi4gaW4gcGxhY2UsIGFuZCBkb25cXHUyMDE5dCByZXR1cm4gYSAuLi4gLCBuZXZlciByZXR1cm4gdGhlIGNvbGxlY3Rpb24gaW5zdGFuY2UgaXRzZWxmIGJ1dCBgTm9uZWAuXFxuLi4uXFxuIyMgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBgbGlzdGAsIGB0dXBsZWAsIGByYW5nZWBcXHUwMGI2XFxuXFxuVGhlcmUgYXJlIHRocmVlIGJhc2ljIHNlcXVlbmNlIHR5cGVzOiBsaXN0cywgdHVwbGVzLCBhbmQgLi4uIG9iamVjdHMuIEFkZGl0aW9uYWwgc2VxdWVuY2UgdHlwZXMgdGFpbG9yZWQgZm9yIHByb2Nlc3Npbmcgb2YgYmluYXJ5IGRhdGEgYW5kIHRleHQgc3RyaW5ncyBhcmUgZGVzY3JpYmVkIGluIGRlZGljYXRlZCBzZWN0aW9ucy5cXG4uLi5cXG5UaGUgb3BlcmF0aW9ucyBpbiB0aGUgZm9sbG93aW5nIHRhYmxlIC4uLiBpbW11dGFibGUuIFRoZSBgIC4uLiBpcyBwcm92aWRlZCB0byBtYWtlIC4uLiBlYXNpZXIgdG8gY29ycmVjdGx5IGltcGxlbWVudCB0aGVzZSBvcGVyYXRpb25zIG9uIGN1c3RvbSBzZXF1ZW5jZSB0eXBlc1xcbi4uLlxcbiMjIyBJbW11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxub3BlcmF0aW9uIHRoYXQgaW1tdXRhYmxlIHNlcXVlbmNlIC4uLiBieSBtdXRhYmxlIHNlcXVlbmNlIHR5cGVzIGlzIC4uLiBgaGFzaCgpYFxcbi4uLlxcblRoaXMgc3VwcG9ydCBhbGxvd3MgaW1tdXRhYmxlIHNlcXVlbmNlcywgc3VjaCBhcyBgdHVwbGVgIGluc3RhbmNlcywgdG8gYmUgdXNlZCBhcyBgZGljdGAga2V5cyBhbmQgc3RvcmVkIGluIGBzZXRgIGFuZCBgZnJvemVuc2V0YCBpbnN0YW5jZXMuXFxuLi4uXFxuIyMjIE11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxuIyMjIExpc3RzXFx1MDBiNlxcblxcbkxpc3RzIGFyZSBtdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaG9tb2dlbmVvdXMgaXRlbXMgKHdoZXJlIHRoZSBwcmVjaXNlIGRlZ3JlZSBvZiBzaW1pbGFyaXR5IHdpbGwgdmFyeSBieSBhcHBsaWNhdGlvbikuXFxuLi4uXFxuIyMjIFR1cGxlc1xcdTAwYjZcXG5cXG5UdXBsZXMgYXJlIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhldGVyb2dlbmVvdXMgZGF0YSAoc3VjaCBhcyB0aGUgMi10dXBsZXMgcHJvZHVjZWQgYnkgdGhlIGBlbnVtZXJhdGUoKWAgYnVpbHQtaW4pLiBUdXBsZXMgYXJlIGFsc28gdXNlZCBmb3IgY2FzZXMgd2hlcmUgYW4gaW1tdXRhYmxlIHNlcXVlbmNlIG9mIGhvbW9nZW5lb3VzIGRhdGEgaXMgbmVlZGVkIChzdWNoIGFzIGFsbG93aW5nIHN0b3JhZ2UgaW4gYSBgc2V0YCBvciBgZGljdGAgaW5zdGFuY2UpLlxcbi4uLlxcblRoZSBjb25zdHJ1Y3RvciBidWlsZHMgYSB0dXBsZSB3aG9zZSBpdGVtcyBhcmUgdGhlIHNhbWUgYW5kIGluIHRoZSBzYW1lIG9yZGVyIGFzIGl0ZXJhYmxlXFx1MjAxOXMgaXRlbXMuIGl0ZXJhYmxlIG1heSBiZSBlaXRoZXIgYSBzZXF1ZW5jZSwgYSBjb250YWluZXIgdGhhdCBzdXBwb3J0cyBpdGVyYXRpb24sIG9yIGFuIGl0ZXJhdG9yIG9iamVjdC4gSWYgaXRlcmFibGUgaXMgYWxyZWFkeSBhIHR1cGxlLCBpdCBpcyByZXR1cm5lZCB1bmNoYW5nZWQuIEZvciBleGFtcGxlLCBgdHVwbGUoJ2FiYycpYCByZXR1cm5zIGAoJ2EnLCAnYicsICdjJylgIGFuZCBgdHVwbGUoIFsxLCAyLCAzXSApYCByZXR1cm5zIGAoMSwgMiwgMylgLiBJZiBubyBhcmd1bWVudCBpcyBnaXZlbiwgdGhlIGNvbnN0cnVjdG9yIGNyZWF0ZXMgYSBuZXcgZW1wdHkgdHVwbGUsIGAoKWAuXFxuLi4uXFxuVHVwbGVzIGltcGxlbWVudCBhbGwgb2YgdGhlIGNvbW1vbiBzZXF1ZW5jZSBvcGVyYXRpb25zLlxcbi4uLlxcbkZvciBoZXRlcm9nZW5lb3VzIGNvbGxlY3Rpb25zIG9mIGRhdGEgd2hlcmUgYWNjZXNzIGJ5IG5hbWUgaXMgY2xlYXJlciB0aGFuIGFjY2VzcyBieSBpbmRleCwgYGNvbGxlY3Rpb25zLm5hbWVkdHVwbGUoKWAgbWF5IGJlIGEgbW9yZSBhcHByb3ByaWF0ZSBjaG9pY2UgdGhhbiBhIHNpbXBsZSB0dXBsZSBvYmplY3QuXCJdfSwge1widGl0bGVcIjogXCI1LiBEYXRhIFN0cnVjdHVyZXMgXFx1MjAxNCBQeXRob24gMy4xMC4yMCBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy4xMC90dXRvcmlhbC9kYXRhc3RydWN0dXJlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJZb3UgbWlnaHQgaGF2ZSBub3RpY2VkIHRoYXQgbWV0aG9kcyBsaWtlIGBpbnNlcnRgLCBgcmVtb3ZlYCBvciBgc29ydGAgdGhhdCBvbmx5IG1vZGlmeSB0aGUgbGlzdCBoYXZlIG5vIHJldHVybiB2YWx1ZSBwcmludGVkIFxcdTIwMTMgdGhleSByZXR1cm4gdGhlIGRlZmF1bHQgYE5vbmVgLiAxIFRoaXMgaXMgYSBkZXNpZ24gcHJpbmNpcGxlIGZvciBhbGwgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgaW4gUHl0aG9uLlxcbi4uLlxcbiMjIDUuMy4gVHVwbGVzIGFuZCBTZXF1ZW5jZXNcXHUwMGI2XFxuXFxuV2Ugc2F3IHRoYXQgbGlzdHMgYW5kIHN0cmluZ3MgaGF2ZSBtYW55IGNvbW1vbiBwcm9wZXJ0aWVzLCBzdWNoIGFzIGluZGV4aW5nIGFuZCBzbGljaW5nIG9wZXJhdGlvbnMuIFRoZXkgYXJlIHR3byBleGFtcGxlcyBvZiBzZXF1ZW5jZSBkYXRhIHR5cGVzIChzZWUgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBsaXN0LCB0dXBsZSwgcmFuZ2UpLiBTaW5jZSBQeXRob24gaXMgYW4gZXZvbHZpbmcgbGFuZ3VhZ2UsIG90aGVyIHNlcXVlbmNlIGRhdGEgdHlwZXMgbWF5IGJlIGFkZGVkLiBUaGVyZSBpcyBhbHNvIGFub3RoZXIgc3RhbmRhcmQgc2VxdWVuY2UgZGF0YSB0eXBlOiB0aGUgdHVwbGUuXFxuXFxuQSB0dXBsZSBjb25zaXN0cyBvZiBhIG51bWJlciBvZiB2YWx1ZXMgc2VwYXJhdGVkIGJ5IGNvbW1hcywgZm9yIGluc3RhbmNlOlxcbi4uLlxcbj4+PiAjIFR1cGxlcyBhcmUgaW1tdXRhYmxlOlxcbi4uLiB0WzBdID0gODg4ODhcXG5UcmFjZWJhY2sgKG1vc3QgcmVjZW50IGNhbGwgbGFzdCk6XFxuICBGaWxlIFxcXCI8c3RkaW4+XFxcIiwgbGluZSAxLCBpbiA8bW9kdWxlPlxcblR5cGVFcnJvcjogJ3R1cGxlJyBvYmplY3QgZG9lcyBub3Qgc3VwcG9ydCBpdGVtIGFzc2lnbm1lbnRcXG4+Pj4gIyBidXQgdGhleSBjYW4gY29udGFpbiBtdXRhYmxlIG9iamVjdHM6XFxuLi4uIHYgPSAoWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuPj4+IHZcXG4oWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuLi4uXFxuQXMgeW91IHNlZSwgb24gb3V0cHV0IHR1cGxlcyBhcmUgYWx3YXlzIGVuY2xvc2VkIGluIHBhcmVudGhlc2VzLCBzbyB0aGF0IG5lc3RlZCB0dXBsZXMgYXJlIGludGVycHJldGVkIGNvcnJlY3RseTsgdGhleSBtYXkgYmUgaW5wdXQgd2l0aCBvciB3aXRob3V0IHN1cnJvdW5kaW5nIHBhcmVudGhlc2VzLCBhbHRob3VnaCBvZnRlbiBwYXJlbnRoZXNlcyBhcmUgbmVjZXNzYXJ5IGFueXdheSAoaWYgdGhlIHR1cGxlIGlzIHBhcnQgb2YgYSBsYXJnZXIgZXhwcmVzc2lvbikuIEl0IGlzIG5vdCBwb3NzaWJsZSB0byBhc3NpZ24gdG8gdGhlIGluZGl2aWR1YWwgaXRlbXMgb2YgYSB0dXBsZSwgaG93ZXZlciBpdCBpcyBwb3NzaWJsZSB0byBjcmVhdGUgdHVwbGVzIHdoaWNoIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzLCBzdWNoIGFzIGxpc3RzLlxcblxcblRob3VnaCB0dXBsZXMgbWF5IHNlZW0gc2ltaWxhciB0byBsaXN0cywgdGhleSBhcmUgb2Z0ZW4gdXNlZCBpbiBkaWZmZXJlbnQgc2l0dWF0aW9ucyBhbmQgZm9yIGRpZmZlcmVudCBwdXJwb3Nlcy4gVHVwbGVzIGFyZSBpbW11dGFibGUsIGFuZCB1c3VhbGx5IGNvbnRhaW4gYSBoZXRlcm9nZW5lb3VzIHNlcXVlbmNlIG9mIGVsZW1lbnRzIHRoYXQgYXJlIGFjY2Vzc2VkIHZpYSB1bnBhY2tpbmcgKHNlZSBsYXRlciBpbiB0aGlzIHNlY3Rpb24pIG9yIGluZGV4aW5nIChvciBldmVuIGJ5IGF0dHJpYnV0ZSBpbiB0aGUgY2FzZSBvZiBgbmFtZWR0dXBsZXNgKS4gTGlzdHMgYXJlIG11dGFibGUsIGFuZCB0aGVpciBlbGVtZW50cyBhcmUgdXN1YWxseSBob21vZ2VuZW91cyBhbmQgYXJlIGFjY2Vzc2VkIGJ5IGl0ZXJhdGluZyBvdmVyIHRoZSBsaXN0Llxcbi4uLlxcbiMjIDUuNC5cXG4uLi5cXG4uIFNldCBvYmplY3RzXFxuLi4uXFxuIyMgNS41LiBEaWN0aW9uYXJpZXNcXHUwMGI2XFxuXFxuQW5vdGhlciB1c2VmdWwgZGF0YSB0eXBlIGJ1aWx0IGludG8gUHl0aG9uIGlzIHRoZSBkaWN0aW9uYXJ5IChzZWUgTWFwcGluZyBUeXBlcyBcXHUyMDE0IGRpY3QpLiBEaWN0aW9uYXJpZXMgYXJlIHNvbWV0aW1lcyBmb3VuZCBpbiBvdGhlciBsYW5ndWFnZXMgYXMgXFx1MjAxY2Fzc29jaWF0aXZlIG1lbW9yaWVzXFx1MjAxZCBvciBcXHUyMDFjYXNzb2NpYXRpdmUgYXJyYXlzXFx1MjAxZC4gVW5saWtlIHNlcXVlbmNlcywgd2hpY2ggYXJlIGluZGV4ZWQgYnkgYSByYW5nZSBvZiBudW1iZXJzLCBkaWN0aW9uYXJpZXMgYXJlIGluZGV4ZWQgYnkga2V5cywgd2hpY2ggY2FuIGJlIGFueSBpbW11dGFibGUgdHlwZTsgc3RyaW5ncyBhbmQgbnVtYmVycyBjYW4gYWx3YXlzIGJlIGtleXMuIFR1cGxlcyBjYW4gYmUgdXNlZCBhcyBrZXlzIGlmIHRoZXkgY29udGFpbiBvbmx5IHN0cmluZ3MsIG51bWJlcnMsIG9yIHR1cGxlczsgaWYgYSB0dXBsZSBjb250YWlucyBhbnkgbXV0YWJsZSBvYmplY3QgZWl0aGVyIGRpcmVjdGx5IG9yIGluZGlyZWN0bHksIGl0IGNhbm5vdCBiZSB1c2VkIGFzIGEga2V5LiBZb3UgY2FuXFx1MjAxOXQgdXNlIGxpc3RzIGFzIGtleXMsIHNpbmNlIGxpc3RzIGNhbiBiZSBtb2RpZmllZCBpbiBwbGFjZSB1c2luZyBpbmRleCBhc3NpZ25tZW50cywgc2xpY2UgYXNzaWdubWVudHMsIG9yIG1ldGhvZHMgbGlrZSBgYXBwZW5kKClgIGFuZCBgZXh0ZW5kKClgLlwiXX1dIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJ0b29sIiwibmFtZSI6IndlYl9zZWFyY2giLCJpZCI6IjZlNTEyNWFjLTMxNjQtNGE3Ni05YzUyLWFjMzQ2MGNlMzZlNCIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsInN0YXR1cyI6InN1Y2Nlc3MifSx7ImNvbnRlbnQiOlt7ImlkIjoidG9vbHVfMDFYamozYVJkV3loQkpmUG1USkJ4UWE5IiwiY2FsbGVyIjp7InR5cGUiOiJkaXJlY3QifSwiaW5wdXQiOnt9LCJuYW1lIjoidHJhbnNmZXJfdG9fc2tlcHRpYyIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHdBNkY0Y0NONXloRDRDcnM5TSIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo3Mzc4LCJvdXRwdXRfdG9rZW5zIjozMywib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6MH0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoicmVzZWFyY2hlciIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy0zY2Q4LTc5ZjEtODhhMS1jZTZjZWUyOTRlNzktMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ0cmFuc2Zlcl90b19za2VwdGljIiwiYXJncyI6e30sImlkIjoidG9vbHVfMDFYamozYVJkV3loQkpmUG1USkJ4UWE5IiwidHlwZSI6InRvb2xfY2FsbCJ9XSwiaW52YWxpZF90b29sX2NhbGxzIjpbXSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjo3Mzc4LCJvdXRwdXRfdG9rZW5zIjozMywidG90YWxfdG9rZW5zIjo3NDExLCJpbnB1dF90b2tlbl9kZXRhaWxzIjp7ImNhY2hlX3JlYWQiOjAsImNhY2hlX2NyZWF0aW9uIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VuX2RldGFpbHMiOnsicmVhc29uaW5nIjowfX19LHsiY29udGVudCI6IlN1Y2Nlc3NmdWxseSB0cmFuc2ZlcnJlZCB0byBza2VwdGljIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJ0b29sIiwibmFtZSI6InRyYW5zZmVyX3RvX3NrZXB0aWMiLCJpZCI6IjM4OTA1OGQxLTA4M2YtNDYzMi1iYmM1LTA5YmZkM2EwZmZkMiIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsInN0YXR1cyI6InN1Y2Nlc3MifSx7ImNvbnRlbnQiOlt7InNpZ25hdHVyZSI6IkNBUVMxUWdLRUFnU0dBSTRBVUlJZEdocGJtdHBibWNTRE42VGZVNk5ETTk3MWxFYnp4b012UlVBMUo5b3gzZkpRWVQ5SWpBckNRUUdIQlA0cFJIUGF2WHpSbGJNYmFZSEM1N1lqK0dieGdyMGhlbHZkMk9JTC9vNlJidDRQRkEzRmFJRlVsZ3E4Z2M3S1FJOUZ0R252U0FrTEJSeklzVXAvMmQ1UmQyemdFc2ZrRlZlWHNuOFdZWVpraW5BUVZWeTZBN0IzTGFFSlcweGIveDg4RGk2N2ZlZHd4MFErRmxadHdjbVQzRGNhYTExSnZleUhScjNkbSt6R1NaUDZDWGVYTko4bnJUWjdXMVlSVHlzRDZ3N283THA4aDFSd3E0ZjRvOGorUVVmWTB2amUrekpac3hIUG1RTnQzeWtlS2g5OWhnbjZkYml6Ni9FSHFtYkhqU0F1MU00U3FFRGZxSUtUaEhZZW5hbUFabWNRNFhBT282bWRRNis3N1ZncFYxbEMxL2s1MklpVW9KamtHbFFybkxDdnV0aUk3S04rc2dLOGMyKzJLQ0pyUFJDdlZWK0JrU3R2UDVqV1dxenl0cEtRK25IemhMNDQ4T3JJQTlnZEpDMzlDOGVEclhlUm5JN3Q0RnFsa1hHb01sWkFWOVVNY3JRSFJ4T01pOHRPeVR6aTlveUJGRFRTc1ErTEpxZXIwdHoxRnQ5Z2VrNnZ6ZzUwU3BWQUZRZExFUnY4UjFZcU1hZUkreHN2VEFFVzlNUmd2Uk1rekx1elJUMmd1dzNqRCtGamlwMzd4Z0s1UUkwUitDc09BbVFQb1RaaXp6Y0l5QVVBdUNKRkNncS9nRC9DaWJMeG95bWlWVUkxYW45U250M0twTlJvV2R2V0luZTMzVjBwdjhQSGc5eVcySnVMT3FRR2lsTENzSy9RM29RcjN1cEVNWHhYR2pDTDdXVWl6dlJaR0ZFRVYrVnBsWXQ3djR0S1pNajVwazN2ZGVNME1KNVhaZzdQdlVzUnp5Y3hjbnJUM0tub2MzcU0rdHRxWmZUc2I4ZDZVL3BRaWZxN0d1YWhGT0hydnJvcWdKTGorLzgxWXkxcFpGa2pmSmdZMFhrbHpBbkE2Q0ZxL1lUMGd4YlYrVEg1SVMrV2FZT1V6SzM2UzhzNXFxTXdobmd2NEl6RTNjdFhYcW5oSVpYTnpueVZCcWJZYS9HeDJiQ2pjRWoybjVSeUdZUlMvK01jdWxnTEJ6blE5TWtHMkQ3UVAzTXEvbndIWEtNSWFMUzhWSzhoRzNkMHJBamx2bmF1WFNoTnJKeHRrajRxa3dueEQ0THJFamsxdWUrUExjYkxxWVd3QXI4bjBzKzQxanJzSVBWeXlLOXZGYUVackE5S2FCeGtKeWlxeDUwR1pObjdja3grbWtJMThnSDUyaXlWUHRVOEZLTVhqdWdDbDZtUExpSFlpRFFkK0c2M0x1ZG1PS3lzOWl0Vm5CREJOTVAyYi9JNytiSVZSbGhlR2FaV3I2TUFUS2w4NkIzeSswa3VPckxSTXlXY2RpYmMyaVFrWm0rOGY3ZFRVd2JLYkJKc0tPRlk3My84bmxML0ppWXBqejZuVUlrK0lxU2phRnlLNlhGbENjdWtXcFdIcERKblN2YkV6ZXk3cUh5Q3FKK0krb244aVA4R090ZW1wUWFTMC80a1pHU3dsckU2eE9qNkV3R3BmamJEWktIa1AvMlJBZE5ZUFFxM3o3YmRWSk1KTm5GTXo2ZTM3bXpNcENCaWh3UGRTRlRSRTNTQ1k2Nnd2VGdKekdDVXloSGJTaTVpMWF6ek9YZDh1SENTcFQvNkwxbzZWVTBwam9SOEthYTR3Q1dIR2Y3UC9XRWQrYXZ5cDAySm51MWNtcFcwK0dxK3ZLOWgwdGRiMklDbE0vZU5mZG91UVArZXA4TUpVanJhRFJJQ21BSlFZNW9iUzcxdm5pT28rcmVhRnkwelJnQiIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJzaWduYXR1cmUiOiJDQVFTL1JFS0VRZ1NHQUk0QVVJSmJtRnljbUYwYVc5dUVnd0ZoM0FWSlpQM3M4eTFIcTBhREc3NnVoSE9OYVNjWEFHU2hpSXdHZ2pMWEhVeDF5WnJrTFBsamp6VE9ReWNwTEppS1Z3WGN4bS9FektKbTFzOE01V3h1UkVXRWxkWGNvYU4wL21zS3BrUk1UK2VMYjVpaDlyQUhVTFRvbVVOc1V3SmV1MGpySmU0R0xhenk3c1A1S3RqRU9FQktRc1hVZm9RN2ZEa1pPUVVIWmk5enpRTjRoRk5tWUpVZjNPVnFmOVRRTmRDMSttLy9IQVpQZldtQXk2SHREdm03a2dCSmtDRW9pRm5tN3RuL1BYMWhYR1BSZ1djNkJUWFVNcjJXQThhS3lxZ2dSVWJNSDIrSll2NWlXWmRkQ2MxdXAxTmJyM3NGN1dQY290OVpCaHlIUVhyNjkyemVKazZTSmd5Z0lWckFTYk1seTJiQnhUV0pCNjIzZ1ZnenBDbVZTTFpGcGtLWGdQY3A4aEd5REtPdkY2U2lRU0hxR1J4UW9mcjdWczFhUGhmWml4Uk9KR0QrTWxya2dnVkFuR015WUlEVVpwYXZ0Q2VpVTFKbUJTejAwODRiWFB0VC90bnBzS09uU1BZWlFvM0IwQlhLYnFBL1dzS25nOC9WM1o4VXVham82SVZaMmhnQTFXRGR1cVFPUWtFeEdVNnBGdUxoZWJJanVXeFNOSS9EZmZ2Ui8vTE5xQ2Z2ZG5mMGh2b1B0RHhmeC9TYXdJeDQvc3BxOXhid0Z6SHRMZ29IZzFGMndGdzdjbFBEbGcxUHlEL3YyeUpHM1Y5VDhHTDZqR1BFNnJoZmlsL3U0SE52ZjdCVzBkUXpDVFRYY3FaeTRuQnl5MCtwK1M3WnhmTWw0S3B4YTJnT2hoU05qZnBmdVgwUXBMNGtNRThKMW5saUZOMWQ4S3FFUHVESTE4QlFIVWhEL1VmWTI5b0EweDUxMEZqTS9Cc0xaNG41c3Bwa29JaTdDRm9RcTJhQUtWTmZDMUtpbGhJcFJEKzFkeUR0WlFrbE44Z1pwT0VTSHc2TUwxVHRVMHlmQWNibXFveEo2aGhqendINkNNc1RRRUV5OE1iUkJNK1FLS2tJUmRSQ1BIR0ZXMkxrdVI1MCtKMHBGL3RyNnBxVnZKZW96ZmlNbnJFaWxCMjdUWlA0bktUSkc4cmQvL3ZDV2VuMHk3Q0EvOGRCSjlnZHA1dHg3TlZBNDVrMWdkY3hFUjFnWW91TlgzbjF2RzMvQjFGNCs5M3dYVzlNcW82VXM1OVRRbXJiOU5xYTdwU1FOU3JIMDZ3MUd4eUJBbGtGZnFBbGFmazE2YU1wcTQxNlRKWXBHeTQzV0NVaUNjTFdsaGlXZ3oyc2dPTEFYUDlhNXdlV0FLUkM4SjZqa2NVa0RlWG5XK0hGU2lQTTYrOUJvNmtWaExVaE41cmM0bUhaT1pwRjdwdFBwc1FONFhDcUxQc3FwUkgvOG81WHB5QzZabmVUZUYyRXh0amh3WlZqWER3TWJJelZZSWJ3MXkyUFBHdzhkQzdwYzRzSVZTbkk0b1FBSDB2NjUwZ2hmaG1NZXZXVzRoNFB5bGJ3L0RHNGZkZVQzKzh6MGY4TU9TVHRmaXB4MDUvUXR0SGxBdk9EUnhhNXlEQ3ZPejZuM2YxQk8wTi80TWJHeU9NMjdxVzR4OG1CVm9yeVVaV2ZzbWhmaTAvR2ZOdkI4K3lsVEdiWU5YMjdPY0VGNVdadE5xYldFeTdWNUdibVpVU3hzcGNnN0ZCWjNqMy8zTDdtdXZaWG9aaTFmNWVycTR5M2xTSCtvRjdxUEZudmlleUxPQ3d1YmNReStveW93ajRjRDBpZFJlY25wK3MrNTJENyttekYxZ3BXK2YrY0phc294aWdieGtPbG9TNHllaUt2Z000bldTcEdFajEyUlB4NW84MEVtemlYTjl0NHpKelcxaHRBNWhYSkplRnltcE1ZdHlDVUNldlJXVm1yUllrdFE3VW5DYTZ0TC9BVlBpMW5pNVhDWEhmTE91cWZLNUtMWXhqd1Z0ZGhoZm1DTHZwYUtWTkNFT0NmNG5OR0hxRDk4d1ViMGVqV053RFlCbkpHa0JSdzlSVnNwZndOZG9Xb1ZjbjJxdjh5d0dVcDd5Yy9zazJMcEk2VnppUnl2V3FiR05HMVpKcVZWdXRSYzQ2K3NPRTRWU2VTMi9DVE5LTmE2Tk1DZi9oeHhhVVZzWWZLZFArUzFMYzRpYW5LcDBzVXVnUHBtdG95ZEMwQ1AyUndqbkhMY256eENudnF4bExRV1hNTlBUc3UyeUJjSGxXSkx0Y21kZy9NY0xhZ3k3bDVrRzZzTjdaeEpMRC9rQXg4RTZ1cW9oUCtLL2pNU3JZdVFKbmhyNHRmNGJpV0ZZM0p6Zi9LcEFZeVZTcStwYmZGS1hNYkw5bkhFb1pRVDE1V3lhYmQ2RE1BcTNRWk90aE01cFV6ZE5xVVhrY3JJMVB3akFTQWc0ZFZVLzJvKzk4bVRYbnloUDdWbEVrVG5kZHB3eUpGK1lDQ2MwMXBxYTZaWDk5cG5SK1lPcElyYjVEdFBjS3EwdDVjRkNEeVF4TjFsVXVxWmNZdWp2WC9HQzcrNklwUlR6L05rRVkvNWNudE9mWkp5ZDZTa1V4L0pSNUFKSVJQQ05laWIwcmNXbm5GbjBYSzhzMjJ4NDB3TGpFdHVmWERPOSt5cmFmNGtIeGxCSTNTQjBtZU1qZXBYNTBDSmhBeDdOZWtrUkZud29lY0xmcUl5cmpYcmdBWnNUTVBpWDk0c25qZHJ6aXNBWHRCS2FudWxpbFN4T2ZXMkhTbWZuazBMMkdYSEpKM1MrdkNiZ1JFbWw5ZGVrZm1IaG13RVl0djNrZmh0aDdFUkl3VlQ4ajY5T3RJQkNtZVRJSnVBN283M1BYYzFMSjJ3aHVaSzdGWmU2TlRPczYxTVA3SlZsSWM2dGJodE9CazNOWFU4cEJQT0NZNlE1NWQ2M0Z0cFVVL0tDYlEwSzJXUTlvNWpOR3dOTko2Q3c1RkRDdCt4NVlKZVAxZDJRekRTaXErY1NacXdxQ1pWY05vQWZ0R0d6Z3JSWjR2a3BiUDB4UVpIQ3RXc1V3SFZUanJZNE5mUkRqSGRWUFJuOEo5WHRvb2MvaElXa3F6RmFjellUbGFwdTJSQ0pxZThhRW8zanpXWkNFSjdBaUJ6QllyYVlEZ09mZWlReWVKVXNiYlhIZEdLaWl3MjJCZHA0TStZejY3MU1aMGllOUVXUmtzMFEyREhHVVRuZTlTNHRBQUpQR2JVZjR4dENNb0o0MFE0UVcwNVFMU1p5M3dORkFVRlltWEU0RUNERkI4K0lFZU5GRkYvdGdBbmZoOEd2RmgvMXhZK0xFK296TW1pbmpTUndocTA5elFBRWtORnl1MElLZ3ZEa3lPSStpY2NGN2RvV0xUVWFzUHhMSDF4R1BCU0trZVdDblNvWC80QzJjOWR1NjZCYlJYYW1IcWFlK1ZDaVM2Qk45NTNpWUtPdThyTWtuZVcrVEZqVjBDekFmOFBORzlMQmF5VWdZNVV3OXgwU2FwNnpJNFBwYVpLSDRkOWt0a0ZVKzkxeUJiTjVTUnRkZDgvRVFhdEpYbUJOZnc3a1JybGwrTGp3WndlV0xFYjBqVDFScEd1RHFITGRMVE1NZ293U2N6cnAyekNzSmRRSC9zZWFZcnB3a0NGUjhkUmpNN0pSVWtWYTFBMFN2ZFpBWDc1dnVCdnZMQnpOMmMrUmJ0NW9xeTkzbHFJQklqS2I3dzJVa1JUYjRwQldjTktOUFVndDNhei9QeFF4RWFzYWx0ZG11eXZzWFlidXIzVlBObjhKek4vNko1TitRQ256NTcxVGNjYXBDbWtQdkZFK2dheVhVK1VGMVZLWXlGY1V2bGV2N2xzUWxtanF6SzJRQThYN1EzVHpGYXBxaHZKS2x0N2R6QkhyYXFaQmdQZ2I5TjRvS05heWk4RlpUNzEra2FZaEloRnJFU0tZUkhmSGtqNW4za2ZoZ0x5K3grakxEYkt4TWdna2dIbkp6OVJ1RUhaWEJXdGtZQVE9PSIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJpZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3ZlcmlmaWVyIiwidHlwZSI6InRvb2xfdXNlIiwidG9vbHNldF9uYW1lIjpudWxsfV0sImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2Zkd0FNUmRTMjFGZUpmelFoTTNzIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjczOTYsIm91dHB1dF90b2tlbnMiOjc3MCwib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6NzM5fSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCIsIm1vZGVsX3Byb3ZpZGVyIjoiYW50aHJvcGljIn0sInR5cGUiOiJhaSIsIm5hbWUiOiJza2VwdGljIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTRhZTktNzZjMC04OWE2LTRmMGIxMTRlMzAzMC0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6InRyYW5zZmVyX3RvX3ZlcmlmaWVyIiwiYXJncyI6e30sImlkIjoidG9vbHVfMDFINmZWOEp2UkpOdlh5NDdlUm5kM2pIIiwidHlwZSI6InRvb2xfY2FsbCJ9XSwiaW52YWxpZF90b29sX2NhbGxzIjpbXSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjo3Mzk2LCJvdXRwdXRfdG9rZW5zIjo3NzAsInRvdGFsX3Rva2VucyI6ODE2NiwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6NzM5fX19LHsiY29udGVudCI6IlN1Y2Nlc3NmdWxseSB0cmFuc2ZlcnJlZCB0byB2ZXJpZmllciIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsImlkIjoiYzA2NDU2M2EtOTc0Zi00ZjBmLWFiNGEtMDgwZTYzYzAxY2Y1IiwidG9vbF9jYWxsX2lkIjoidG9vbHVfMDFINmZWOEp2UkpOdlh5NDdlUm5kM2pIIiwic3RhdHVzIjoic3VjY2VzcyJ9LHsiY29udGVudCI6W3sic2lnbmF0dXJlIjoiQ0FRUzlnVUtFQWdTR0FJNEFVSUlkR2hwYm10cGJtY1NER0Z5SjFNWG9ZK0FpNk5rYnhvTVJkMlRyVWZVVTdXclBwQTJJakRneThkVXBsY1pLYzdEbkp3TkxUQmZINVVOUituR25ha1NQUWlhOFVWSXpyamtPbkoyZkxyV1VzQTJvRU84eVJzcWt3WGtrRENzeWhCOTdkd2o1N1JNdDkxWHpnV2hMOWx1TkVHTzkyeVcwck5GR29NU1VWT3hQSXJGYzVSanlyaEtFYzMrMVJucHhDa3lscStMTmxRNU5GeExuNnYyY0Y1RHpxcHZCSE5SN1hBaWcrdXJKSVUzZ1h5ZENrYUdaWkRldEp0UU9VT1hnWmk5ejkxMDBOcGRBRWtYRG8ySUNIQ0dUR3BWYzBKL3BiWHhXUkJ4YjZCMS9HbDBZam42OFY2SGxlc0g3SW9DN0dSLzNId1J1OU5TREJHNHR0ZlFqRVdjZytQZUFlYTRSbjdqengrVG05SDdMMWpvVEtWZ0RiUWc1aUtNTGZqdlBxbG5rc0NMdFhPdDhqU0lTNU9ZTE43YS83VFg2L3c1ZDM0dm53OHZQRG1VMVVZaTBRNzJxVVdoVWEydnc4T1hOcGNLTDdCcDBsTmZCUzhydkhZK3ZCWTgvaGFBb1RMM3d2TEZVMWZUKzR6L3ZBM0Jjc2lnWkRZbGZKZURZY3hYbVJ1cXh6eEVHcFhDUmU5czFIV3RJRCs0MXBIdTZscFpIa1hhZ2hwYzZUWkhMeE53Y1ZoVGI2Zk1RN1FvKzJMNlB2a2xoMW0zMnBlb2R4ZWcyWWlQaGRwbDN3L3hrT0V1NS9KRUY3WkJ4cGRVTCt1M3drTzVwVWxzcWZnWDd1aHp2N0hDMldwU1dWaW1yd2lRd1FPVXgwK0pQckx0dytsWWZxN0kzanJ1bzBHVzBndVd2bnVmN0pNM1lMajg0cStKZ0hvbW5TU1publptckwwVWFSdnJWeUxvODc3RjM5Y2NMUzdYb1pubnVpOEo0UDU4WlRtaUxrRUplR2N4MUl1YWcyZTMzMzM5TzdZZXcvNTl0TGJRdElHaDlWV013SEJuRngzOVM4LzRMRUZySzVIbGRQUWhPcmxveVR0a1JzNjZpUVJLdHhNS1MwVkk2UVRLWkRBYnM3YTdIMXlCdittSmk5WGc0dUtteVU2cW5uN3pFS2pqWTJiSWxDd2V6eDRMTEdQNUhEM1pyNmZac21kRzFVSHpEc0J2WUcyb0NJMG9xRUtNK3ZhbHNUNmhVOE1SamJRQkQvSko4SkpOL2lCR2xoQkFVSG8wVW1oaHRSMmV1VFZwWldsTW5obXBYQWRLRFhTZlBkYVBvaGdCIiwidGhpbmtpbmciOiIiLCJ0eXBlIjoidGhpbmtpbmcifSx7ImlkIjoidG9vbHVfMDFYRUF1aFFIQ0JkeUN3dHltWndGZkRpIiwiY2FsbGVyIjp7InR5cGUiOiJkaXJlY3QifSwiaW5wdXQiOnt9LCJuYW1lIjoidHJhbnNmZXJfdG9fcmVkX3RlYW0iLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QXNhYkRxdWVnSGk2WVVwQ24iLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzUwOSwib3V0cHV0X3Rva2VucyI6ODgsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjU1fSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCIsIm1vZGVsX3Byb3ZpZGVyIjoiYW50aHJvcGljIn0sInR5cGUiOiJhaSIsIm5hbWUiOiJ2ZXJpZmllciIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy02NjgyLTdkMjEtOGMxNS1mZmJjZTk0MjJjOWYtMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ0cmFuc2Zlcl90b19yZWRfdGVhbSIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxWEVBdWhRSENCZHlDd3R5bVp3RmZEaSIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzUwOSwib3V0cHV0X3Rva2VucyI6ODgsInRvdGFsX3Rva2VucyI6NzU5NywiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6NTV9fX0seyJjb250ZW50IjoiU3VjY2Vzc2Z1bGx5IHRyYW5zZmVycmVkIHRvIHJlZF90ZWFtIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJ0b29sIiwibmFtZSI6InRyYW5zZmVyX3RvX3JlZF90ZWFtIiwiaWQiOiI5ODJjMDEyNi1lNmFiLTQ1OGEtOTIyZS03YjI2NmU4M2MyYjIiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMVhFQXVoUUhDQmR5Q3d0eW1ad0ZmRGkiLCJzdGF0dXMiOiJzdWNjZXNzIn0seyJjb250ZW50IjpbeyJzaWduYXR1cmUiOiJDQVFTd2djS0VBZ1NHQUk0QVVJSWRHaHBibXRwYm1jU0RPUlhnZTl1TDE4MzJSTVF3Qm9NalF3QXdBSGI5aVAxb2pQR0lqRERTY2FsNDg2YmVDSmJ0RzgvRDJZOWc5UzV4SVBmam9IZy9keHdYNUh5cFNRRUJiakZpVFNiUEVDWFNGelY2WUlxM3diTGNteVBidnVzR25EZXhGZ3grM3NJWHp2cXlGVi82OXhoaWhNblNwZlRDSHkwKzB6U3UxV3BZVS9XVXBJZm9hU1hUY1RhMXFrTTh4OEVrL3kvMWprS01aVWZERmE5M3VoVUp4T0M0K1FnUFdCbC8rWHkydDgyTDJ4L2VicTdRTFFUcXRSMGpYZWFVUlFtM0NvNkp2T2dXbUR2TXBFZFRlVFdFM2ZEaDF0enpWS3NmbHVtNGQ4OWI2Q2FLdG44bmxRZDdSaE45OXZOUU1RSEc1VjRPdVZOZHJIUFpacEZPdWhkZWpzTUZXWHlCKzU1c2xHQWQ4d0VYZ0ZLYkkva25PWFZHekwvTkRGOEd0eTF0TlpsdTJmZFNDQXNZV01zRlFQMWNid0xlUUdzenlYekpNVlpKMkk5eEJQa3l6MUhPWENQeWg1RitqMmdSVFBpa1ZCM0FSQzZ0ZkIvbERTVWVLQlZaUFFTOHBZL0I0ZUNzK0lqamFCTldydUx6ZnFDc2M2eEwwQ3VRUFNZeVFYYVlGallyU1JOY0M3MFdRQldUNVZWaHAwbUE5eVpER1liUlp0OUk1T0s5UWtRbXVKNXpERmFNRU9YelNyc0M0bHRlUDZNZGltaU1panZzSDNpcStjNldVWW9sUmozbmVJODhFYUhJekxBR1JxNjRCTkNMemRBWUwycEVVMnNiNXVHNzhQS1ZvVVVDYlZKTDVqcERPaExPVE8xYlFzVHVvVXBuUlJyTFFEL2tJelYrbGpwVkJ3d2ZKY3lxNzVUSkEvQ3RQci94cjJTWjlCMWIyc2JqRzZqRDBsaHRvQ2RkZURnY3FnbC9MU1NrY3VhWVk4eFBXQ3krTHROaGNxVGhmbWZ2bkhMSklwTDZnamJqVy9PUXBSZlZVd2Z3MWpPYjliV1Z3Y2dIaE5ZZktZR1B0TThvdGg1eHdMWVJSVmd6cU1LT3ZzN2ZhbFhSaFZ6WjJTUDZJdzBCTW1BQVo2SGpmQ2w5TCt0UHFKTmxuUVFPSHBvV2p1bTRCK1hzWWxkRkVLR1FXUXFzZDU5aU5QdmFvOXAvc0NPdDdyR3FDb3pjbWdpRW00dzY2ZXovUUlEeWI0Nm4wK3RjTkpQVWNvQkt2US9qRHdFNlZ3bWtRL25FQ2pGcDNVZERUK2tXWVFTTmQ4ajhWQlpPcEc4TGxxVVlyRVpDdjBxN0JzMXZjTnJkWWlhRm9TMzJvc2F5bldrK0h0M09IdUZXYkVGOEpsc0ZUUUFPMDVWdllrZWNmMFYvb1RIc0c1aHlPT3M3VUVhREZxN3VhSnV6V1ZoaDVwQW9qSWFCY1I4ZlZsbjJQTFJUWDdpczJuc0ErUXJkaWRlUGNadEZ0Z3hBaUtZZTFpMnNTRmk0dm04RU9rQ2JRNjN3ZFFlWTRVcnI1UHVvTUV3eFVVTGpnS2FnaUFtc0pEcTRSclZtN3NPNCs2WXVBOWo2bDJ1eWxrT0VjNjd2Zi81ZmtYdUNzaUFwTkw2a2ViOGMrdkwwbjRGWEdkemorYXBTRGZGR0JnQiIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJpZCI6InRvb2x1XzAxVkJyV2RXRVdZZ2l0NGtOWWVoQk1VNSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX2VkaXRvciIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHdBemZqcVVvcG9FbVpMYXFtMSIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo3NDE2LCJvdXRwdXRfdG9rZW5zIjoxNTEsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjExOX0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoicmVkX3RlYW0iLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtNmNlNi03MjUxLWI3OTYtNGY2NDMzM2Y2N2M2LTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoidHJhbnNmZXJfdG9fZWRpdG9yIiwiYXJncyI6e30sImlkIjoidG9vbHVfMDFWQnJXZFdFV1lnaXQ0a05ZZWhCTVU1IiwidHlwZSI6InRvb2xfY2FsbCJ9XSwiaW52YWxpZF90b29sX2NhbGxzIjpbXSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjo3NDE2LCJvdXRwdXRfdG9rZW5zIjoxNTEsInRvdGFsX3Rva2VucyI6NzU2NywiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MTE5fX19LHsiY29udGVudCI6IlN1Y2Nlc3NmdWxseSB0cmFuc2ZlcnJlZCB0byBlZGl0b3IiLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7fSwidHlwZSI6InRvb2wiLCJuYW1lIjoidHJhbnNmZXJfdG9fZWRpdG9yIiwidG9vbF9jYWxsX2lkIjoidG9vbHVfMDFWQnJXZFdFV1lnaXQ0a05ZZWhCTVU1Iiwic3RhdHVzIjoic3VjY2VzcyJ9XSwiYWN0aXZlX2FnZW50IjoiZWRpdG9yIn0sInJlc3VtZSI6bnVsbCwiZ290byI6ImVkaXRvciJ9fQ==" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "1723d5322674ec35", + "parentSpanId": "59d91c7c0791b7de", + "name": "model", + "kind": 1, + "startTimeUnixNano": "1790968032482510080", + "endTimeUnixNano": "1790968035029985792", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "1" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "red_team:15ccef64-4162-9e02-e87c-4cb0eb6ea16b|model:04f09f43-f066-c110-818d-1a33d3ba1569" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "red_team" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "red_team:15ccef64-4162-9e02-e87c-4cb0eb6ea16b" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "graph:step:1" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W3siY29udGVudCI6IkZvciBhIHN5bnRoZXRpYyB0cmFjaW5nIGZpeHR1cmUsIGV4cGxhaW4gUHl0aG9uIHR1cGxlIHZlcnN1cyBsaXN0IGltbXV0YWJpbGl0eSBpbiBhdCBtb3N0IDEyMCB3b3Jkcy4gVXNlIG9ubHkgb2ZmaWNpYWwgUHl0aG9uIGRvY3VtZW50YXRpb24gaWYgeW91IHNlYXJjaC4gSGF2ZSB0aGUgc2tlcHRpYyBhbmQgdmVyaWZpZXIgY2hlY2sgaXQsIHRoZW4gaGF2ZSB0aGUgZWRpdG9yIHdyaXRlIC9hbnN3ZXIubWQuIEtlZXAgdGhlIHJ1biBzaG9ydCBhbmQgZG8gbm90IGFzayBmb2xsb3ctdXAgcXVlc3Rpb25zLiIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoiaHVtYW4iLCJpZCI6IjQ2MjA0MzA1LWY4ZWUtNDM4NC05Y2U4LWZjZDgwYmZjZGY3NSJ9LHsiY29udGVudCI6W3siaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9LCJuYW1lIjoid2ViX3NlYXJjaCIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHc5YVc5YnJlblBxZ3liZk53UCIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjoyODQ1LCJvdXRwdXRfdG9rZW5zIjo3Mywib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6MH0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoicmVzZWFyY2hlciIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy0yMWEzLTcwMDAtYTM4MS0yODRhNmEwYzAzZjctMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ3ZWJfc2VhcmNoIiwiYXJncyI6eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9LCJpZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6Mjg0NSwib3V0cHV0X3Rva2VucyI6NzMsInRvdGFsX3Rva2VucyI6MjkxOCwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fSx7ImNvbnRlbnQiOiJbe1widGl0bGVcIjogXCI1LiBEYXRhIFN0cnVjdHVyZXMgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3R1dG9yaWFsL2RhdGFzdHJ1Y3R1cmVzLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcIllvdSBtaWdodCBoYXZlIG5vdGljZWQgdGhhdCBtZXRob2RzIGxpa2VgaW5zZXJ0YCxgcmVtb3ZlYCBvcmBzb3J0YCB0aGF0IG9ubHkgbW9kaWZ5IHRoZSBsaXN0IGhhdmUgbm8gcmV0dXJuIHZhbHVlIHByaW50ZWQgXFx1MjAxMyB0aGV5IHJldHVybiB0aGUgZGVmYXVsdGBOb25lYC4gWzFdIFRoaXMgaXMgYSBkZXNpZ24gcHJpbmNpcGxlIGZvciBhbGwgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgaW4gUHl0aG9uLlxcbi4uLlxcbiMjIDUuMy4gVHVwbGVzIGFuZCBTZXF1ZW5jZXNcXHUwMGI2XFxuXFxuV2Ugc2F3IHRoYXQgbGlzdHMgYW5kIHN0cmluZ3MgaGF2ZSBtYW55IGNvbW1vbiBwcm9wZXJ0aWVzLCBzdWNoIGFzIGluZGV4aW5nIGFuZCBzbGljaW5nIG9wZXJhdGlvbnMuIFRoZXkgYXJlIHR3byBleGFtcGxlcyBvZiBzZXF1ZW5jZSBkYXRhIHR5cGVzIChzZWUgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBsaXN0LCB0dXBsZSwgcmFuZ2UpLiBTaW5jZSBQeXRob24gaXMgYW4gZXZvbHZpbmcgbGFuZ3VhZ2UsIG90aGVyIHNlcXVlbmNlIGRhdGEgdHlwZXMgbWF5IGJlIGFkZGVkLiBUaGVyZSBpcyBhbHNvIGFub3RoZXIgc3RhbmRhcmQgc2VxdWVuY2UgZGF0YSB0eXBlOiB0aGUgdHVwbGUuXFxuXFxuQSB0dXBsZSBjb25zaXN0cyBvZiBhIG51bWJlciBvZiB2YWx1ZXMgc2VwYXJhdGVkIGJ5IGNvbW1hcywgZm9yIGluc3RhbmNlOlxcbi4uLlxcbj4+PiAjIFR1cGxlcyBhcmUgaW1tdXRhYmxlOlxcbj4+PiB0WzBdID0gODg4ODhcXG5UcmFjZWJhY2sgKG1vc3QgcmVjZW50IGNhbGwgbGFzdCk6XFxuICBGaWxlIFxcXCI8c3RkaW4+XFxcIiwgbGluZSAxLCBpbiA8bW9kdWxlPlxcblR5cGVFcnJvcjogJ3R1cGxlJyBvYmplY3QgZG9lcyBub3Qgc3VwcG9ydCBpdGVtIGFzc2lnbm1lbnRcXG4+Pj4gIyBidXQgdGhleSBjYW4gY29udGFpbiBtdXRhYmxlIG9iamVjdHM6XFxuPj4+IHYgPSAoWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuPj4+IHZcXG4oWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuLi4uXFxuQXMgeW91IHNlZSwgb24gb3V0cHV0IHR1cGxlcyBhcmUgYWx3YXlzIGVuY2xvc2VkIGluIHBhcmVudGhlc2VzLCBzbyB0aGF0IG5lc3RlZCB0dXBsZXMgYXJlIGludGVycHJldGVkIGNvcnJlY3RseTsgdGhleSBtYXkgYmUgaW5wdXQgd2l0aCBvciB3aXRob3V0IHN1cnJvdW5kaW5nIHBhcmVudGhlc2VzLCBhbHRob3VnaCBvZnRlbiBwYXJlbnRoZXNlcyBhcmUgbmVjZXNzYXJ5IGFueXdheSAoaWYgdGhlIHR1cGxlIGlzIHBhcnQgb2YgYSBsYXJnZXIgZXhwcmVzc2lvbikuIEl0IGlzIG5vdCBwb3NzaWJsZSB0byBhc3NpZ24gdG8gdGhlIGluZGl2aWR1YWwgaXRlbXMgb2YgYSB0dXBsZSwgaG93ZXZlciBpdCBpcyBwb3NzaWJsZSB0byBjcmVhdGUgdHVwbGVzIHdoaWNoIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzLCBzdWNoIGFzIGxpc3RzLlxcblxcblRob3VnaCB0dXBsZXMgbWF5IHNlZW0gc2ltaWxhciB0byBsaXN0cywgdGhleSBhcmUgb2Z0ZW4gdXNlZCBpbiBkaWZmZXJlbnQgc2l0dWF0aW9ucyBhbmQgZm9yIGRpZmZlcmVudCBwdXJwb3Nlcy4gVHVwbGVzIGFyZSBpbW11dGFibGUsIGFuZCB1c3VhbGx5IGNvbnRhaW4gYSBoZXRlcm9nZW5lb3VzIHNlcXVlbmNlIG9mIGVsZW1lbnRzIHRoYXQgYXJlIGFjY2Vzc2VkIHZpYSB1bnBhY2tpbmcgKHNlZSBsYXRlciBpbiB0aGlzIHNlY3Rpb24pIG9yIGluZGV4aW5nIChvciBldmVuIGJ5IGF0dHJpYnV0ZSBpbiB0aGUgY2FzZSBvZiBuYW1lZHR1cGxlcykuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG5Bbm90aGVyIHVzZWZ1bCBkYXRhIHR5cGUgYnVpbHQgaW50byBQeXRob24gaXMgdGhlIGRpY3Rpb25hcnkgKHNlZSBNYXBwaW5nIFR5cGVzIFxcdTIwMTQgZGljdCkuIERpY3Rpb25hcmllcyBhcmUgc29tZXRpbWVzIGZvdW5kIGluIG90aGVyIGxhbmd1YWdlcyBhcyBcXHUyMDFjYXNzb2NpYXRpdmUgbWVtb3JpZXNcXHUyMDFkIG9yIFxcdTIwMWNhc3NvY2lhdGl2ZSBhcnJheXNcXHUyMDFkLiBVbmxpa2Ugc2VxdWVuY2VzLCB3aGljaCBhcmUgaW5kZXhlZCBieSBhIHJhbmdlIG9mIG51bWJlcnMsIGRpY3Rpb25hcmllcyBhcmUgaW5kZXhlZCBieSBrZXlzLCB3aGljaCBjYW4gYmUgYW55IGltbXV0YWJsZSB0eXBlOyBzdHJpbmdzIGFuZCBudW1iZXJzIGNhbiBhbHdheXMgYmUga2V5cy4gVHVwbGVzIGNhbiBiZSB1c2VkIGFzIGtleXMgaWYgdGhleSBjb250YWluIG9ubHkgc3RyaW5ncywgbnVtYmVycywgb3IgdHVwbGVzOyBpZiBhIHR1cGxlIGNvbnRhaW5zIGFueSBtdXRhYmxlIG9iamVjdCBlaXRoZXIgZGlyZWN0bHkgb3IgaW5kaXJlY3RseSwgaXQgY2Fubm90IGJlIHVzZWQgYXMgYSBrZXkuIFlvdSBjYW5cXHUyMDE5dCB1c2UgbGlzdHMgYXMga2V5cywgc2luY2UgbGlzdHMgY2FuIGJlIG1vZGlmaWVkIGluIHBsYWNlIHVzaW5nIGluZGV4IGFzc2lnbm1lbnRzLCBzbGljZSBhc3NpZ25tZW50cywgb3IgbWV0aG9kcyBsaWtlIGFwcGVuZCgpIGFuZCBleHRlbmQoKS5cIl19LCB7XCJ0aXRsZVwiOiBcIjMuIERhdGEgbW9kZWwgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3JlZmVyZW5jZS9kYXRhbW9kZWwuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiVGhlIHZhbHVlIG9mIHNvbWUgb2JqZWN0cyBjYW4gY2hhbmdlLiBPYmplY3RzIHdob3NlIHZhbHVlIGNhbiBjaGFuZ2UgYXJlIHNhaWQgdG8gYmUgbXV0YWJsZTsgb2JqZWN0cyB3aG9zZSB2YWx1ZSBpcyB1bmNoYW5nZWFibGUgb25jZSB0aGV5IGFyZSBjcmVhdGVkIGFyZSBjYWxsZWQgaW1tdXRhYmxlLiAoVGhlIHZhbHVlIG9mIGFuIGltbXV0YWJsZSBjb250YWluZXIgb2JqZWN0IHRoYXQgY29udGFpbnMgYSByZWZlcmVuY2UgdG8gYSBtdXRhYmxlIG9iamVjdCBjYW4gY2hhbmdlIHdoZW4gdGhlIGxhdHRlclxcdTIwMTlzIHZhbHVlIGlzIGNoYW5nZWQ7IGhvd2V2ZXIgdGhlIGNvbnRhaW5lciBpcyBzdGlsbCBjb25zaWRlcmVkIGltbXV0YWJsZSwgYmVjYXVzZSB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGl0IGNvbnRhaW5zIGNhbm5vdCBiZSBjaGFuZ2VkLiBTbywgaW1tdXRhYmlsaXR5IGlzIG5vdCBzdHJpY3RseSB0aGUgc2FtZSBhcyBoYXZpbmcgYW4gdW5jaGFuZ2VhYmxlIHZhbHVlLCBpdCBpcyBtb3JlIHN1YnRsZS4pIEFuIG9iamVjdFxcdTIwMTlzIG11dGFiaWxpdHkgaXMgZGV0ZXJtaW5lZCBieSBpdHMgdHlwZTsgZm9yIGluc3RhbmNlLCBudW1iZXJzLCBzdHJpbmdzIGFuZCB0dXBsZXMgYXJlIGltbXV0YWJsZSwgd2hpbGUgZGljdGlvbmFyaWVzIGFuZCBsaXN0cyBhcmUgbXV0YWJsZS5cXG4uLi5cXG5Tb21lIG9iamVjdHMgY29udGFpbiByZWZlcmVuY2VzIHRvIG90aGVyIG9iamVjdHM7IHRoZXNlIGFyZSBjYWxsZWQgY29udGFpbmVycy4gRXhhbXBsZXMgb2YgY29udGFpbmVycyBhcmUgdHVwbGVzLCBsaXN0cyBhbmQgZGljdGlvbmFyaWVzLiBUaGUgcmVmZXJlbmNlcyBhcmUgcGFydCBvZiBhIGNvbnRhaW5lclxcdTIwMTlzIHZhbHVlLiBJbiBtb3N0IGNhc2VzLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIHZhbHVlIG9mIGEgY29udGFpbmVyLCB3ZSBpbXBseSB0aGUgdmFsdWVzLCBub3QgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGNvbnRhaW5lZCBvYmplY3RzOyBob3dldmVyLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIG11dGFiaWxpdHkgb2YgYSBjb250YWluZXIsIG9ubHkgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGltbWVkaWF0ZWx5IGNvbnRhaW5lZCBvYmplY3RzIGFyZSBpbXBsaWVkLiBTbywgaWYgYW4gaW1tdXRhYmxlIGNvbnRhaW5lciAobGlrZSBhIHR1cGxlKSBjb250YWlucyBhIHJlZmVyZW5jZSB0byBhIG11dGFibGUgb2JqZWN0LCBpdHMgdmFsdWUgY2hhbmdlcyBpZiB0aGF0IG11dGFibGUgb2JqZWN0IGlzIGNoYW5nZWQuXFxuLi4uXFxuIyMjIDMuMi41LiBTZXF1ZW5jZXNcXHUwMGI2XFxuLi4uXFxuIyMjIyAzLjIuNS4xLiBJbW11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbkFuIG9iamVjdCBvZiBhbiBpbW11dGFibGUgc2VxdWVuY2UgdHlwZSBjYW5ub3QgY2hhbmdlIG9uY2UgaXQgaXMgY3JlYXRlZC4gKElmIHRoZSBvYmplY3QgY29udGFpbnMgcmVmZXJlbmNlcyB0byBvdGhlciBvYmplY3RzLCB0aGVzZSBvdGhlciBvYmplY3RzIG1heSBiZSBtdXRhYmxlIGFuZCBtYXkgYmUgY2hhbmdlZDsgaG93ZXZlciwgdGhlIGNvbGxlY3Rpb24gb2Ygb2JqZWN0cyBkaXJlY3RseSByZWZlcmVuY2VkIGJ5IGFuIGltbXV0YWJsZSBvYmplY3QgY2Fubm90IGNoYW5nZS4pXFxuXFxuVGhlIGZvbGxvd2luZyB0eXBlcyBhcmUgaW1tdXRhYmxlIHNlcXVlbmNlczpcXG4uLi5cXG5UdXBsZXNcXG46IFRoZSBpdGVtcyBvZiBhIGB0dXBsZWAgYXJlIGFyYml0cmFyeSBQeXRob24gb2JqZWN0cy4gVHVwbGVzIG9mIHR3byBvciBtb3JlIGl0ZW1zIGFyZSBmb3JtZWQgYnkgY29tbWEtc2VwYXJhdGVkIGxpc3RzIG9mIGV4cHJlc3Npb25zLiBBIHR1cGxlIG9mIG9uZSBpdGVtIChhIFxcdTIwMThzaW5nbGV0b25cXHUyMDE5KSBjYW4gYmUgZm9ybWVkIGJ5IGFmZml4aW5nIGEgY29tbWEgdG8gYW4gZXhwcmVzc2lvbiAoYW4gZXhwcmVzc2lvbiBieSBpdHNlbGYgZG9lcyBub3QgY3JlYXRlIGEgdHVwbGUsIHNpbmNlIHBhcmVudGhlc2VzIG11c3QgYmUgdXNhYmxlIGZvciBncm91cGluZyBvZiBleHByZXNzaW9ucykuIEFuIGVtcHR5IHR1cGxlIGNhbiBiZSBmb3JtZWQgYnkgYW4gZW1wdHkgcGFpciBvZiBwYXJlbnRoZXNlcy5cXG4uLi5cXG4jIyMjIDMuMi41LjIuIE11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbk11dGFibGUgc2VxdWVuY2VzIGNhbiBiZSBjaGFuZ2VkIGFmdGVyIHRoZXkgYXJlIGNyZWF0ZWQuIFRoZSBzdWJzY3JpcHRpb24gYW5kIHNsaWNpbmcgbm90YXRpb25zIGNhbiBiZSB1c2VkIGFzIHRoZSB0YXJnZXQgb2YgYXNzaWdubWVudCBhbmQgYGRlbGAgKGRlbGV0ZSkgc3RhdGVtZW50cy5cXG4uLi5cXG5UaGVyZSBhcmUgY3VycmVudGx5IHR3byBpbnRyaW5zaWMgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlczpcXG5cXG5MaXN0c1xcbjogVGhlIGl0ZW1zIG9mIGEgbGlzdCBhcmUgYXJiaXRyYXJ5IFB5dGhvbiBvYmplY3RzLiBMaXN0cyBhcmUgZm9ybWVkIGJ5IHBsYWNpbmcgYSBjb21tYS1zZXBhcmF0ZWQgbGlzdCBvZiBleHByZXNzaW9ucyBpbiBzcXVhcmUgYnJhY2tldHMuIChOb3RlIHRoYXQgdGhlcmUgYXJlIG5vIHNwZWNpYWwgY2FzZXMgbmVlZGVkIHRvIGZvcm0gbGlzdHMgb2YgbGVuZ3RoIDAgb3IgMS4pXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjcgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvYnVpbHRpbnMvc3RkdHlwZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiY29sbGVjdGlvbiBjbGFzc2VzIGFyZSBtdXRhYmxlLiBUaGUgbWV0aG9kcyB0aGF0IGFkZCwgc3VidHJhY3QsIG9yIHJlYXJyYW5nZSB0aGVpciAuLi4gaW4gcGxhY2UsIGFuZCBkb25cXHUyMDE5dCByZXR1cm4gYSAuLi4gLCBuZXZlciByZXR1cm4gdGhlIGNvbGxlY3Rpb24gaW5zdGFuY2UgaXRzZWxmIGJ1dCBgTm9uZWAuXFxuLi4uXFxuIyMgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBgbGlzdGAsIGB0dXBsZWAsIGByYW5nZWBcXHUwMGI2XFxuXFxuVGhlcmUgYXJlIHRocmVlIGJhc2ljIHNlcXVlbmNlIHR5cGVzOiBsaXN0cywgdHVwbGVzLCBhbmQgcmFuZ2Ugb2JqZWN0cy4gQWRkaXRpb25hbCBzZXF1ZW5jZSB0eXBlcyB0YWlsb3JlZCBmb3IgcHJvY2Vzc2luZyBvZiBiaW5hcnkgZGF0YSBhbmQgdGV4dCBzdHJpbmdzIGFyZSBkZXNjcmliZWQgaW4gZGVkaWNhdGVkIHNlY3Rpb25zLlxcbi4uLlxcblRoZSBvcGVyYXRpb25zIGluIHRoZSBmb2xsb3dpbmcgdGFibGUgLi4uIG11dGFibGUgYW5kIGltbXV0YWJsZS4gVGhlIGBjb2xsZWN0aW9ucy4gLi4uIGlzIHByb3ZpZGVkIHRvIG1ha2UgLi4uIGVhc2llciB0byBjb3JyZWN0bHkgaW1wbGVtZW50IHRoZXNlIG9wZXJhdGlvbnMgb24gY3VzdG9tIHNlcXVlbmNlIHR5cGVzXFxuLi4uXFxuIyMjIEltbXV0YWJsZSBTZXF1ZW5jZSBUeXBlc1xcdTAwYjZcXG4uLi5cXG5tdXRhYmxlIHNlcXVlbmNlIHR5cGVzIGlzXFxuLi4uXFxuc3VwcG9ydCBhbGxvd3MgaW1tdXRhYmxlIHNlcXVlbmNlcywgLi4uICwgdG8gYmUgdXNlZCBhcyBgZGljdGAga2V5cyBhbmQgc3RvcmVkIGluIC4uLiBlbnNldGAgaW5zdGFuY2VzXFxuLi4uXFxuIyMjIE11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxuIyMjIExpc3RzXFx1MDBiNlxcblxcbkxpc3RzIGFyZSBtdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaG9tb2dlbmVvdXMgaXRlbXMgKHdoZXJlIHRoZSBwcmVjaXNlIGRlZ3JlZSBvZiBzaW1pbGFyaXR5IHdpbGwgdmFyeSBieSBhcHBsaWNhdGlvbikuXFxuLi4uXFxuIyMjIFR1cGxlc1xcdTAwYjZcXG5cXG5UdXBsZXMgYXJlIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhldGVyb2dlbmVvdXMgZGF0YSAoc3VjaCBhcyB0aGUgMi10dXBsZXMgcHJvZHVjZWQgYnkgdGhlIGBlbnVtZXJhdGUoKWAgYnVpbHQtaW4pLiBUdXBsZXMgYXJlIGFsc28gdXNlZCBmb3IgY2FzZXMgd2hlcmUgYW4gaW1tdXRhYmxlIHNlcXVlbmNlIG9mIGhvbW9nZW5lb3VzIGRhdGEgaXMgbmVlZGVkIChzdWNoIGFzIGFsbG93aW5nIHN0b3JhZ2UgaW4gYSBgc2V0YCBvciBgZGljdGAgaW5zdGFuY2UpLlxcbi4uLlxcbnR1cGxlKGl0ZXJhYmxlPVxcbi4uLlxcblRoZSBjb25zdHJ1Y3RvciBidWlsZHMgYSB0dXBsZSB3aG9zZSBpdGVtcyBhcmUgdGhlIHNhbWUgYW5kIGluIHRoZSBzYW1lIG9yZGVyIGFzIGl0ZXJhYmxlXFx1MjAxOXMgaXRlbXMuIGl0ZXJhYmxlIG1heSBiZSBlaXRoZXIgYSBzZXF1ZW5jZSwgYSBjb250YWluZXIgdGhhdCBzdXBwb3J0cyBpdGVyYXRpb24sIG9yIGFuIGl0ZXJhdG9yIG9iamVjdC4gSWYgaXRlcmFibGUgaXMgYWxyZWFkeSBhIHR1cGxlLCBpdCBpcyByZXR1cm5lZCB1bmNoYW5nZWQuIEZvciBleGFtcGxlLCBgdHVwbGUoJ2FiYycpYCByZXR1cm5zIGAoJ2EnLCAnYicsICdjJylgIGFuZCBgdHVwbGUoIFsxLCAyLCAzXSApYCByZXR1cm5zIGAoMSwgMiwgMylgLiBJZiBubyBhcmd1bWVudCBpcyBnaXZlbiwgdGhlIGNvbnN0cnVjdG9yIGNyZWF0ZXMgYSBuZXcgZW1wdHkgdHVwbGUsIGAoKWAuXFxuLi4uXFxuVHVwbGVzIGltcGxlbWVudCBhbGwgb2YgdGhlIGNvbW1vbiBzZXF1ZW5jZSBvcGVyYXRpb25zLlxcbi4uLlxcbkZvciBoZXRlcm9nZW5lb3VzIGNvbGxlY3Rpb25zIG9mIGRhdGEgd2hlcmUgYWNjZXNzIGJ5IG5hbWUgaXMgY2xlYXJlciB0aGFuIGFjY2VzcyBieSBpbmRleCwgYGNvbGxlY3Rpb25zLm5hbWVkdHVwbGUoKWAgbWF5IGJlIGEgbW9yZSBhcHByb3ByaWF0ZSBjaG9pY2UgdGhhbiBhIHNpbXBsZSB0dXBsZSBvYmplY3QuXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvbGlicmFyeS9zdGR0eXBlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJTb21lIGNvbGxlY3Rpb24gY2xhc3NlcyBhcmUgbXV0YWJsZS4gVGhlIG1ldGhvZHMgdGhhdCBhZGQsIHN1YnRyYWN0LCBvciByZWFycmFuZ2UgdGhlaXIgLi4uIGluIHBsYWNlLCBhbmQgZG9uXFx1MjAxOXQgcmV0dXJuIGEgLi4uICwgbmV2ZXIgcmV0dXJuIHRoZSBjb2xsZWN0aW9uIGluc3RhbmNlIGl0c2VsZiBidXQgYE5vbmVgLlxcbi4uLlxcbiMjIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgYGxpc3RgLCBgdHVwbGVgLCBgcmFuZ2VgXFx1MDBiNlxcblxcblRoZXJlIGFyZSB0aHJlZSBiYXNpYyBzZXF1ZW5jZSB0eXBlczogbGlzdHMsIHR1cGxlcywgYW5kIC4uLiBvYmplY3RzLiBBZGRpdGlvbmFsIHNlcXVlbmNlIHR5cGVzIHRhaWxvcmVkIGZvciBwcm9jZXNzaW5nIG9mIGJpbmFyeSBkYXRhIGFuZCB0ZXh0IHN0cmluZ3MgYXJlIGRlc2NyaWJlZCBpbiBkZWRpY2F0ZWQgc2VjdGlvbnMuXFxuLi4uXFxuVGhlIG9wZXJhdGlvbnMgaW4gdGhlIGZvbGxvd2luZyB0YWJsZSAuLi4gaW1tdXRhYmxlLiBUaGUgYCAuLi4gaXMgcHJvdmlkZWQgdG8gbWFrZSAuLi4gZWFzaWVyIHRvIGNvcnJlY3RseSBpbXBsZW1lbnQgdGhlc2Ugb3BlcmF0aW9ucyBvbiBjdXN0b20gc2VxdWVuY2UgdHlwZXNcXG4uLi5cXG4jIyMgSW1tdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbm9wZXJhdGlvbiB0aGF0IGltbXV0YWJsZSBzZXF1ZW5jZSAuLi4gYnkgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlcyBpcyAuLi4gYGhhc2goKWBcXG4uLi5cXG5UaGlzIHN1cHBvcnQgYWxsb3dzIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHN1Y2ggYXMgYHR1cGxlYCBpbnN0YW5jZXMsIHRvIGJlIHVzZWQgYXMgYGRpY3RgIGtleXMgYW5kIHN0b3JlZCBpbiBgc2V0YCBhbmQgYGZyb3plbnNldGAgaW5zdGFuY2VzLlxcbi4uLlxcbiMjIyBNdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbiMjIyBMaXN0c1xcdTAwYjZcXG5cXG5MaXN0cyBhcmUgbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhvbW9nZW5lb3VzIGl0ZW1zICh3aGVyZSB0aGUgcHJlY2lzZSBkZWdyZWUgb2Ygc2ltaWxhcml0eSB3aWxsIHZhcnkgYnkgYXBwbGljYXRpb24pLlxcbi4uLlxcbiMjIyBUdXBsZXNcXHUwMGI2XFxuXFxuVHVwbGVzIGFyZSBpbW11dGFibGUgc2VxdWVuY2VzLCB0eXBpY2FsbHkgdXNlZCB0byBzdG9yZSBjb2xsZWN0aW9ucyBvZiBoZXRlcm9nZW5lb3VzIGRhdGEgKHN1Y2ggYXMgdGhlIDItdHVwbGVzIHByb2R1Y2VkIGJ5IHRoZSBgZW51bWVyYXRlKClgIGJ1aWx0LWluKS4gVHVwbGVzIGFyZSBhbHNvIHVzZWQgZm9yIGNhc2VzIHdoZXJlIGFuIGltbXV0YWJsZSBzZXF1ZW5jZSBvZiBob21vZ2VuZW91cyBkYXRhIGlzIG5lZWRlZCAoc3VjaCBhcyBhbGxvd2luZyBzdG9yYWdlIGluIGEgYHNldGAgb3IgYGRpY3RgIGluc3RhbmNlKS5cXG4uLi5cXG5UaGUgY29uc3RydWN0b3IgYnVpbGRzIGEgdHVwbGUgd2hvc2UgaXRlbXMgYXJlIHRoZSBzYW1lIGFuZCBpbiB0aGUgc2FtZSBvcmRlciBhcyBpdGVyYWJsZVxcdTIwMTlzIGl0ZW1zLiBpdGVyYWJsZSBtYXkgYmUgZWl0aGVyIGEgc2VxdWVuY2UsIGEgY29udGFpbmVyIHRoYXQgc3VwcG9ydHMgaXRlcmF0aW9uLCBvciBhbiBpdGVyYXRvciBvYmplY3QuIElmIGl0ZXJhYmxlIGlzIGFscmVhZHkgYSB0dXBsZSwgaXQgaXMgcmV0dXJuZWQgdW5jaGFuZ2VkLiBGb3IgZXhhbXBsZSwgYHR1cGxlKCdhYmMnKWAgcmV0dXJucyBgKCdhJywgJ2InLCAnYycpYCBhbmQgYHR1cGxlKCBbMSwgMiwgM10gKWAgcmV0dXJucyBgKDEsIDIsIDMpYC4gSWYgbm8gYXJndW1lbnQgaXMgZ2l2ZW4sIHRoZSBjb25zdHJ1Y3RvciBjcmVhdGVzIGEgbmV3IGVtcHR5IHR1cGxlLCBgKClgLlxcbi4uLlxcblR1cGxlcyBpbXBsZW1lbnQgYWxsIG9mIHRoZSBjb21tb24gc2VxdWVuY2Ugb3BlcmF0aW9ucy5cXG4uLi5cXG5Gb3IgaGV0ZXJvZ2VuZW91cyBjb2xsZWN0aW9ucyBvZiBkYXRhIHdoZXJlIGFjY2VzcyBieSBuYW1lIGlzIGNsZWFyZXIgdGhhbiBhY2Nlc3MgYnkgaW5kZXgsIGBjb2xsZWN0aW9ucy5uYW1lZHR1cGxlKClgIG1heSBiZSBhIG1vcmUgYXBwcm9wcmlhdGUgY2hvaWNlIHRoYW4gYSBzaW1wbGUgdHVwbGUgb2JqZWN0LlwiXX0sIHtcInRpdGxlXCI6IFwiNS4gRGF0YSBTdHJ1Y3R1cmVzIFxcdTIwMTQgUHl0aG9uIDMuMTAuMjAgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMuMTAvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiWW91IG1pZ2h0IGhhdmUgbm90aWNlZCB0aGF0IG1ldGhvZHMgbGlrZSBgaW5zZXJ0YCwgYHJlbW92ZWAgb3IgYHNvcnRgIHRoYXQgb25seSBtb2RpZnkgdGhlIGxpc3QgaGF2ZSBubyByZXR1cm4gdmFsdWUgcHJpbnRlZCBcXHUyMDEzIHRoZXkgcmV0dXJuIHRoZSBkZWZhdWx0IGBOb25lYC4gMSBUaGlzIGlzIGEgZGVzaWduIHByaW5jaXBsZSBmb3IgYWxsIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIGluIFB5dGhvbi5cXG4uLi5cXG4jIyA1LjMuIFR1cGxlcyBhbmQgU2VxdWVuY2VzXFx1MDBiNlxcblxcbldlIHNhdyB0aGF0IGxpc3RzIGFuZCBzdHJpbmdzIGhhdmUgbWFueSBjb21tb24gcHJvcGVydGllcywgc3VjaCBhcyBpbmRleGluZyBhbmQgc2xpY2luZyBvcGVyYXRpb25zLiBUaGV5IGFyZSB0d28gZXhhbXBsZXMgb2Ygc2VxdWVuY2UgZGF0YSB0eXBlcyAoc2VlIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgbGlzdCwgdHVwbGUsIHJhbmdlKS4gU2luY2UgUHl0aG9uIGlzIGFuIGV2b2x2aW5nIGxhbmd1YWdlLCBvdGhlciBzZXF1ZW5jZSBkYXRhIHR5cGVzIG1heSBiZSBhZGRlZC4gVGhlcmUgaXMgYWxzbyBhbm90aGVyIHN0YW5kYXJkIHNlcXVlbmNlIGRhdGEgdHlwZTogdGhlIHR1cGxlLlxcblxcbkEgdHVwbGUgY29uc2lzdHMgb2YgYSBudW1iZXIgb2YgdmFsdWVzIHNlcGFyYXRlZCBieSBjb21tYXMsIGZvciBpbnN0YW5jZTpcXG4uLi5cXG4+Pj4gIyBUdXBsZXMgYXJlIGltbXV0YWJsZTpcXG4uLi4gdFswXSA9IDg4ODg4XFxuVHJhY2ViYWNrIChtb3N0IHJlY2VudCBjYWxsIGxhc3QpOlxcbiAgRmlsZSBcXFwiPHN0ZGluPlxcXCIsIGxpbmUgMSwgaW4gPG1vZHVsZT5cXG5UeXBlRXJyb3I6ICd0dXBsZScgb2JqZWN0IGRvZXMgbm90IHN1cHBvcnQgaXRlbSBhc3NpZ25tZW50XFxuPj4+ICMgYnV0IHRoZXkgY2FuIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzOlxcbi4uLiB2ID0gKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbj4+PiB2XFxuKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbi4uLlxcbkFzIHlvdSBzZWUsIG9uIG91dHB1dCB0dXBsZXMgYXJlIGFsd2F5cyBlbmNsb3NlZCBpbiBwYXJlbnRoZXNlcywgc28gdGhhdCBuZXN0ZWQgdHVwbGVzIGFyZSBpbnRlcnByZXRlZCBjb3JyZWN0bHk7IHRoZXkgbWF5IGJlIGlucHV0IHdpdGggb3Igd2l0aG91dCBzdXJyb3VuZGluZyBwYXJlbnRoZXNlcywgYWx0aG91Z2ggb2Z0ZW4gcGFyZW50aGVzZXMgYXJlIG5lY2Vzc2FyeSBhbnl3YXkgKGlmIHRoZSB0dXBsZSBpcyBwYXJ0IG9mIGEgbGFyZ2VyIGV4cHJlc3Npb24pLiBJdCBpcyBub3QgcG9zc2libGUgdG8gYXNzaWduIHRvIHRoZSBpbmRpdmlkdWFsIGl0ZW1zIG9mIGEgdHVwbGUsIGhvd2V2ZXIgaXQgaXMgcG9zc2libGUgdG8gY3JlYXRlIHR1cGxlcyB3aGljaCBjb250YWluIG11dGFibGUgb2JqZWN0cywgc3VjaCBhcyBsaXN0cy5cXG5cXG5UaG91Z2ggdHVwbGVzIG1heSBzZWVtIHNpbWlsYXIgdG8gbGlzdHMsIHRoZXkgYXJlIG9mdGVuIHVzZWQgaW4gZGlmZmVyZW50IHNpdHVhdGlvbnMgYW5kIGZvciBkaWZmZXJlbnQgcHVycG9zZXMuIFR1cGxlcyBhcmUgaW1tdXRhYmxlLCBhbmQgdXN1YWxseSBjb250YWluIGEgaGV0ZXJvZ2VuZW91cyBzZXF1ZW5jZSBvZiBlbGVtZW50cyB0aGF0IGFyZSBhY2Nlc3NlZCB2aWEgdW5wYWNraW5nIChzZWUgbGF0ZXIgaW4gdGhpcyBzZWN0aW9uKSBvciBpbmRleGluZyAob3IgZXZlbiBieSBhdHRyaWJ1dGUgaW4gdGhlIGNhc2Ugb2YgYG5hbWVkdHVwbGVzYCkuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG4jIyA1LjQuXFxuLi4uXFxuLiBTZXQgb2JqZWN0c1xcbi4uLlxcbiMjIDUuNS4gRGljdGlvbmFyaWVzXFx1MDBiNlxcblxcbkFub3RoZXIgdXNlZnVsIGRhdGEgdHlwZSBidWlsdCBpbnRvIFB5dGhvbiBpcyB0aGUgZGljdGlvbmFyeSAoc2VlIE1hcHBpbmcgVHlwZXMgXFx1MjAxNCBkaWN0KS4gRGljdGlvbmFyaWVzIGFyZSBzb21ldGltZXMgZm91bmQgaW4gb3RoZXIgbGFuZ3VhZ2VzIGFzIFxcdTIwMWNhc3NvY2lhdGl2ZSBtZW1vcmllc1xcdTIwMWQgb3IgXFx1MjAxY2Fzc29jaWF0aXZlIGFycmF5c1xcdTIwMWQuIFVubGlrZSBzZXF1ZW5jZXMsIHdoaWNoIGFyZSBpbmRleGVkIGJ5IGEgcmFuZ2Ugb2YgbnVtYmVycywgZGljdGlvbmFyaWVzIGFyZSBpbmRleGVkIGJ5IGtleXMsIHdoaWNoIGNhbiBiZSBhbnkgaW1tdXRhYmxlIHR5cGU7IHN0cmluZ3MgYW5kIG51bWJlcnMgY2FuIGFsd2F5cyBiZSBrZXlzLiBUdXBsZXMgY2FuIGJlIHVzZWQgYXMga2V5cyBpZiB0aGV5IGNvbnRhaW4gb25seSBzdHJpbmdzLCBudW1iZXJzLCBvciB0dXBsZXM7IGlmIGEgdHVwbGUgY29udGFpbnMgYW55IG11dGFibGUgb2JqZWN0IGVpdGhlciBkaXJlY3RseSBvciBpbmRpcmVjdGx5LCBpdCBjYW5ub3QgYmUgdXNlZCBhcyBhIGtleS4gWW91IGNhblxcdTIwMTl0IHVzZSBsaXN0cyBhcyBrZXlzLCBzaW5jZSBsaXN0cyBjYW4gYmUgbW9kaWZpZWQgaW4gcGxhY2UgdXNpbmcgaW5kZXggYXNzaWdubWVudHMsIHNsaWNlIGFzc2lnbm1lbnRzLCBvciBtZXRob2RzIGxpa2UgYGFwcGVuZCgpYCBhbmQgYGV4dGVuZCgpYC5cIl19XSIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ3ZWJfc2VhcmNoIiwiaWQiOiI2ZTUxMjVhYy0zMTY0LTRhNzYtOWM1Mi1hYzM0NjBjZTM2ZTQiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJzdGF0dXMiOiJzdWNjZXNzIn0seyJjb250ZW50IjpbeyJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3NrZXB0aWMiLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QTZGNGNDTjV5aEQ0Q3JzOU0iLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjB9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InJlc2VhcmNoZXIiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtM2NkOC03OWYxLTg4YTEtY2U2Y2VlMjk0ZTc5LTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoidHJhbnNmZXJfdG9fc2tlcHRpYyIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsInRvdGFsX3Rva2VucyI6NzQxMSwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fSx7ImNvbnRlbnQiOiJTdWNjZXNzZnVsbHkgdHJhbnNmZXJyZWQgdG8gc2tlcHRpYyIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ0cmFuc2Zlcl90b19za2VwdGljIiwiaWQiOiIzODkwNThkMS0wODNmLTQ2MzItYmJjNS0wOWJmZDNhMGZmZDIiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMVhqajNhUmRXeWhCSmZQbVRKQnhRYTkiLCJzdGF0dXMiOiJzdWNjZXNzIn0seyJjb250ZW50IjpbeyJzaWduYXR1cmUiOiJDQVFTMVFnS0VBZ1NHQUk0QVVJSWRHaHBibXRwYm1jU0RONlRmVTZORE05NzFsRWJ6eG9NdlJVQTFKOW94M2ZKUVlUOUlqQXJDUVFHSEJQNHBSSFBhdlh6UmxiTWJhWUhDNTdZaitHYnhncjBoZWx2ZDJPSUwvbzZSYnQ0UEZBM0ZhSUZVbGdxOGdjN0tRSTlGdEdudlNBa0xCUnpJc1VwLzJkNVJkMnpnRXNma0ZWZVhzbjhXWVlaa2luQVFWVnk2QTdCM0xhRUpXMHhiL3g4OERpNjdmZWR3eDBRK0ZsWnR3Y21UM0RjYWExMUp2ZXlIUnIzZG0rekdTWlA2Q1hlWE5KOG5yVFo3VzFZUlR5c0Q2dzdvN0xwOGgxUndxNGY0bzhqK1FVZlkwdmplK3pKWnN4SFBtUU50M3lrZUtoOTloZ242ZGJpejYvRUhxbWJIalNBdTFNNFNxRURmcUlLVGhIWWVuYW1BWm1jUTRYQU9vNm1kUTYrNzdWZ3BWMWxDMS9rNTJJaVVvSmprR2xRcm5MQ3Z1dGlJN0tOK3NnSzhjMisyS0NKclBSQ3ZWVitCa1N0dlA1aldXcXp5dHBLUStuSHpoTDQ0OE9ySUE5Z2RKQzM5QzhlRHJYZVJuSTd0NEZxbGtYR29NbFpBVjlVTWNyUUhSeE9NaTh0T3lUemk5b3lCRkRUU3NRK0xKcWVyMHR6MUZ0OWdlazZ2emc1MFNwVkFGUWRMRVJ2OFIxWXFNYWVJK3hzdlRBRVc5TVJndlJNa3pMdXpSVDJndXczakQrRmppcDM3eGdLNVFJMFIrQ3NPQW1RUG9UWml6emNJeUFVQXVDSkZDZ3EvZ0QvQ2liTHhveW1pVlVJMWFuOVNudDNLcE5Sb1dkdldJbmUzM1YwcHY4UEhnOXlXMkp1TE9xUUdpbExDc0svUTNvUXIzdXBFTVh4WEdqQ0w3V1VpenZSWkdGRUVWK1ZwbFl0N3Y0dEtaTWo1cGszdmRlTTBNSjVYWmc3UHZVc1J6eWN4Y25yVDNLbm9jM3FNK3R0cVpmVHNiOGQ2VS9wUWlmcTdHdWFoRk9IcnZyb3FnSkxqKy84MVl5MXBaRmtqZkpnWTBYa2x6QW5BNkNGcS9ZVDBneGJWK1RINUlTK1dhWU9VekszNlM4czVxcU13aG5ndjRJekUzY3RYWHFuaElaWE56bnlWQnFiWWEvR3gyYkNqY0VqMm41UnlHWVJTLytNY3VsZ0xCem5ROU1rRzJEN1FQM01xL253SFhLTUlhTFM4Vks4aEczZDByQWpsdm5hdVhTaE5ySnh0a2o0cWt3bnhENExyRWprMXVlK1BMY2JMcVlXd0FyOG4wcys0MWpyc0lQVnl5Szl2RmFFWnJBOUthQnhrSnlpcXg1MEdaTm43Y2t4K21rSTE4Z0g1Mml5VlB0VThGS01YanVnQ2w2bVBMaUhZaURRZCtHNjNMdWRtT0t5czlpdFZuQkRCTk1QMmIvSTcrYklWUmxoZUdhWldyNk1BVEtsODZCM3krMGt1T3JMUk15V2NkaWJjMmlRa1ptKzhmN2RUVXdiS2JCSnNLT0ZZNzMvOG5sTC9KaVlwano2blVJaytJcVNqYUZ5SzZYRmxDY3VrV3BXSHBESm5TdmJFemV5N3FIeUNxSitJK29uOGlQOEdPdGVtcFFhUzAvNGtaR1N3bHJFNnhPajZFd0dwZmpiRFpLSGtQLzJSQWROWVBRcTN6N2JkVkpNSk5uRk16NmUzN216TXBDQmlod1BkU0ZUUkUzU0NZNjZ3dlRnSnpHQ1V5aEhiU2k1aTFhenpPWGQ4dUhDU3BULzZMMW82VlUwcGpvUjhLYWE0d0NXSEdmN1AvV0VkK2F2eXAwMkpudTFjbXBXMCtHcSt2SzloMHRkYjJJQ2xNL2VOZmRvdVFQK2VwOE1KVWpyYURSSUNtQUpRWTVvYlM3MXZuaU9vK3JlYUZ5MHpSZ0IiLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsic2lnbmF0dXJlIjoiQ0FRUy9SRUtFUWdTR0FJNEFVSUpibUZ5Y21GMGFXOXVFZ3dGaDNBVkpaUDNzOHkxSHEwYURHNzZ1aEhPTmFTY1hBR1NoaUl3R2dqTFhIVXgxeVpya0xQbGpqelRPUXljcExKaUtWd1hjeG0vRXpLSm0xczhNNVd4dVJFV0VsZFhjb2FOMC9tc0twa1JNVCtlTGI1aWg5ckFIVUxUb21VTnNVd0pldTBqckplNEdMYXp5N3NQNUt0akVPRUJLUXNYVWZvUTdmRGtaT1FVSFppOXp6UU40aEZObVlKVWYzT1ZxZjlUUU5kQzErbS8vSEFaUGZXbUF5Nkh0RHZtN2tnQkprQ0VvaUZubTd0bi9QWDFoWEdQUmdXYzZCVFhVTXIyV0E4YUt5cWdnUlViTUgyK0pZdjVpV1pkZENjMXVwMU5icjNzRjdXUGNvdDlaQmh5SFFYcjY5MnplSms2U0pneWdJVnJBU2JNbHkyYkJ4VFdKQjYyM2dWZ3pwQ21WU0xaRnBrS1hnUGNwOGhHeURLT3ZGNlNpUVNIcUdSeFFvZnI3VnMxYVBoZlppeFJPSkdEK01scmtnZ1ZBbkdNeVlJRFVacGF2dENlaVUxSm1CU3owMDg0YlhQdFQvdG5wc0tPblNQWVpRbzNCMEJYS2JxQS9Xc0tuZzgvVjNaOFV1YWpvNklWWjJoZ0ExV0RkdXFRT1FrRXhHVTZwRnVMaGViSWp1V3hTTkkvRGZmdlIvL0xOcUNmdmRuZjBodm9QdER4ZngvU2F3SXg0L3NwcTl4YndGekh0TGdvSGcxRjJ3Rnc3Y2xQRGxnMVB5RC92MnlKRzNWOVQ4R0w2akdQRTZyaGZpbC91NEhOdmY3QlcwZFF6Q1RUWGNxWnk0bkJ5eTArcCtTN1p4Zk1sNEtweGEyZ09oaFNOamZwZnVYMFFwTDRrTUU4SjFubGlGTjFkOEtxRVB1REkxOEJRSFVoRC9VZlkyOW9BMHg1MTBGak0vQnNMWjRuNXNwcGtvSWk3Q0ZvUXEyYUFLVk5mQzFLaWxoSXBSRCsxZHlEdFpRa2xOOGdacE9FU0h3Nk1MMVR0VTB5ZkFjYm1xb3hKNmhoanp3SDZDTXNUUUVFeThNYlJCTStRS0trSVJkUkNQSEdGVzJMa3VSNTArSjBwRi90cjZwcVZ2SmVvemZpTW5yRWlsQjI3VFpQNG5LVEpHOHJkLy92Q1dlbjB5N0NBLzhkQko5Z2RwNXR4N05WQTQ1azFnZGN4RVIxZ1lvdU5YM24xdkczL0IxRjQrOTN3WFc5TXFvNlVzNTlUUW1yYjlOcWE3cFNRTlNySDA2dzFHeHlCQWxrRmZxQWxhZmsxNmFNcHE0MTZUSllwR3k0M1dDVWlDY0xXbGhpV2d6MnNnT0xBWFA5YTV3ZVdBS1JDOEo2amtjVWtEZVhuVytIRlNpUE02KzlCbzZrVmhMVWhONXJjNG1IWk9acEY3cHRQcHNRTjRYQ3FMUHNxcFJILzhvNVhweUM2Wm5lVGVGMkV4dGpod1pWalhEd01iSXpWWUlidzF5MlBQR3c4ZEM3cGM0c0lWU25JNG9RQUgwdjY1MGdoZmhtTWV2V1c0aDRQeWxidy9ERzRmZGVUMys4ejBmOE1PU1R0ZmlweDA1L1F0dEhsQXZPRFJ4YTV5REN2T3o2bjNmMUJPME4vNE1iR3lPTTI3cVc0eDhtQlZvcnlVWldmc21oZmkwL0dmTnZCOCt5bFRHYllOWDI3T2NFRjVXWnROcWJXRXk3VjVHYm1aVVN4c3BjZzdGQlozajMvM0w3bXV2WlhvWmkxZjVlcnE0eTNsU0grb0Y3cVBGbnZpZXlMT0N3dWJjUXkrb3lvd2o0Y0QwaWRSZWNucCtzKzUyRDcrbXpGMWdwVytmK2NKYXNveGlnYnhrT2xvUzR5ZWlLdmdNNG5XU3BHRWoxMlJQeDVvODBFbXppWE45dDR6SnpXMWh0QTVoWEpKZUZ5bXBNWXR5Q1VDZXZSV1ZtclJZa3RRN1VuQ2E2dEwvQVZQaTFuaTVYQ1hIZkxPdXFmSzVLTFl4andWdGRoaGZtQ0x2cGFLVk5DRU9DZjRuTkdIcUQ5OHdVYjBlaldOd0RZQm5KR2tCUnc5UlZzcGZ3TmRvV29WY24ycXY4eXdHVXA3eWMvc2syTHBJNlZ6aVJ5dldxYkdORzFaSnFWVnV0UmM0NitzT0U0VlNlUzIvQ1ROS05hNk5NQ2YvaHh4YVVWc1lmS2RQK1MxTGM0aWFuS3Awc1V1Z1BwbXRveWRDMENQMlJ3am5ITGNuenhDbnZxeGxMUVdYTU5QVHN1MnlCY0hsV0pMdGNtZGcvTWNMYWd5N2w1a0c2c043WnhKTEQva0F4OEU2dXFvaFArSy9qTVNyWXVRSm5ocjR0ZjRiaVdGWTNKemYvS3BBWXlWU3ErcGJmRktYTWJMOW5IRW9aUVQxNVd5YWJkNkRNQXEzUVpPdGhNNXBVemROcVVYa2NySTFQd2pBU0FnNGRWVS8ybys5OG1UWG55aFA3VmxFa1RuZGRwd3lKRitZQ0NjMDFwcWE2Wlg5OXBuUitZT3BJcmI1RHRQY0txMHQ1Y0ZDRHlReE4xbFV1cVpjWXVqdlgvR0M3KzZJcFJUei9Oa0VZLzVjbnRPZlpKeWQ2U2tVeC9KUjVBSklSUENOZWliMHJjV25uRm4wWEs4czIyeDQwd0xqRXR1ZlhETzkreXJhZjRrSHhsQkkzU0IwbWVNamVwWDUwQ0poQXg3TmVra1JGbndvZWNMZnFJeXJqWHJnQVpzVE1QaVg5NHNuamRyemlzQVh0QkthbnVsaWxTeE9mVzJIU21mbmswTDJHWEhKSjNTK3ZDYmdSRW1sOWRla2ZtSGhtd0VZdHYza2ZodGg3RVJJd1ZUOGo2OU90SUJDbWVUSUp1QTdvNzNQWGMxTEoyd2h1Wks3RlplNk5UT3M2MU1QN0pWbEljNnRiaHRPQmszTlhVOHBCUE9DWTZRNTVkNjNGdHBVVS9LQ2JRMEsyV1E5bzVqTkd3Tk5KNkN3NUZEQ3QreDVZSmVQMWQyUXpEU2lxK2NTWnF3cUNaVmNOb0FmdEdHemdyUlo0dmtwYlAweFFaSEN0V3NVd0hWVGpyWTROZlJEakhkVlBSbjhKOVh0b29jL2hJV2txekZhY3pZVGxhcHUyUkNKcWU4YUVvM2p6V1pDRUo3QWlCekJZcmFZRGdPZmVpUXllSlVzYmJYSGRHS2lpdzIyQmRwNE0rWXo2NzFNWjBpZTlFV1JrczBRMkRIR1VUbmU5UzR0QUFKUEdiVWY0eHRDTW9KNDBRNFFXMDVRTFNaeTN3TkZBVUZZbVhFNEVDREZCOCtJRWVORkZGL3RnQW5maDhHdkZoLzF4WStMRStvek1taW5qU1J3aHEwOXpRQUVrTkZ5dTBJS2d2RGt5T0kraWNjRjdkb1dMVFVhc1B4TEgxeEdQQlNLa2VXQ25Tb1gvNEMyYzlkdTY2QmJSWGFtSHFhZStWQ2lTNkJOOTUzaVlLT3U4ck1rbmVXK1RGalYwQ3pBZjhQTkc5TEJheVVnWTVVdzl4MFNhcDZ6STRQcGFaS0g0ZDlrdGtGVSs5MXlCYk41U1J0ZGQ4L0VRYXRKWG1CTmZ3N2tScmxsK0xqd1p3ZVdMRWIwalQxUnBHdURxSExkTFRNTWdvd1NjenJwMnpDc0pkUUgvc2VhWXJwd2tDRlI4ZFJqTTdKUlVrVmExQTBTdmRaQVg3NXZ1QnZ2TEJ6TjJjK1JidDVvcXk5M2xxSUJJaktiN3cyVWtSVGI0cEJXY05LTlBVZ3QzYXovUHhReEVhc2FsdGRtdXl2c1hZYnVyM1ZQTm44SnpOLzZKNU4rUUNuejU3MVRjY2FwQ21rUHZGRStnYXlYVStVRjFWS1l5RmNVdmxldjdsc1FsbWpxeksyUUE4WDdRM1R6RmFwcWh2SktsdDdkekJIcmFxWkJnUGdiOU40b0tOYXlpOEZaVDcxK2thWWhJaEZyRVNLWVJIZkhrajVuM2tmaGdMeSt4K2pMRGJLeE1nZ2tnSG5KejlSdUVIWlhCV3RrWUFRPT0iLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsiaWQiOiJ0b29sdV8wMUg2ZlY4SnZSSk52WHk0N2VSbmQzakgiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6e30sIm5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHdBTVJkUzIxRmVKZnpRaE0zcyIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo3Mzk2LCJvdXRwdXRfdG9rZW5zIjo3NzAsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjczOX0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoic2tlcHRpYyIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy00YWU5LTc2YzAtODlhNi00ZjBiMTE0ZTMwMzAtMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzM5Niwib3V0cHV0X3Rva2VucyI6NzcwLCJ0b3RhbF90b2tlbnMiOjgxNjYsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjczOX19fSx7ImNvbnRlbnQiOiJTdWNjZXNzZnVsbHkgdHJhbnNmZXJyZWQgdG8gdmVyaWZpZXIiLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7fSwidHlwZSI6InRvb2wiLCJuYW1lIjoidHJhbnNmZXJfdG9fdmVyaWZpZXIiLCJpZCI6ImMwNjQ1NjNhLTk3NGYtNGYwZi1hYjRhLTA4MGU2M2MwMWNmNSIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsInN0YXR1cyI6InN1Y2Nlc3MifSx7ImNvbnRlbnQiOlt7InNpZ25hdHVyZSI6IkNBUVM5Z1VLRUFnU0dBSTRBVUlJZEdocGJtdHBibWNTREdGeUoxTVhvWStBaTZOa2J4b01SZDJUclVmVVU3V3JQcEEySWpEZ3k4ZFVwbGNaS2M3RG5Kd05MVEJmSDVVTlIrbkduYWtTUFFpYThVVkl6cmprT25KMmZMcldVc0Eyb0VPOHlSc3Frd1hra0RDc3loQjk3ZHdqNTdSTXQ5MVh6Z1doTDlsdU5FR085MnlXMHJORkdvTVNVVk94UElyRmM1Ump5cmhLRWMzKzFSbnB4Q2t5bHErTE5sUTVORnhMbjZ2MmNGNUR6cXB2QkhOUjdYQWlnK3VySklVM2dYeWRDa2FHWlpEZXRKdFFPVU9YZ1ppOXo5MTAwTnBkQUVrWERvMklDSENHVEdwVmMwSi9wYlh4V1JCeGI2QjEvR2wwWWpuNjhWNkhsZXNIN0lvQzdHUi8zSHdSdTlOU0RCRzR0dGZRakVXY2crUGVBZWE0Um43anp4K1RtOUg3TDFqb1RLVmdEYlFnNWlLTUxmanZQcWxua3NDTHRYT3Q4alNJUzVPWUxON2EvN1RYNi93NWQzNHZudzh2UERtVTFVWWkwUTcycVVXaFVhMnZ3OE9YTnBjS0w3QnAwbE5mQlM4cnZIWSt2Qlk4L2hhQW9UTDN3dkxGVTFmVCs0ei92QTNCY3NpZ1pEWWxmSmVEWWN4WG1SdXF4enhFR3BYQ1JlOXMxSFd0SUQrNDFwSHU2bHBaSGtYYWdocGM2VFpITHhOd2NWaFRiNmZNUTdRbysyTDZQdmtsaDFtMzJwZW9keGVnMllpUGhkcGwzdy94a09FdTUvSkVGN1pCeHBkVUwrdTN3a081cFVsc3FmZ1g3dWh6djdIQzJXcFNXVmltcndpUXdRT1V4MCtKUHJMdHcrbFlmcTdJM2pydW8wR1cwZ3VXdm51ZjdKTTNZTGo4NHErSmdIb21uU1Nabm5abXJMMFVhUnZyVnlMbzg3N0YzOWNjTFM3WG9abm51aThKNFA1OFpUbWlMa0VKZUdjeDFJdWFnMmUzMzMzOU83WWV3LzU5dExiUXRJR2g5VldNd0hCbkZ4MzlTOC80TEVGcks1SGxkUFFoT3Jsb3lUdGtSczY2aVFSS3R4TUtTMFZJNlFUS1pEQWJzN2E3SDF5QnYrbUppOVhnNHVLbXlVNnFubjd6RUtqalkyYklsQ3dleng0TExHUDVIRDNacjZmWnNtZEcxVUh6RHNCdllHMm9DSTBvcUVLTSt2YWxzVDZoVThNUmpiUUJEL0pKOEpKTi9pQkdsaEJBVUhvMFVtaGh0UjJldVRWcFpXbE1uaG1wWEFkS0RYU2ZQZGFQb2hnQiIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJpZCI6InRvb2x1XzAxWEVBdWhRSENCZHlDd3R5bVp3RmZEaSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3JlZF90ZWFtIiwidHlwZSI6InRvb2xfdXNlIiwidG9vbHNldF9uYW1lIjpudWxsfV0sImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2Zkd0FzYWJEcXVlZ0hpNllVcENuIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjc1MDksIm91dHB1dF90b2tlbnMiOjg4LCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjo1NX0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoidmVyaWZpZXIiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtNjY4Mi03ZDIxLThjMTUtZmZiY2U5NDIyYzlmLTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoidHJhbnNmZXJfdG9fcmVkX3RlYW0iLCJhcmdzIjp7fSwiaWQiOiJ0b29sdV8wMVhFQXVoUUhDQmR5Q3d0eW1ad0ZmRGkiLCJ0eXBlIjoidG9vbF9jYWxsIn1dLCJpbnZhbGlkX3Rvb2xfY2FsbHMiOltdLCJ1c2FnZV9tZXRhZGF0YSI6eyJpbnB1dF90b2tlbnMiOjc1MDksIm91dHB1dF90b2tlbnMiOjg4LCJ0b3RhbF90b2tlbnMiOjc1OTcsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjU1fX19LHsiY29udGVudCI6IlN1Y2Nlc3NmdWxseSB0cmFuc2ZlcnJlZCB0byByZWRfdGVhbSIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ0cmFuc2Zlcl90b19yZWRfdGVhbSIsImlkIjoiOTgyYzAxMjYtZTZhYi00NThhLTkyMmUtN2IyNjZlODNjMmIyIiwidG9vbF9jYWxsX2lkIjoidG9vbHVfMDFYRUF1aFFIQ0JkeUN3dHltWndGZkRpIiwic3RhdHVzIjoic3VjY2VzcyJ9XX0=" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJvdXRwdXQiOlt7ImdyYXBoIjpudWxsLCJ1cGRhdGUiOnsibWVzc2FnZXMiOlt7ImNvbnRlbnQiOlt7InNpZ25hdHVyZSI6IkNBUVN3Z2NLRUFnU0dBSTRBVUlJZEdocGJtdHBibWNTRE9SWGdlOXVMMTgzMlJNUXdCb01qUXdBd0FIYjlpUDFvalBHSWpERFNjYWw0ODZiZUNKYnRHOC9EMlk5ZzlTNXhJUGZqb0hnL2R4d1g1SHlwU1FFQmJqRmlUU2JQRUNYU0Z6VjZZSXEzd2JMY215UGJ2dXNHbkRleEZneCszc0lYenZxeUZWLzY5eGhpaE1uU3BmVENIeTArMHpTdTFXcFlVL1dVcElmb2FTWFRjVGExcWtNOHg4RWsveS8xamtLTVpVZkRGYTkzdWhVSnhPQzQrUWdQV0JsLytYeTJ0ODJMMngvZWJxN1FMUVRxdFIwalhlYVVSUW0zQ282SnZPZ1dtRHZNcEVkVGVUV0UzZkRoMXR6elZLc2ZsdW00ZDg5YjZDYUt0bjhubFFkN1JoTjk5dk5RTVFIRzVWNE91Vk5kckhQWlpwRk91aGRlanNNRldYeUIrNTVzbEdBZDh3RVhnRktiSS9rbk9YVkd6TC9OREY4R3R5MXROWmx1MmZkU0NBc1lXTXNGUVAxY2J3TGVRR3N6eVh6Sk1WWkoySTl4QlBreXoxSE9YQ1B5aDVGK2oyZ1JUUGlrVkIzQVJDNnRmQi9sRFNVZUtCVlpQUVM4cFkvQjRlQ3MrSWpqYUJOV3J1THpmcUNzYzZ4TDBDdVFQU1l5UVhhWUZqWXJTUk5jQzcwV1FCV1Q1VlZocDBtQTl5WkRHWWJSWnQ5STVPSzlRa1FtdUo1ekRGYU1FT1h6U3JzQzRsdGVQNk1kaW1pTWlqdnNIM2lxK2M2V1VZb2xSajNuZUk4OEVhSEl6TEFHUnE2NEJOQ0x6ZEFZTDJwRVUyc2I1dUc3OFBLVm9VVUNiVkpMNWpwRE9oTE9UTzFiUXNUdW9VcG5SUnJMUUQva0l6VitsanBWQnd3ZkpjeXE3NVRKQS9DdFByL3hyMlNaOUIxYjJzYmpHNmpEMGxodG9DZGRlRGdjcWdsL0xTU2tjdWFZWTh4UFdDeStMdE5oY3FUaGZtZnZuSExKSXBMNmdqYmpXL09RcFJmVlV3Zncxak9iOWJXVndjZ0hoTllmS1lHUHRNOG90aDV4d0xZUlJWZ3pxTUtPdnM3ZmFsWFJoVnpaMlNQNkl3MEJNbUFBWjZIamZDbDlMK3RQcUpObG5RUU9IcG9XanVtNEIrWHNZbGRGRUtHUVdRcXNkNTlpTlB2YW85cC9zQ090N3JHcUNvemNtZ2lFbTR3NjZlei9RSUR5YjQ2bjArdGNOSlBVY29CS3ZRL2pEd0U2Vndta1EvbkVDakZwM1VkRFQra1dZUVNOZDhqOFZCWk9wRzhMbHFVWXJFWkN2MHE3QnMxdmNOcmRZaWFGb1MzMm9zYXluV2srSHQzT0h1RldiRUY4SmxzRlRRQU8wNVZ2WWtlY2YwVi9vVEhzRzVoeU9PczdVRWFERnE3dWFKdXpXVmhoNXBBb2pJYUJjUjhmVmxuMlBMUlRYN2lzMm5zQStRcmRpZGVQY1p0RnRneEFpS1llMWkyc1NGaTR2bThFT2tDYlE2M3dkUWVZNFVycjVQdW9NRXd4VVVMamdLYWdpQW1zSkRxNFJyVm03c080KzZZdUE5ajZsMnV5bGtPRWM2N3ZmLzVma1h1Q3NpQXBOTDZrZWI4Yyt2TDBuNEZYR2R6aithcFNEZkZHQmdCIiwidGhpbmtpbmciOiIiLCJ0eXBlIjoidGhpbmtpbmcifSx7ImlkIjoidG9vbHVfMDFWQnJXZFdFV1lnaXQ0a05ZZWhCTVU1IiwiY2FsbGVyIjp7InR5cGUiOiJkaXJlY3QifSwiaW5wdXQiOnt9LCJuYW1lIjoidHJhbnNmZXJfdG9fZWRpdG9yIiwidHlwZSI6InRvb2xfdXNlIiwidG9vbHNldF9uYW1lIjpudWxsfV0sImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2Zkd0F6ZmpxVW9wb0VtWkxhcW0xIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjc0MTYsIm91dHB1dF90b2tlbnMiOjE1MSwib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6MTE5fSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCIsIm1vZGVsX3Byb3ZpZGVyIjoiYW50aHJvcGljIn0sInR5cGUiOiJhaSIsIm5hbWUiOiJyZWRfdGVhbSIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy02Y2U2LTcyNTEtYjc5Ni00ZjY0MzMzZjY3YzYtMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ0cmFuc2Zlcl90b19lZGl0b3IiLCJhcmdzIjp7fSwiaWQiOiJ0b29sdV8wMVZCcldkV0VXWWdpdDRrTlllaEJNVTUiLCJ0eXBlIjoidG9vbF9jYWxsIn1dLCJpbnZhbGlkX3Rvb2xfY2FsbHMiOltdLCJ1c2FnZV9tZXRhZGF0YSI6eyJpbnB1dF90b2tlbnMiOjc0MTYsIm91dHB1dF90b2tlbnMiOjE1MSwidG90YWxfdG9rZW5zIjo3NTY3LCJpbnB1dF90b2tlbl9kZXRhaWxzIjp7ImNhY2hlX3JlYWQiOjAsImNhY2hlX2NyZWF0aW9uIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VuX2RldGFpbHMiOnsicmVhc29uaW5nIjoxMTl9fX1dfSwicmVzdW1lIjpudWxsLCJnb3RvIjpbXX1dfQ==" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "028f466e9c7e011c", + "parentSpanId": "1723d5322674ec35", + "name": "FilesystemMiddleware.wrap_model_call", + "kind": 1, + "startTimeUnixNano": "1790968032482876160", + "endTimeUnixNano": "1790968035029612032", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "e30=" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "FilesystemMiddleware.wrap_model_call" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "1" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "red_team:15ccef64-4162-9e02-e87c-4cb0eb6ea16b|model:04f09f43-f066-c110-818d-1a33d3ba1569" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "red_team" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "red_team:15ccef64-4162-9e02-e87c-4cb0eb6ea16b" + } + }, + { + "key": "langsmith.metadata.ls_method", + "value": { + "stringValue": "traceable" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJvdXRwdXQiOnsicmVzdWx0IjpbeyJjb250ZW50IjpbeyJzaWduYXR1cmUiOiJDQVFTd2djS0VBZ1NHQUk0QVVJSWRHaHBibXRwYm1jU0RPUlhnZTl1TDE4MzJSTVF3Qm9NalF3QXdBSGI5aVAxb2pQR0lqRERTY2FsNDg2YmVDSmJ0RzgvRDJZOWc5UzV4SVBmam9IZy9keHdYNUh5cFNRRUJiakZpVFNiUEVDWFNGelY2WUlxM3diTGNteVBidnVzR25EZXhGZ3grM3NJWHp2cXlGVi82OXhoaWhNblNwZlRDSHkwKzB6U3UxV3BZVS9XVXBJZm9hU1hUY1RhMXFrTTh4OEVrL3kvMWprS01aVWZERmE5M3VoVUp4T0M0K1FnUFdCbC8rWHkydDgyTDJ4L2VicTdRTFFUcXRSMGpYZWFVUlFtM0NvNkp2T2dXbUR2TXBFZFRlVFdFM2ZEaDF0enpWS3NmbHVtNGQ4OWI2Q2FLdG44bmxRZDdSaE45OXZOUU1RSEc1VjRPdVZOZHJIUFpacEZPdWhkZWpzTUZXWHlCKzU1c2xHQWQ4d0VYZ0ZLYkkva25PWFZHekwvTkRGOEd0eTF0TlpsdTJmZFNDQXNZV01zRlFQMWNid0xlUUdzenlYekpNVlpKMkk5eEJQa3l6MUhPWENQeWg1RitqMmdSVFBpa1ZCM0FSQzZ0ZkIvbERTVWVLQlZaUFFTOHBZL0I0ZUNzK0lqamFCTldydUx6ZnFDc2M2eEwwQ3VRUFNZeVFYYVlGallyU1JOY0M3MFdRQldUNVZWaHAwbUE5eVpER1liUlp0OUk1T0s5UWtRbXVKNXpERmFNRU9YelNyc0M0bHRlUDZNZGltaU1panZzSDNpcStjNldVWW9sUmozbmVJODhFYUhJekxBR1JxNjRCTkNMemRBWUwycEVVMnNiNXVHNzhQS1ZvVVVDYlZKTDVqcERPaExPVE8xYlFzVHVvVXBuUlJyTFFEL2tJelYrbGpwVkJ3d2ZKY3lxNzVUSkEvQ3RQci94cjJTWjlCMWIyc2JqRzZqRDBsaHRvQ2RkZURnY3FnbC9MU1NrY3VhWVk4eFBXQ3krTHROaGNxVGhmbWZ2bkhMSklwTDZnamJqVy9PUXBSZlZVd2Z3MWpPYjliV1Z3Y2dIaE5ZZktZR1B0TThvdGg1eHdMWVJSVmd6cU1LT3ZzN2ZhbFhSaFZ6WjJTUDZJdzBCTW1BQVo2SGpmQ2w5TCt0UHFKTmxuUVFPSHBvV2p1bTRCK1hzWWxkRkVLR1FXUXFzZDU5aU5QdmFvOXAvc0NPdDdyR3FDb3pjbWdpRW00dzY2ZXovUUlEeWI0Nm4wK3RjTkpQVWNvQkt2US9qRHdFNlZ3bWtRL25FQ2pGcDNVZERUK2tXWVFTTmQ4ajhWQlpPcEc4TGxxVVlyRVpDdjBxN0JzMXZjTnJkWWlhRm9TMzJvc2F5bldrK0h0M09IdUZXYkVGOEpsc0ZUUUFPMDVWdllrZWNmMFYvb1RIc0c1aHlPT3M3VUVhREZxN3VhSnV6V1ZoaDVwQW9qSWFCY1I4ZlZsbjJQTFJUWDdpczJuc0ErUXJkaWRlUGNadEZ0Z3hBaUtZZTFpMnNTRmk0dm04RU9rQ2JRNjN3ZFFlWTRVcnI1UHVvTUV3eFVVTGpnS2FnaUFtc0pEcTRSclZtN3NPNCs2WXVBOWo2bDJ1eWxrT0VjNjd2Zi81ZmtYdUNzaUFwTkw2a2ViOGMrdkwwbjRGWEdkemorYXBTRGZGR0JnQiIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJpZCI6InRvb2x1XzAxVkJyV2RXRVdZZ2l0NGtOWWVoQk1VNSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX2VkaXRvciIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHdBemZqcVVvcG9FbVpMYXFtMSIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo3NDE2LCJvdXRwdXRfdG9rZW5zIjoxNTEsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjExOX0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoicmVkX3RlYW0iLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtNmNlNi03MjUxLWI3OTYtNGY2NDMzM2Y2N2M2LTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoidHJhbnNmZXJfdG9fZWRpdG9yIiwiYXJncyI6e30sImlkIjoidG9vbHVfMDFWQnJXZFdFV1lnaXQ0a05ZZWhCTVU1IiwidHlwZSI6InRvb2xfY2FsbCJ9XSwiaW52YWxpZF90b29sX2NhbGxzIjpbXSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjo3NDE2LCJvdXRwdXRfdG9rZW5zIjoxNTEsInRvdGFsX3Rva2VucyI6NzU2NywiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MTE5fX19XSwic3RydWN0dXJlZF9yZXNwb25zZSI6bnVsbH19" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "ee45871111076e7a", + "parentSpanId": "028f466e9c7e011c", + "name": "UnsupportedContentMiddleware.wrap_model_call", + "kind": 1, + "startTimeUnixNano": "1790968032483560192", + "endTimeUnixNano": "1790968035029467136", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "e30=" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "UnsupportedContentMiddleware.wrap_model_call" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "1" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "red_team:15ccef64-4162-9e02-e87c-4cb0eb6ea16b|model:04f09f43-f066-c110-818d-1a33d3ba1569" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "red_team" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "red_team:15ccef64-4162-9e02-e87c-4cb0eb6ea16b" + } + }, + { + "key": "langsmith.metadata.ls_method", + "value": { + "stringValue": "traceable" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJvdXRwdXQiOnsicmVzdWx0IjpbeyJjb250ZW50IjpbeyJzaWduYXR1cmUiOiJDQVFTd2djS0VBZ1NHQUk0QVVJSWRHaHBibXRwYm1jU0RPUlhnZTl1TDE4MzJSTVF3Qm9NalF3QXdBSGI5aVAxb2pQR0lqRERTY2FsNDg2YmVDSmJ0RzgvRDJZOWc5UzV4SVBmam9IZy9keHdYNUh5cFNRRUJiakZpVFNiUEVDWFNGelY2WUlxM3diTGNteVBidnVzR25EZXhGZ3grM3NJWHp2cXlGVi82OXhoaWhNblNwZlRDSHkwKzB6U3UxV3BZVS9XVXBJZm9hU1hUY1RhMXFrTTh4OEVrL3kvMWprS01aVWZERmE5M3VoVUp4T0M0K1FnUFdCbC8rWHkydDgyTDJ4L2VicTdRTFFUcXRSMGpYZWFVUlFtM0NvNkp2T2dXbUR2TXBFZFRlVFdFM2ZEaDF0enpWS3NmbHVtNGQ4OWI2Q2FLdG44bmxRZDdSaE45OXZOUU1RSEc1VjRPdVZOZHJIUFpacEZPdWhkZWpzTUZXWHlCKzU1c2xHQWQ4d0VYZ0ZLYkkva25PWFZHekwvTkRGOEd0eTF0TlpsdTJmZFNDQXNZV01zRlFQMWNid0xlUUdzenlYekpNVlpKMkk5eEJQa3l6MUhPWENQeWg1RitqMmdSVFBpa1ZCM0FSQzZ0ZkIvbERTVWVLQlZaUFFTOHBZL0I0ZUNzK0lqamFCTldydUx6ZnFDc2M2eEwwQ3VRUFNZeVFYYVlGallyU1JOY0M3MFdRQldUNVZWaHAwbUE5eVpER1liUlp0OUk1T0s5UWtRbXVKNXpERmFNRU9YelNyc0M0bHRlUDZNZGltaU1panZzSDNpcStjNldVWW9sUmozbmVJODhFYUhJekxBR1JxNjRCTkNMemRBWUwycEVVMnNiNXVHNzhQS1ZvVVVDYlZKTDVqcERPaExPVE8xYlFzVHVvVXBuUlJyTFFEL2tJelYrbGpwVkJ3d2ZKY3lxNzVUSkEvQ3RQci94cjJTWjlCMWIyc2JqRzZqRDBsaHRvQ2RkZURnY3FnbC9MU1NrY3VhWVk4eFBXQ3krTHROaGNxVGhmbWZ2bkhMSklwTDZnamJqVy9PUXBSZlZVd2Z3MWpPYjliV1Z3Y2dIaE5ZZktZR1B0TThvdGg1eHdMWVJSVmd6cU1LT3ZzN2ZhbFhSaFZ6WjJTUDZJdzBCTW1BQVo2SGpmQ2w5TCt0UHFKTmxuUVFPSHBvV2p1bTRCK1hzWWxkRkVLR1FXUXFzZDU5aU5QdmFvOXAvc0NPdDdyR3FDb3pjbWdpRW00dzY2ZXovUUlEeWI0Nm4wK3RjTkpQVWNvQkt2US9qRHdFNlZ3bWtRL25FQ2pGcDNVZERUK2tXWVFTTmQ4ajhWQlpPcEc4TGxxVVlyRVpDdjBxN0JzMXZjTnJkWWlhRm9TMzJvc2F5bldrK0h0M09IdUZXYkVGOEpsc0ZUUUFPMDVWdllrZWNmMFYvb1RIc0c1aHlPT3M3VUVhREZxN3VhSnV6V1ZoaDVwQW9qSWFCY1I4ZlZsbjJQTFJUWDdpczJuc0ErUXJkaWRlUGNadEZ0Z3hBaUtZZTFpMnNTRmk0dm04RU9rQ2JRNjN3ZFFlWTRVcnI1UHVvTUV3eFVVTGpnS2FnaUFtc0pEcTRSclZtN3NPNCs2WXVBOWo2bDJ1eWxrT0VjNjd2Zi81ZmtYdUNzaUFwTkw2a2ViOGMrdkwwbjRGWEdkemorYXBTRGZGR0JnQiIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJpZCI6InRvb2x1XzAxVkJyV2RXRVdZZ2l0NGtOWWVoQk1VNSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX2VkaXRvciIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHdBemZqcVVvcG9FbVpMYXFtMSIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo3NDE2LCJvdXRwdXRfdG9rZW5zIjoxNTEsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjExOX0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoicmVkX3RlYW0iLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtNmNlNi03MjUxLWI3OTYtNGY2NDMzM2Y2N2M2LTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoidHJhbnNmZXJfdG9fZWRpdG9yIiwiYXJncyI6e30sImlkIjoidG9vbHVfMDFWQnJXZFdFV1lnaXQ0a05ZZWhCTVU1IiwidHlwZSI6InRvb2xfY2FsbCJ9XSwiaW52YWxpZF90b29sX2NhbGxzIjpbXSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjo3NDE2LCJvdXRwdXRfdG9rZW5zIjoxNTEsInRvdGFsX3Rva2VucyI6NzU2NywiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MTE5fX19XSwic3RydWN0dXJlZF9yZXNwb25zZSI6bnVsbH19" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "28bebc087f4f6678", + "parentSpanId": "ee45871111076e7a", + "name": "ChatAnthropic", + "kind": 1, + "startTimeUnixNano": "1790968032486740992", + "endTimeUnixNano": "1790968035029083136", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chat" + } + }, + { + "key": "gen_ai.serialized.name", + "value": { + "stringValue": "ChatAnthropic" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "llm" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "ChatAnthropic" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "anthropic" + } + }, + { + "key": "gen_ai.request.model", + "value": { + "stringValue": "claude-sonnet" + } + }, + { + "key": "gen_ai.tool.definitions", + "value": { + "stringValue": "[{\"name\":\"ls\",\"input_schema\":{\"properties\":{\"path\":{\"description\":\"Absolute path to the directory to list. Must be absolute, not relative.\",\"type\":\"string\"}},\"required\":[\"path\"],\"type\":\"object\"},\"description\":\"Lists all files in a directory.\\n\\nThis is useful for exploring the filesystem and finding the right file to read or edit.\\nYou should almost ALWAYS use this tool before using the read_file or edit_file tools.\"},{\"name\":\"read_file\",\"input_schema\":{\"properties\":{\"file_path\":{\"description\":\"Absolute path to the file to read. Must be absolute, not relative.\",\"type\":\"string\"},\"offset\":{\"default\":0,\"description\":\"Line number to start reading from (0-indexed). Use for pagination of large files.\",\"type\":\"integer\"},\"limit\":{\"default\":100,\"description\":\"Maximum number of lines to read. Use for pagination of large files.\",\"type\":\"integer\"}},\"required\":[\"file_path\"],\"type\":\"object\"},\"description\":\"Reads a file from the filesystem. Assume any path the user provides is valid; reading a missing file returns an error.\\n\\nUsage:\\n- By default, it reads up to 100 lines starting from the beginning of the file. Use `offset`/`limit` to page through large files instead of reading them whole.\\n- A status header, `@@ field | field | ... @@`, sits above the file content, and every line after it is verbatim file content. When content is truncated, there may be an explanation before the header. Never include the header when editing.\\n- Speculatively batch multiple `read_file` calls in one response when several files may be useful.\\n- An empty file returns a system-reminder warning in place of contents.\\n- Large tool results may be offloaded to a file; the tool message gives the path. Read that path here, paging with `offset`/`limit`.\\n- Images (`.png`, `.jpg`, etc.), audio, video, and PDFs return multimodal content blocks (https://docs.langchain.com/oss/python/langchain/messages#multimodal).\\n- For images and PDFs, pagination via `offset`/`limit` is text-only - supply `file_path` only\\n- Always read a file before editing it.\"},{\"name\":\"glob\",\"input_schema\":{\"properties\":{\"pattern\":{\"description\":\"Glob pattern to match files (e.g., '*.py', '**/*.py', '/subdir/**/*.md'). A pattern without '/' matches the file name at any depth; a pattern containing '/' matches the search-root-relative path; a leading '/' anchors to the search root ('/*.py' matches only top-level files). Leading-dot names are excluded unless the pattern segment starts with '.', so prefer the bare form '*.py' over '**/*.py' -- '**' will not descend into dot-directories like '.github'.\",\"type\":\"string\"},\"path\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"null\"}],\"default\":null,\"description\":\"Base directory to search from. Defaults to the backend's default root.\"}},\"required\":[\"pattern\"],\"type\":\"object\"},\"description\":\"Find files matching a glob pattern, returning absolute paths.\\n\\nSupports `*` (any characters within a path segment), `**` (any directories), `?` (single character), `[abc]` (one character from a set), and `{a,b}` (alternatives), e.g. `*.py`, `src/**/*.py`, `*.{yml,yaml}`.\\n\\nA pattern without `/` matches the file name at any depth under the search root (`*.py` matches `src/app/main.py`). A pattern containing `/` matches the search-root-relative path (`src/**/*.py`). A leading `/` anchors to the search root (`/*.py` matches only top-level Python files).\\n\\nLeading-dot names are only matched when the pattern segment itself starts with `.` (use `.env`, or `.github/**/*.yml`). Because `**` will not descend into dot-directories, the bare form `*.yml` is *broader* than `**/*.yml` and is usually what you want.\"},{\"name\":\"grep\",\"input_schema\":{\"properties\":{\"pattern\":{\"description\":\"Text pattern to search for (literal string, not regex).\",\"type\":\"string\"},\"path\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"null\"}],\"default\":null,\"description\":\"Directory to search in. Defaults to current working directory.\"},\"glob\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"null\"}],\"default\":null,\"description\":\"Glob pattern (NOT regex) limiting which files are searched (e.g. '*.py', '*.ts'). A pattern without '/' matches the file name at any depth; a pattern containing '/' matches the search-root-relative path (e.g. 'src/**/*.py'). This is an in-tool file filter, not a call to the separate glob tool. Brace expansion (e.g. '*.{ts,tsx}') is not supported on all backends; run a separate search per extension for reliable results.\"},\"output_mode\":{\"default\":\"files_with_matches\",\"description\":\"Shape of the returned text. 'files_with_matches' (default): newline-separated matching file paths. 'content': matching lines grouped by file under a ':' header, each line indented and formatted ': ' (only the matched line, no surrounding context). 'count': one ': ' line per file.\",\"enum\":[\"files_with_matches\",\"content\",\"count\"],\"type\":\"string\"},\"max_count\":{\"anyOf\":[{\"exclusiveMinimum\":0,\"type\":\"integer\"},{\"type\":\"null\"}],\"default\":null,\"description\":\"Optional cap on the total number of matches returned across all files. Leave unset to use the configured default. When the cap is hit, results are truncated and a note says so; narrow the pattern or path to see the rest.\"}},\"required\":[\"pattern\"],\"type\":\"object\"},\"description\":\"Search for a LITERAL text pattern across files (NOT regex).\\n\\nThe pattern is matched verbatim: regex metacharacters are ordinary characters, not operators. To match any of several strings, run a separate grep for each; `grep(pattern=\\\"foo|bar\\\")` searches for the literal text \\\"foo|bar\\\", and `.*` or `\\\\.` match those characters literally.\\n\\nReturns matching files or content per `output_mode`. Offloaded large tool results live under the artifacts root (`/large_tool_results/` by default); grep that directory to search them when you do not know the exact path.\"},{\"name\":\"transfer_to_skeptic\",\"input_schema\":{\"properties\":{},\"type\":\"object\"},\"description\":\"Return an unresolved objection to the skeptic\"},{\"name\":\"transfer_to_editor\",\"input_schema\":{\"properties\":{},\"type\":\"object\"},\"description\":\"Send adversarial findings to the editor\"}]" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_chat_model" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "1" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "red_team:15ccef64-4162-9e02-e87c-4cb0eb6ea16b|model:04f09f43-f066-c110-818d-1a33d3ba1569" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "red_team" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "red_team:15ccef64-4162-9e02-e87c-4cb0eb6ea16b" + } + }, + { + "key": "langsmith.metadata.ls_provider", + "value": { + "stringValue": "anthropic" + } + }, + { + "key": "langsmith.metadata.ls_model_name", + "value": { + "stringValue": "claude-sonnet" + } + }, + { + "key": "langsmith.metadata.ls_model_type", + "value": { + "stringValue": "chat" + } + }, + { + "key": "langsmith.metadata.ls_max_tokens", + "value": { + "intValue": "4096" + } + }, + { + "key": "langsmith.metadata.lc_versions", + "value": { + "stringValue": "{\"langchain-core\":\"1.6.6\",\"langchain\":\"1.4.3\",\"langchain-anthropic\":\"1.7.5\"}" + } + }, + { + "key": "langsmith.metadata.ls_method", + "value": { + "stringValue": "traceable" + } + }, + { + "key": "langsmith.metadata.model", + "value": { + "stringValue": "claude-sonnet" + } + }, + { + "key": "langsmith.metadata.max_tokens", + "value": { + "intValue": "4096" + } + }, + { + "key": "langsmith.metadata.model_kwargs", + "value": { + "stringValue": "{\"extra_body\":{\"extra_headers\":{\"anthropic-workspace-id\":\"[redacted workspace]\"}}}" + } + }, + { + "key": "langsmith.metadata.streaming", + "value": { + "boolValue": false + } + }, + { + "key": "langsmith.metadata.max_retries", + "value": { + "intValue": "2" + } + }, + { + "key": "langsmith.metadata._type", + "value": { + "stringValue": "anthropic-chat" + } + }, + { + "key": "langsmith.metadata.usage_metadata", + "value": { + "stringValue": "{\"input_tokens\":7416,\"output_tokens\":151,\"total_tokens\":7567,\"input_token_details\":{\"cache_read\":0,\"cache_creation\":0,\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_token_details\":{\"reasoning\":119}}" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "seq:step:1" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W1t7ImxjIjoxLCJ0eXBlIjoiY29uc3RydWN0b3IiLCJpZCI6WyJsYW5nY2hhaW4iLCJzY2hlbWEiLCJtZXNzYWdlcyIsIlN5c3RlbU1lc3NhZ2UiXSwia3dhcmdzIjp7ImNvbnRlbnQiOiJZb3UgYXJlIHJlZF90ZWFtLiBGaW5kIHRoZSBzdHJvbmdlc3QgcmVtYWluaW5nIG9iamVjdGlvbiB0byB0aGUgdmVyaWZpZWQgZmluZGluZ3MuIFNlbmQgdW5yZXNvbHZlZCBpc3N1ZXMgdG8gdGhlIHNrZXB0aWMsIG9yIHNlbmQgeW91ciBhc3Nlc3NtZW50IHRvIHRoZSBlZGl0b3IuIERvIG5vdCBnaXZlIHRoZSBmaW5hbCBhbnN3ZXIuIiwidHlwZSI6InN5c3RlbSJ9fSx7ImxjIjoxLCJ0eXBlIjoiY29uc3RydWN0b3IiLCJpZCI6WyJsYW5nY2hhaW4iLCJzY2hlbWEiLCJtZXNzYWdlcyIsIkh1bWFuTWVzc2FnZSJdLCJrd2FyZ3MiOnsiY29udGVudCI6IkZvciBhIHN5bnRoZXRpYyB0cmFjaW5nIGZpeHR1cmUsIGV4cGxhaW4gUHl0aG9uIHR1cGxlIHZlcnN1cyBsaXN0IGltbXV0YWJpbGl0eSBpbiBhdCBtb3N0IDEyMCB3b3Jkcy4gVXNlIG9ubHkgb2ZmaWNpYWwgUHl0aG9uIGRvY3VtZW50YXRpb24gaWYgeW91IHNlYXJjaC4gSGF2ZSB0aGUgc2tlcHRpYyBhbmQgdmVyaWZpZXIgY2hlY2sgaXQsIHRoZW4gaGF2ZSB0aGUgZWRpdG9yIHdyaXRlIC9hbnN3ZXIubWQuIEtlZXAgdGhlIHJ1biBzaG9ydCBhbmQgZG8gbm90IGFzayBmb2xsb3ctdXAgcXVlc3Rpb25zLiIsInR5cGUiOiJodW1hbiIsImlkIjoiNDYyMDQzMDUtZjhlZS00Mzg0LTljZTgtZmNkODBiZmNkZjc1In19LHsibGMiOjEsInR5cGUiOiJjb25zdHJ1Y3RvciIsImlkIjpbImxhbmdjaGFpbiIsInNjaGVtYSIsIm1lc3NhZ2VzIiwiQUlNZXNzYWdlIl0sImt3YXJncyI6eyJjb250ZW50IjpbeyJpZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7InF1ZXJ5IjoiZG9jcy5weXRob24ub3JnIHR1cGxlcyBpbW11dGFibGUgc2VxdWVuY2VzIGxpc3RzIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIHR1dG9yaWFsIn0sIm5hbWUiOiJ3ZWJfc2VhcmNoIiwidHlwZSI6InRvb2xfdXNlIiwidG9vbHNldF9uYW1lIjpudWxsfV0sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHc5YVc5YnJlblBxZ3liZk53UCIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjoyODQ1LCJvdXRwdXRfdG9rZW5zIjo3Mywib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6MH0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoicmVzZWFyY2hlciIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy0yMWEzLTcwMDAtYTM4MS0yODRhNmEwYzAzZjctMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ3ZWJfc2VhcmNoIiwiYXJncyI6eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9LCJpZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsInR5cGUiOiJ0b29sX2NhbGwifV0sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6Mjg0NSwib3V0cHV0X3Rva2VucyI6NzMsInRvdGFsX3Rva2VucyI6MjkxOCwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19LCJpbnZhbGlkX3Rvb2xfY2FsbHMiOltdfX0seyJsYyI6MSwidHlwZSI6ImNvbnN0cnVjdG9yIiwiaWQiOlsibGFuZ2NoYWluIiwic2NoZW1hIiwibWVzc2FnZXMiLCJUb29sTWVzc2FnZSJdLCJrd2FyZ3MiOnsiY29udGVudCI6Ilt7XCJ0aXRsZVwiOiBcIjUuIERhdGEgU3RydWN0dXJlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiWW91IG1pZ2h0IGhhdmUgbm90aWNlZCB0aGF0IG1ldGhvZHMgbGlrZWBpbnNlcnRgLGByZW1vdmVgIG9yYHNvcnRgIHRoYXQgb25seSBtb2RpZnkgdGhlIGxpc3QgaGF2ZSBubyByZXR1cm4gdmFsdWUgcHJpbnRlZCBcXHUyMDEzIHRoZXkgcmV0dXJuIHRoZSBkZWZhdWx0YE5vbmVgLiBbMV0gVGhpcyBpcyBhIGRlc2lnbiBwcmluY2lwbGUgZm9yIGFsbCBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyBpbiBQeXRob24uXFxuLi4uXFxuIyMgNS4zLiBUdXBsZXMgYW5kIFNlcXVlbmNlc1xcdTAwYjZcXG5cXG5XZSBzYXcgdGhhdCBsaXN0cyBhbmQgc3RyaW5ncyBoYXZlIG1hbnkgY29tbW9uIHByb3BlcnRpZXMsIHN1Y2ggYXMgaW5kZXhpbmcgYW5kIHNsaWNpbmcgb3BlcmF0aW9ucy4gVGhleSBhcmUgdHdvIGV4YW1wbGVzIG9mIHNlcXVlbmNlIGRhdGEgdHlwZXMgKHNlZSBTZXF1ZW5jZSBUeXBlcyBcXHUyMDE0IGxpc3QsIHR1cGxlLCByYW5nZSkuIFNpbmNlIFB5dGhvbiBpcyBhbiBldm9sdmluZyBsYW5ndWFnZSwgb3RoZXIgc2VxdWVuY2UgZGF0YSB0eXBlcyBtYXkgYmUgYWRkZWQuIFRoZXJlIGlzIGFsc28gYW5vdGhlciBzdGFuZGFyZCBzZXF1ZW5jZSBkYXRhIHR5cGU6IHRoZSB0dXBsZS5cXG5cXG5BIHR1cGxlIGNvbnNpc3RzIG9mIGEgbnVtYmVyIG9mIHZhbHVlcyBzZXBhcmF0ZWQgYnkgY29tbWFzLCBmb3IgaW5zdGFuY2U6XFxuLi4uXFxuPj4+ICMgVHVwbGVzIGFyZSBpbW11dGFibGU6XFxuPj4+IHRbMF0gPSA4ODg4OFxcblRyYWNlYmFjayAobW9zdCByZWNlbnQgY2FsbCBsYXN0KTpcXG4gIEZpbGUgXFxcIjxzdGRpbj5cXFwiLCBsaW5lIDEsIGluIDxtb2R1bGU+XFxuVHlwZUVycm9yOiAndHVwbGUnIG9iamVjdCBkb2VzIG5vdCBzdXBwb3J0IGl0ZW0gYXNzaWdubWVudFxcbj4+PiAjIGJ1dCB0aGV5IGNhbiBjb250YWluIG11dGFibGUgb2JqZWN0czpcXG4+Pj4gdiA9IChbMSwgMiwgM10sIFszLCAyLCAxXSlcXG4+Pj4gdlxcbihbMSwgMiwgM10sIFszLCAyLCAxXSlcXG4uLi5cXG5BcyB5b3Ugc2VlLCBvbiBvdXRwdXQgdHVwbGVzIGFyZSBhbHdheXMgZW5jbG9zZWQgaW4gcGFyZW50aGVzZXMsIHNvIHRoYXQgbmVzdGVkIHR1cGxlcyBhcmUgaW50ZXJwcmV0ZWQgY29ycmVjdGx5OyB0aGV5IG1heSBiZSBpbnB1dCB3aXRoIG9yIHdpdGhvdXQgc3Vycm91bmRpbmcgcGFyZW50aGVzZXMsIGFsdGhvdWdoIG9mdGVuIHBhcmVudGhlc2VzIGFyZSBuZWNlc3NhcnkgYW55d2F5IChpZiB0aGUgdHVwbGUgaXMgcGFydCBvZiBhIGxhcmdlciBleHByZXNzaW9uKS4gSXQgaXMgbm90IHBvc3NpYmxlIHRvIGFzc2lnbiB0byB0aGUgaW5kaXZpZHVhbCBpdGVtcyBvZiBhIHR1cGxlLCBob3dldmVyIGl0IGlzIHBvc3NpYmxlIHRvIGNyZWF0ZSB0dXBsZXMgd2hpY2ggY29udGFpbiBtdXRhYmxlIG9iamVjdHMsIHN1Y2ggYXMgbGlzdHMuXFxuXFxuVGhvdWdoIHR1cGxlcyBtYXkgc2VlbSBzaW1pbGFyIHRvIGxpc3RzLCB0aGV5IGFyZSBvZnRlbiB1c2VkIGluIGRpZmZlcmVudCBzaXR1YXRpb25zIGFuZCBmb3IgZGlmZmVyZW50IHB1cnBvc2VzLiBUdXBsZXMgYXJlIGltbXV0YWJsZSwgYW5kIHVzdWFsbHkgY29udGFpbiBhIGhldGVyb2dlbmVvdXMgc2VxdWVuY2Ugb2YgZWxlbWVudHMgdGhhdCBhcmUgYWNjZXNzZWQgdmlhIHVucGFja2luZyAoc2VlIGxhdGVyIGluIHRoaXMgc2VjdGlvbikgb3IgaW5kZXhpbmcgKG9yIGV2ZW4gYnkgYXR0cmlidXRlIGluIHRoZSBjYXNlIG9mIG5hbWVkdHVwbGVzKS4gTGlzdHMgYXJlIG11dGFibGUsIGFuZCB0aGVpciBlbGVtZW50cyBhcmUgdXN1YWxseSBob21vZ2VuZW91cyBhbmQgYXJlIGFjY2Vzc2VkIGJ5IGl0ZXJhdGluZyBvdmVyIHRoZSBsaXN0Llxcbi4uLlxcbkFub3RoZXIgdXNlZnVsIGRhdGEgdHlwZSBidWlsdCBpbnRvIFB5dGhvbiBpcyB0aGUgZGljdGlvbmFyeSAoc2VlIE1hcHBpbmcgVHlwZXMgXFx1MjAxNCBkaWN0KS4gRGljdGlvbmFyaWVzIGFyZSBzb21ldGltZXMgZm91bmQgaW4gb3RoZXIgbGFuZ3VhZ2VzIGFzIFxcdTIwMWNhc3NvY2lhdGl2ZSBtZW1vcmllc1xcdTIwMWQgb3IgXFx1MjAxY2Fzc29jaWF0aXZlIGFycmF5c1xcdTIwMWQuIFVubGlrZSBzZXF1ZW5jZXMsIHdoaWNoIGFyZSBpbmRleGVkIGJ5IGEgcmFuZ2Ugb2YgbnVtYmVycywgZGljdGlvbmFyaWVzIGFyZSBpbmRleGVkIGJ5IGtleXMsIHdoaWNoIGNhbiBiZSBhbnkgaW1tdXRhYmxlIHR5cGU7IHN0cmluZ3MgYW5kIG51bWJlcnMgY2FuIGFsd2F5cyBiZSBrZXlzLiBUdXBsZXMgY2FuIGJlIHVzZWQgYXMga2V5cyBpZiB0aGV5IGNvbnRhaW4gb25seSBzdHJpbmdzLCBudW1iZXJzLCBvciB0dXBsZXM7IGlmIGEgdHVwbGUgY29udGFpbnMgYW55IG11dGFibGUgb2JqZWN0IGVpdGhlciBkaXJlY3RseSBvciBpbmRpcmVjdGx5LCBpdCBjYW5ub3QgYmUgdXNlZCBhcyBhIGtleS4gWW91IGNhblxcdTIwMTl0IHVzZSBsaXN0cyBhcyBrZXlzLCBzaW5jZSBsaXN0cyBjYW4gYmUgbW9kaWZpZWQgaW4gcGxhY2UgdXNpbmcgaW5kZXggYXNzaWdubWVudHMsIHNsaWNlIGFzc2lnbm1lbnRzLCBvciBtZXRob2RzIGxpa2UgYXBwZW5kKCkgYW5kIGV4dGVuZCgpLlwiXX0sIHtcInRpdGxlXCI6IFwiMy4gRGF0YSBtb2RlbCBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvcmVmZXJlbmNlL2RhdGFtb2RlbC5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJUaGUgdmFsdWUgb2Ygc29tZSBvYmplY3RzIGNhbiBjaGFuZ2UuIE9iamVjdHMgd2hvc2UgdmFsdWUgY2FuIGNoYW5nZSBhcmUgc2FpZCB0byBiZSBtdXRhYmxlOyBvYmplY3RzIHdob3NlIHZhbHVlIGlzIHVuY2hhbmdlYWJsZSBvbmNlIHRoZXkgYXJlIGNyZWF0ZWQgYXJlIGNhbGxlZCBpbW11dGFibGUuIChUaGUgdmFsdWUgb2YgYW4gaW1tdXRhYmxlIGNvbnRhaW5lciBvYmplY3QgdGhhdCBjb250YWlucyBhIHJlZmVyZW5jZSB0byBhIG11dGFibGUgb2JqZWN0IGNhbiBjaGFuZ2Ugd2hlbiB0aGUgbGF0dGVyXFx1MjAxOXMgdmFsdWUgaXMgY2hhbmdlZDsgaG93ZXZlciB0aGUgY29udGFpbmVyIGlzIHN0aWxsIGNvbnNpZGVyZWQgaW1tdXRhYmxlLCBiZWNhdXNlIHRoZSBjb2xsZWN0aW9uIG9mIG9iamVjdHMgaXQgY29udGFpbnMgY2Fubm90IGJlIGNoYW5nZWQuIFNvLCBpbW11dGFiaWxpdHkgaXMgbm90IHN0cmljdGx5IHRoZSBzYW1lIGFzIGhhdmluZyBhbiB1bmNoYW5nZWFibGUgdmFsdWUsIGl0IGlzIG1vcmUgc3VidGxlLikgQW4gb2JqZWN0XFx1MjAxOXMgbXV0YWJpbGl0eSBpcyBkZXRlcm1pbmVkIGJ5IGl0cyB0eXBlOyBmb3IgaW5zdGFuY2UsIG51bWJlcnMsIHN0cmluZ3MgYW5kIHR1cGxlcyBhcmUgaW1tdXRhYmxlLCB3aGlsZSBkaWN0aW9uYXJpZXMgYW5kIGxpc3RzIGFyZSBtdXRhYmxlLlxcbi4uLlxcblNvbWUgb2JqZWN0cyBjb250YWluIHJlZmVyZW5jZXMgdG8gb3RoZXIgb2JqZWN0czsgdGhlc2UgYXJlIGNhbGxlZCBjb250YWluZXJzLiBFeGFtcGxlcyBvZiBjb250YWluZXJzIGFyZSB0dXBsZXMsIGxpc3RzIGFuZCBkaWN0aW9uYXJpZXMuIFRoZSByZWZlcmVuY2VzIGFyZSBwYXJ0IG9mIGEgY29udGFpbmVyXFx1MjAxOXMgdmFsdWUuIEluIG1vc3QgY2FzZXMsIHdoZW4gd2UgdGFsayBhYm91dCB0aGUgdmFsdWUgb2YgYSBjb250YWluZXIsIHdlIGltcGx5IHRoZSB2YWx1ZXMsIG5vdCB0aGUgaWRlbnRpdGllcyBvZiB0aGUgY29udGFpbmVkIG9iamVjdHM7IGhvd2V2ZXIsIHdoZW4gd2UgdGFsayBhYm91dCB0aGUgbXV0YWJpbGl0eSBvZiBhIGNvbnRhaW5lciwgb25seSB0aGUgaWRlbnRpdGllcyBvZiB0aGUgaW1tZWRpYXRlbHkgY29udGFpbmVkIG9iamVjdHMgYXJlIGltcGxpZWQuIFNvLCBpZiBhbiBpbW11dGFibGUgY29udGFpbmVyIChsaWtlIGEgdHVwbGUpIGNvbnRhaW5zIGEgcmVmZXJlbmNlIHRvIGEgbXV0YWJsZSBvYmplY3QsIGl0cyB2YWx1ZSBjaGFuZ2VzIGlmIHRoYXQgbXV0YWJsZSBvYmplY3QgaXMgY2hhbmdlZC5cXG4uLi5cXG4jIyMgMy4yLjUuIFNlcXVlbmNlc1xcdTAwYjZcXG4uLi5cXG4jIyMjIDMuMi41LjEuIEltbXV0YWJsZSBzZXF1ZW5jZXNcXHUwMGI2XFxuXFxuQW4gb2JqZWN0IG9mIGFuIGltbXV0YWJsZSBzZXF1ZW5jZSB0eXBlIGNhbm5vdCBjaGFuZ2Ugb25jZSBpdCBpcyBjcmVhdGVkLiAoSWYgdGhlIG9iamVjdCBjb250YWlucyByZWZlcmVuY2VzIHRvIG90aGVyIG9iamVjdHMsIHRoZXNlIG90aGVyIG9iamVjdHMgbWF5IGJlIG11dGFibGUgYW5kIG1heSBiZSBjaGFuZ2VkOyBob3dldmVyLCB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGRpcmVjdGx5IHJlZmVyZW5jZWQgYnkgYW4gaW1tdXRhYmxlIG9iamVjdCBjYW5ub3QgY2hhbmdlLilcXG5cXG5UaGUgZm9sbG93aW5nIHR5cGVzIGFyZSBpbW11dGFibGUgc2VxdWVuY2VzOlxcbi4uLlxcblR1cGxlc1xcbjogVGhlIGl0ZW1zIG9mIGEgYHR1cGxlYCBhcmUgYXJiaXRyYXJ5IFB5dGhvbiBvYmplY3RzLiBUdXBsZXMgb2YgdHdvIG9yIG1vcmUgaXRlbXMgYXJlIGZvcm1lZCBieSBjb21tYS1zZXBhcmF0ZWQgbGlzdHMgb2YgZXhwcmVzc2lvbnMuIEEgdHVwbGUgb2Ygb25lIGl0ZW0gKGEgXFx1MjAxOHNpbmdsZXRvblxcdTIwMTkpIGNhbiBiZSBmb3JtZWQgYnkgYWZmaXhpbmcgYSBjb21tYSB0byBhbiBleHByZXNzaW9uIChhbiBleHByZXNzaW9uIGJ5IGl0c2VsZiBkb2VzIG5vdCBjcmVhdGUgYSB0dXBsZSwgc2luY2UgcGFyZW50aGVzZXMgbXVzdCBiZSB1c2FibGUgZm9yIGdyb3VwaW5nIG9mIGV4cHJlc3Npb25zKS4gQW4gZW1wdHkgdHVwbGUgY2FuIGJlIGZvcm1lZCBieSBhbiBlbXB0eSBwYWlyIG9mIHBhcmVudGhlc2VzLlxcbi4uLlxcbiMjIyMgMy4yLjUuMi4gTXV0YWJsZSBzZXF1ZW5jZXNcXHUwMGI2XFxuXFxuTXV0YWJsZSBzZXF1ZW5jZXMgY2FuIGJlIGNoYW5nZWQgYWZ0ZXIgdGhleSBhcmUgY3JlYXRlZC4gVGhlIHN1YnNjcmlwdGlvbiBhbmQgc2xpY2luZyBub3RhdGlvbnMgY2FuIGJlIHVzZWQgYXMgdGhlIHRhcmdldCBvZiBhc3NpZ25tZW50IGFuZCBgZGVsYCAoZGVsZXRlKSBzdGF0ZW1lbnRzLlxcbi4uLlxcblRoZXJlIGFyZSBjdXJyZW50bHkgdHdvIGludHJpbnNpYyBtdXRhYmxlIHNlcXVlbmNlIHR5cGVzOlxcblxcbkxpc3RzXFxuOiBUaGUgaXRlbXMgb2YgYSBsaXN0IGFyZSBhcmJpdHJhcnkgUHl0aG9uIG9iamVjdHMuIExpc3RzIGFyZSBmb3JtZWQgYnkgcGxhY2luZyBhIGNvbW1hLXNlcGFyYXRlZCBsaXN0IG9mIGV4cHJlc3Npb25zIGluIHNxdWFyZSBicmFja2V0cy4gKE5vdGUgdGhhdCB0aGVyZSBhcmUgbm8gc3BlY2lhbCBjYXNlcyBuZWVkZWQgdG8gZm9ybSBsaXN0cyBvZiBsZW5ndGggMCBvciAxLilcIl19LCB7XCJ0aXRsZVwiOiBcIkJ1aWx0LWluIFR5cGVzIFxcdTIwMTQgUHl0aG9uIDMuMTQuNyBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9idWlsdGlucy9zdGR0eXBlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJjb2xsZWN0aW9uIGNsYXNzZXMgYXJlIG11dGFibGUuIFRoZSBtZXRob2RzIHRoYXQgYWRkLCBzdWJ0cmFjdCwgb3IgcmVhcnJhbmdlIHRoZWlyIC4uLiBpbiBwbGFjZSwgYW5kIGRvblxcdTIwMTl0IHJldHVybiBhIC4uLiAsIG5ldmVyIHJldHVybiB0aGUgY29sbGVjdGlvbiBpbnN0YW5jZSBpdHNlbGYgYnV0IGBOb25lYC5cXG4uLi5cXG4jIyBTZXF1ZW5jZSBUeXBlcyBcXHUyMDE0IGBsaXN0YCwgYHR1cGxlYCwgYHJhbmdlYFxcdTAwYjZcXG5cXG5UaGVyZSBhcmUgdGhyZWUgYmFzaWMgc2VxdWVuY2UgdHlwZXM6IGxpc3RzLCB0dXBsZXMsIGFuZCByYW5nZSBvYmplY3RzLiBBZGRpdGlvbmFsIHNlcXVlbmNlIHR5cGVzIHRhaWxvcmVkIGZvciBwcm9jZXNzaW5nIG9mIGJpbmFyeSBkYXRhIGFuZCB0ZXh0IHN0cmluZ3MgYXJlIGRlc2NyaWJlZCBpbiBkZWRpY2F0ZWQgc2VjdGlvbnMuXFxuLi4uXFxuVGhlIG9wZXJhdGlvbnMgaW4gdGhlIGZvbGxvd2luZyB0YWJsZSAuLi4gbXV0YWJsZSBhbmQgaW1tdXRhYmxlLiBUaGUgYGNvbGxlY3Rpb25zLiAuLi4gaXMgcHJvdmlkZWQgdG8gbWFrZSAuLi4gZWFzaWVyIHRvIGNvcnJlY3RseSBpbXBsZW1lbnQgdGhlc2Ugb3BlcmF0aW9ucyBvbiBjdXN0b20gc2VxdWVuY2UgdHlwZXNcXG4uLi5cXG4jIyMgSW1tdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbm11dGFibGUgc2VxdWVuY2UgdHlwZXMgaXNcXG4uLi5cXG5zdXBwb3J0IGFsbG93cyBpbW11dGFibGUgc2VxdWVuY2VzLCAuLi4gLCB0byBiZSB1c2VkIGFzIGBkaWN0YCBrZXlzIGFuZCBzdG9yZWQgaW4gLi4uIGVuc2V0YCBpbnN0YW5jZXNcXG4uLi5cXG4jIyMgTXV0YWJsZSBTZXF1ZW5jZSBUeXBlc1xcdTAwYjZcXG4uLi5cXG4jIyMgTGlzdHNcXHUwMGI2XFxuXFxuTGlzdHMgYXJlIG11dGFibGUgc2VxdWVuY2VzLCB0eXBpY2FsbHkgdXNlZCB0byBzdG9yZSBjb2xsZWN0aW9ucyBvZiBob21vZ2VuZW91cyBpdGVtcyAod2hlcmUgdGhlIHByZWNpc2UgZGVncmVlIG9mIHNpbWlsYXJpdHkgd2lsbCB2YXJ5IGJ5IGFwcGxpY2F0aW9uKS5cXG4uLi5cXG4jIyMgVHVwbGVzXFx1MDBiNlxcblxcblR1cGxlcyBhcmUgaW1tdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaGV0ZXJvZ2VuZW91cyBkYXRhIChzdWNoIGFzIHRoZSAyLXR1cGxlcyBwcm9kdWNlZCBieSB0aGUgYGVudW1lcmF0ZSgpYCBidWlsdC1pbikuIFR1cGxlcyBhcmUgYWxzbyB1c2VkIGZvciBjYXNlcyB3aGVyZSBhbiBpbW11dGFibGUgc2VxdWVuY2Ugb2YgaG9tb2dlbmVvdXMgZGF0YSBpcyBuZWVkZWQgKHN1Y2ggYXMgYWxsb3dpbmcgc3RvcmFnZSBpbiBhIGBzZXRgIG9yIGBkaWN0YCBpbnN0YW5jZSkuXFxuLi4uXFxudHVwbGUoaXRlcmFibGU9XFxuLi4uXFxuVGhlIGNvbnN0cnVjdG9yIGJ1aWxkcyBhIHR1cGxlIHdob3NlIGl0ZW1zIGFyZSB0aGUgc2FtZSBhbmQgaW4gdGhlIHNhbWUgb3JkZXIgYXMgaXRlcmFibGVcXHUyMDE5cyBpdGVtcy4gaXRlcmFibGUgbWF5IGJlIGVpdGhlciBhIHNlcXVlbmNlLCBhIGNvbnRhaW5lciB0aGF0IHN1cHBvcnRzIGl0ZXJhdGlvbiwgb3IgYW4gaXRlcmF0b3Igb2JqZWN0LiBJZiBpdGVyYWJsZSBpcyBhbHJlYWR5IGEgdHVwbGUsIGl0IGlzIHJldHVybmVkIHVuY2hhbmdlZC4gRm9yIGV4YW1wbGUsIGB0dXBsZSgnYWJjJylgIHJldHVybnMgYCgnYScsICdiJywgJ2MnKWAgYW5kIGB0dXBsZSggWzEsIDIsIDNdIClgIHJldHVybnMgYCgxLCAyLCAzKWAuIElmIG5vIGFyZ3VtZW50IGlzIGdpdmVuLCB0aGUgY29uc3RydWN0b3IgY3JlYXRlcyBhIG5ldyBlbXB0eSB0dXBsZSwgYCgpYC5cXG4uLi5cXG5UdXBsZXMgaW1wbGVtZW50IGFsbCBvZiB0aGUgY29tbW9uIHNlcXVlbmNlIG9wZXJhdGlvbnMuXFxuLi4uXFxuRm9yIGhldGVyb2dlbmVvdXMgY29sbGVjdGlvbnMgb2YgZGF0YSB3aGVyZSBhY2Nlc3MgYnkgbmFtZSBpcyBjbGVhcmVyIHRoYW4gYWNjZXNzIGJ5IGluZGV4LCBgY29sbGVjdGlvbnMubmFtZWR0dXBsZSgpYCBtYXkgYmUgYSBtb3JlIGFwcHJvcHJpYXRlIGNob2ljZSB0aGFuIGEgc2ltcGxlIHR1cGxlIG9iamVjdC5cIl19LCB7XCJ0aXRsZVwiOiBcIkJ1aWx0LWluIFR5cGVzIFxcdTIwMTQgUHl0aG9uIDMuMTQuNSBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9saWJyYXJ5L3N0ZHR5cGVzLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcIlNvbWUgY29sbGVjdGlvbiBjbGFzc2VzIGFyZSBtdXRhYmxlLiBUaGUgbWV0aG9kcyB0aGF0IGFkZCwgc3VidHJhY3QsIG9yIHJlYXJyYW5nZSB0aGVpciAuLi4gaW4gcGxhY2UsIGFuZCBkb25cXHUyMDE5dCByZXR1cm4gYSAuLi4gLCBuZXZlciByZXR1cm4gdGhlIGNvbGxlY3Rpb24gaW5zdGFuY2UgaXRzZWxmIGJ1dCBgTm9uZWAuXFxuLi4uXFxuIyMgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBgbGlzdGAsIGB0dXBsZWAsIGByYW5nZWBcXHUwMGI2XFxuXFxuVGhlcmUgYXJlIHRocmVlIGJhc2ljIHNlcXVlbmNlIHR5cGVzOiBsaXN0cywgdHVwbGVzLCBhbmQgLi4uIG9iamVjdHMuIEFkZGl0aW9uYWwgc2VxdWVuY2UgdHlwZXMgdGFpbG9yZWQgZm9yIHByb2Nlc3Npbmcgb2YgYmluYXJ5IGRhdGEgYW5kIHRleHQgc3RyaW5ncyBhcmUgZGVzY3JpYmVkIGluIGRlZGljYXRlZCBzZWN0aW9ucy5cXG4uLi5cXG5UaGUgb3BlcmF0aW9ucyBpbiB0aGUgZm9sbG93aW5nIHRhYmxlIC4uLiBpbW11dGFibGUuIFRoZSBgIC4uLiBpcyBwcm92aWRlZCB0byBtYWtlIC4uLiBlYXNpZXIgdG8gY29ycmVjdGx5IGltcGxlbWVudCB0aGVzZSBvcGVyYXRpb25zIG9uIGN1c3RvbSBzZXF1ZW5jZSB0eXBlc1xcbi4uLlxcbiMjIyBJbW11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxub3BlcmF0aW9uIHRoYXQgaW1tdXRhYmxlIHNlcXVlbmNlIC4uLiBieSBtdXRhYmxlIHNlcXVlbmNlIHR5cGVzIGlzIC4uLiBgaGFzaCgpYFxcbi4uLlxcblRoaXMgc3VwcG9ydCBhbGxvd3MgaW1tdXRhYmxlIHNlcXVlbmNlcywgc3VjaCBhcyBgdHVwbGVgIGluc3RhbmNlcywgdG8gYmUgdXNlZCBhcyBgZGljdGAga2V5cyBhbmQgc3RvcmVkIGluIGBzZXRgIGFuZCBgZnJvemVuc2V0YCBpbnN0YW5jZXMuXFxuLi4uXFxuIyMjIE11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxuIyMjIExpc3RzXFx1MDBiNlxcblxcbkxpc3RzIGFyZSBtdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaG9tb2dlbmVvdXMgaXRlbXMgKHdoZXJlIHRoZSBwcmVjaXNlIGRlZ3JlZSBvZiBzaW1pbGFyaXR5IHdpbGwgdmFyeSBieSBhcHBsaWNhdGlvbikuXFxuLi4uXFxuIyMjIFR1cGxlc1xcdTAwYjZcXG5cXG5UdXBsZXMgYXJlIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhldGVyb2dlbmVvdXMgZGF0YSAoc3VjaCBhcyB0aGUgMi10dXBsZXMgcHJvZHVjZWQgYnkgdGhlIGBlbnVtZXJhdGUoKWAgYnVpbHQtaW4pLiBUdXBsZXMgYXJlIGFsc28gdXNlZCBmb3IgY2FzZXMgd2hlcmUgYW4gaW1tdXRhYmxlIHNlcXVlbmNlIG9mIGhvbW9nZW5lb3VzIGRhdGEgaXMgbmVlZGVkIChzdWNoIGFzIGFsbG93aW5nIHN0b3JhZ2UgaW4gYSBgc2V0YCBvciBgZGljdGAgaW5zdGFuY2UpLlxcbi4uLlxcblRoZSBjb25zdHJ1Y3RvciBidWlsZHMgYSB0dXBsZSB3aG9zZSBpdGVtcyBhcmUgdGhlIHNhbWUgYW5kIGluIHRoZSBzYW1lIG9yZGVyIGFzIGl0ZXJhYmxlXFx1MjAxOXMgaXRlbXMuIGl0ZXJhYmxlIG1heSBiZSBlaXRoZXIgYSBzZXF1ZW5jZSwgYSBjb250YWluZXIgdGhhdCBzdXBwb3J0cyBpdGVyYXRpb24sIG9yIGFuIGl0ZXJhdG9yIG9iamVjdC4gSWYgaXRlcmFibGUgaXMgYWxyZWFkeSBhIHR1cGxlLCBpdCBpcyByZXR1cm5lZCB1bmNoYW5nZWQuIEZvciBleGFtcGxlLCBgdHVwbGUoJ2FiYycpYCByZXR1cm5zIGAoJ2EnLCAnYicsICdjJylgIGFuZCBgdHVwbGUoIFsxLCAyLCAzXSApYCByZXR1cm5zIGAoMSwgMiwgMylgLiBJZiBubyBhcmd1bWVudCBpcyBnaXZlbiwgdGhlIGNvbnN0cnVjdG9yIGNyZWF0ZXMgYSBuZXcgZW1wdHkgdHVwbGUsIGAoKWAuXFxuLi4uXFxuVHVwbGVzIGltcGxlbWVudCBhbGwgb2YgdGhlIGNvbW1vbiBzZXF1ZW5jZSBvcGVyYXRpb25zLlxcbi4uLlxcbkZvciBoZXRlcm9nZW5lb3VzIGNvbGxlY3Rpb25zIG9mIGRhdGEgd2hlcmUgYWNjZXNzIGJ5IG5hbWUgaXMgY2xlYXJlciB0aGFuIGFjY2VzcyBieSBpbmRleCwgYGNvbGxlY3Rpb25zLm5hbWVkdHVwbGUoKWAgbWF5IGJlIGEgbW9yZSBhcHByb3ByaWF0ZSBjaG9pY2UgdGhhbiBhIHNpbXBsZSB0dXBsZSBvYmplY3QuXCJdfSwge1widGl0bGVcIjogXCI1LiBEYXRhIFN0cnVjdHVyZXMgXFx1MjAxNCBQeXRob24gMy4xMC4yMCBkb2N1bWVudGF0aW9uXCIsIFwidXJsXCI6IFwiaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy4xMC90dXRvcmlhbC9kYXRhc3RydWN0dXJlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJZb3UgbWlnaHQgaGF2ZSBub3RpY2VkIHRoYXQgbWV0aG9kcyBsaWtlIGBpbnNlcnRgLCBgcmVtb3ZlYCBvciBgc29ydGAgdGhhdCBvbmx5IG1vZGlmeSB0aGUgbGlzdCBoYXZlIG5vIHJldHVybiB2YWx1ZSBwcmludGVkIFxcdTIwMTMgdGhleSByZXR1cm4gdGhlIGRlZmF1bHQgYE5vbmVgLiAxIFRoaXMgaXMgYSBkZXNpZ24gcHJpbmNpcGxlIGZvciBhbGwgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgaW4gUHl0aG9uLlxcbi4uLlxcbiMjIDUuMy4gVHVwbGVzIGFuZCBTZXF1ZW5jZXNcXHUwMGI2XFxuXFxuV2Ugc2F3IHRoYXQgbGlzdHMgYW5kIHN0cmluZ3MgaGF2ZSBtYW55IGNvbW1vbiBwcm9wZXJ0aWVzLCBzdWNoIGFzIGluZGV4aW5nIGFuZCBzbGljaW5nIG9wZXJhdGlvbnMuIFRoZXkgYXJlIHR3byBleGFtcGxlcyBvZiBzZXF1ZW5jZSBkYXRhIHR5cGVzIChzZWUgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBsaXN0LCB0dXBsZSwgcmFuZ2UpLiBTaW5jZSBQeXRob24gaXMgYW4gZXZvbHZpbmcgbGFuZ3VhZ2UsIG90aGVyIHNlcXVlbmNlIGRhdGEgdHlwZXMgbWF5IGJlIGFkZGVkLiBUaGVyZSBpcyBhbHNvIGFub3RoZXIgc3RhbmRhcmQgc2VxdWVuY2UgZGF0YSB0eXBlOiB0aGUgdHVwbGUuXFxuXFxuQSB0dXBsZSBjb25zaXN0cyBvZiBhIG51bWJlciBvZiB2YWx1ZXMgc2VwYXJhdGVkIGJ5IGNvbW1hcywgZm9yIGluc3RhbmNlOlxcbi4uLlxcbj4+PiAjIFR1cGxlcyBhcmUgaW1tdXRhYmxlOlxcbi4uLiB0WzBdID0gODg4ODhcXG5UcmFjZWJhY2sgKG1vc3QgcmVjZW50IGNhbGwgbGFzdCk6XFxuICBGaWxlIFxcXCI8c3RkaW4+XFxcIiwgbGluZSAxLCBpbiA8bW9kdWxlPlxcblR5cGVFcnJvcjogJ3R1cGxlJyBvYmplY3QgZG9lcyBub3Qgc3VwcG9ydCBpdGVtIGFzc2lnbm1lbnRcXG4+Pj4gIyBidXQgdGhleSBjYW4gY29udGFpbiBtdXRhYmxlIG9iamVjdHM6XFxuLi4uIHYgPSAoWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuPj4+IHZcXG4oWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuLi4uXFxuQXMgeW91IHNlZSwgb24gb3V0cHV0IHR1cGxlcyBhcmUgYWx3YXlzIGVuY2xvc2VkIGluIHBhcmVudGhlc2VzLCBzbyB0aGF0IG5lc3RlZCB0dXBsZXMgYXJlIGludGVycHJldGVkIGNvcnJlY3RseTsgdGhleSBtYXkgYmUgaW5wdXQgd2l0aCBvciB3aXRob3V0IHN1cnJvdW5kaW5nIHBhcmVudGhlc2VzLCBhbHRob3VnaCBvZnRlbiBwYXJlbnRoZXNlcyBhcmUgbmVjZXNzYXJ5IGFueXdheSAoaWYgdGhlIHR1cGxlIGlzIHBhcnQgb2YgYSBsYXJnZXIgZXhwcmVzc2lvbikuIEl0IGlzIG5vdCBwb3NzaWJsZSB0byBhc3NpZ24gdG8gdGhlIGluZGl2aWR1YWwgaXRlbXMgb2YgYSB0dXBsZSwgaG93ZXZlciBpdCBpcyBwb3NzaWJsZSB0byBjcmVhdGUgdHVwbGVzIHdoaWNoIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzLCBzdWNoIGFzIGxpc3RzLlxcblxcblRob3VnaCB0dXBsZXMgbWF5IHNlZW0gc2ltaWxhciB0byBsaXN0cywgdGhleSBhcmUgb2Z0ZW4gdXNlZCBpbiBkaWZmZXJlbnQgc2l0dWF0aW9ucyBhbmQgZm9yIGRpZmZlcmVudCBwdXJwb3Nlcy4gVHVwbGVzIGFyZSBpbW11dGFibGUsIGFuZCB1c3VhbGx5IGNvbnRhaW4gYSBoZXRlcm9nZW5lb3VzIHNlcXVlbmNlIG9mIGVsZW1lbnRzIHRoYXQgYXJlIGFjY2Vzc2VkIHZpYSB1bnBhY2tpbmcgKHNlZSBsYXRlciBpbiB0aGlzIHNlY3Rpb24pIG9yIGluZGV4aW5nIChvciBldmVuIGJ5IGF0dHJpYnV0ZSBpbiB0aGUgY2FzZSBvZiBgbmFtZWR0dXBsZXNgKS4gTGlzdHMgYXJlIG11dGFibGUsIGFuZCB0aGVpciBlbGVtZW50cyBhcmUgdXN1YWxseSBob21vZ2VuZW91cyBhbmQgYXJlIGFjY2Vzc2VkIGJ5IGl0ZXJhdGluZyBvdmVyIHRoZSBsaXN0Llxcbi4uLlxcbiMjIDUuNC5cXG4uLi5cXG4uIFNldCBvYmplY3RzXFxuLi4uXFxuIyMgNS41LiBEaWN0aW9uYXJpZXNcXHUwMGI2XFxuXFxuQW5vdGhlciB1c2VmdWwgZGF0YSB0eXBlIGJ1aWx0IGludG8gUHl0aG9uIGlzIHRoZSBkaWN0aW9uYXJ5IChzZWUgTWFwcGluZyBUeXBlcyBcXHUyMDE0IGRpY3QpLiBEaWN0aW9uYXJpZXMgYXJlIHNvbWV0aW1lcyBmb3VuZCBpbiBvdGhlciBsYW5ndWFnZXMgYXMgXFx1MjAxY2Fzc29jaWF0aXZlIG1lbW9yaWVzXFx1MjAxZCBvciBcXHUyMDFjYXNzb2NpYXRpdmUgYXJyYXlzXFx1MjAxZC4gVW5saWtlIHNlcXVlbmNlcywgd2hpY2ggYXJlIGluZGV4ZWQgYnkgYSByYW5nZSBvZiBudW1iZXJzLCBkaWN0aW9uYXJpZXMgYXJlIGluZGV4ZWQgYnkga2V5cywgd2hpY2ggY2FuIGJlIGFueSBpbW11dGFibGUgdHlwZTsgc3RyaW5ncyBhbmQgbnVtYmVycyBjYW4gYWx3YXlzIGJlIGtleXMuIFR1cGxlcyBjYW4gYmUgdXNlZCBhcyBrZXlzIGlmIHRoZXkgY29udGFpbiBvbmx5IHN0cmluZ3MsIG51bWJlcnMsIG9yIHR1cGxlczsgaWYgYSB0dXBsZSBjb250YWlucyBhbnkgbXV0YWJsZSBvYmplY3QgZWl0aGVyIGRpcmVjdGx5IG9yIGluZGlyZWN0bHksIGl0IGNhbm5vdCBiZSB1c2VkIGFzIGEga2V5LiBZb3UgY2FuXFx1MjAxOXQgdXNlIGxpc3RzIGFzIGtleXMsIHNpbmNlIGxpc3RzIGNhbiBiZSBtb2RpZmllZCBpbiBwbGFjZSB1c2luZyBpbmRleCBhc3NpZ25tZW50cywgc2xpY2UgYXNzaWdubWVudHMsIG9yIG1ldGhvZHMgbGlrZSBgYXBwZW5kKClgIGFuZCBgZXh0ZW5kKClgLlwiXX1dIiwidHlwZSI6InRvb2wiLCJuYW1lIjoid2ViX3NlYXJjaCIsImlkIjoiNmU1MTI1YWMtMzE2NC00YTc2LTljNTItYWMzNDYwY2UzNmU0IiwidG9vbF9jYWxsX2lkIjoidG9vbHVfMDFCMkxmSEdEallTRkpobWlYTmo0Q3VRIiwic3RhdHVzIjoic3VjY2VzcyJ9fSx7ImxjIjoxLCJ0eXBlIjoiY29uc3RydWN0b3IiLCJpZCI6WyJsYW5nY2hhaW4iLCJzY2hlbWEiLCJtZXNzYWdlcyIsIkFJTWVzc2FnZSJdLCJrd2FyZ3MiOnsiY29udGVudCI6W3siaWQiOiJ0b29sdV8wMVhqajNhUmRXeWhCSmZQbVRKQnhRYTkiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6e30sIm5hbWUiOiJ0cmFuc2Zlcl90b19za2VwdGljIiwidHlwZSI6InRvb2xfdXNlIiwidG9vbHNldF9uYW1lIjpudWxsfV0sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHdBNkY0Y0NONXloRDRDcnM5TSIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo3Mzc4LCJvdXRwdXRfdG9rZW5zIjozMywib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6MH0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoicmVzZWFyY2hlciIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy0zY2Q4LTc5ZjEtODhhMS1jZTZjZWUyOTRlNzktMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ0cmFuc2Zlcl90b19za2VwdGljIiwiYXJncyI6e30sImlkIjoidG9vbHVfMDFYamozYVJkV3loQkpmUG1USkJ4UWE5IiwidHlwZSI6InRvb2xfY2FsbCJ9XSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjo3Mzc4LCJvdXRwdXRfdG9rZW5zIjozMywidG90YWxfdG9rZW5zIjo3NDExLCJpbnB1dF90b2tlbl9kZXRhaWxzIjp7ImNhY2hlX3JlYWQiOjAsImNhY2hlX2NyZWF0aW9uIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VuX2RldGFpbHMiOnsicmVhc29uaW5nIjowfX0sImludmFsaWRfdG9vbF9jYWxscyI6W119fSx7ImxjIjoxLCJ0eXBlIjoiY29uc3RydWN0b3IiLCJpZCI6WyJsYW5nY2hhaW4iLCJzY2hlbWEiLCJtZXNzYWdlcyIsIlRvb2xNZXNzYWdlIl0sImt3YXJncyI6eyJjb250ZW50IjoiU3VjY2Vzc2Z1bGx5IHRyYW5zZmVycmVkIHRvIHNrZXB0aWMiLCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ0cmFuc2Zlcl90b19za2VwdGljIiwiaWQiOiIzODkwNThkMS0wODNmLTQ2MzItYmJjNS0wOWJmZDNhMGZmZDIiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMVhqajNhUmRXeWhCSmZQbVRKQnhRYTkiLCJzdGF0dXMiOiJzdWNjZXNzIn19LHsibGMiOjEsInR5cGUiOiJjb25zdHJ1Y3RvciIsImlkIjpbImxhbmdjaGFpbiIsInNjaGVtYSIsIm1lc3NhZ2VzIiwiQUlNZXNzYWdlIl0sImt3YXJncyI6eyJjb250ZW50IjpbeyJzaWduYXR1cmUiOiJDQVFTMVFnS0VBZ1NHQUk0QVVJSWRHaHBibXRwYm1jU0RONlRmVTZORE05NzFsRWJ6eG9NdlJVQTFKOW94M2ZKUVlUOUlqQXJDUVFHSEJQNHBSSFBhdlh6UmxiTWJhWUhDNTdZaitHYnhncjBoZWx2ZDJPSUwvbzZSYnQ0UEZBM0ZhSUZVbGdxOGdjN0tRSTlGdEdudlNBa0xCUnpJc1VwLzJkNVJkMnpnRXNma0ZWZVhzbjhXWVlaa2luQVFWVnk2QTdCM0xhRUpXMHhiL3g4OERpNjdmZWR3eDBRK0ZsWnR3Y21UM0RjYWExMUp2ZXlIUnIzZG0rekdTWlA2Q1hlWE5KOG5yVFo3VzFZUlR5c0Q2dzdvN0xwOGgxUndxNGY0bzhqK1FVZlkwdmplK3pKWnN4SFBtUU50M3lrZUtoOTloZ242ZGJpejYvRUhxbWJIalNBdTFNNFNxRURmcUlLVGhIWWVuYW1BWm1jUTRYQU9vNm1kUTYrNzdWZ3BWMWxDMS9rNTJJaVVvSmprR2xRcm5MQ3Z1dGlJN0tOK3NnSzhjMisyS0NKclBSQ3ZWVitCa1N0dlA1aldXcXp5dHBLUStuSHpoTDQ0OE9ySUE5Z2RKQzM5QzhlRHJYZVJuSTd0NEZxbGtYR29NbFpBVjlVTWNyUUhSeE9NaTh0T3lUemk5b3lCRkRUU3NRK0xKcWVyMHR6MUZ0OWdlazZ2emc1MFNwVkFGUWRMRVJ2OFIxWXFNYWVJK3hzdlRBRVc5TVJndlJNa3pMdXpSVDJndXczakQrRmppcDM3eGdLNVFJMFIrQ3NPQW1RUG9UWml6emNJeUFVQXVDSkZDZ3EvZ0QvQ2liTHhveW1pVlVJMWFuOVNudDNLcE5Sb1dkdldJbmUzM1YwcHY4UEhnOXlXMkp1TE9xUUdpbExDc0svUTNvUXIzdXBFTVh4WEdqQ0w3V1VpenZSWkdGRUVWK1ZwbFl0N3Y0dEtaTWo1cGszdmRlTTBNSjVYWmc3UHZVc1J6eWN4Y25yVDNLbm9jM3FNK3R0cVpmVHNiOGQ2VS9wUWlmcTdHdWFoRk9IcnZyb3FnSkxqKy84MVl5MXBaRmtqZkpnWTBYa2x6QW5BNkNGcS9ZVDBneGJWK1RINUlTK1dhWU9VekszNlM4czVxcU13aG5ndjRJekUzY3RYWHFuaElaWE56bnlWQnFiWWEvR3gyYkNqY0VqMm41UnlHWVJTLytNY3VsZ0xCem5ROU1rRzJEN1FQM01xL253SFhLTUlhTFM4Vks4aEczZDByQWpsdm5hdVhTaE5ySnh0a2o0cWt3bnhENExyRWprMXVlK1BMY2JMcVlXd0FyOG4wcys0MWpyc0lQVnl5Szl2RmFFWnJBOUthQnhrSnlpcXg1MEdaTm43Y2t4K21rSTE4Z0g1Mml5VlB0VThGS01YanVnQ2w2bVBMaUhZaURRZCtHNjNMdWRtT0t5czlpdFZuQkRCTk1QMmIvSTcrYklWUmxoZUdhWldyNk1BVEtsODZCM3krMGt1T3JMUk15V2NkaWJjMmlRa1ptKzhmN2RUVXdiS2JCSnNLT0ZZNzMvOG5sTC9KaVlwano2blVJaytJcVNqYUZ5SzZYRmxDY3VrV3BXSHBESm5TdmJFemV5N3FIeUNxSitJK29uOGlQOEdPdGVtcFFhUzAvNGtaR1N3bHJFNnhPajZFd0dwZmpiRFpLSGtQLzJSQWROWVBRcTN6N2JkVkpNSk5uRk16NmUzN216TXBDQmlod1BkU0ZUUkUzU0NZNjZ3dlRnSnpHQ1V5aEhiU2k1aTFhenpPWGQ4dUhDU3BULzZMMW82VlUwcGpvUjhLYWE0d0NXSEdmN1AvV0VkK2F2eXAwMkpudTFjbXBXMCtHcSt2SzloMHRkYjJJQ2xNL2VOZmRvdVFQK2VwOE1KVWpyYURSSUNtQUpRWTVvYlM3MXZuaU9vK3JlYUZ5MHpSZ0IiLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsic2lnbmF0dXJlIjoiQ0FRUy9SRUtFUWdTR0FJNEFVSUpibUZ5Y21GMGFXOXVFZ3dGaDNBVkpaUDNzOHkxSHEwYURHNzZ1aEhPTmFTY1hBR1NoaUl3R2dqTFhIVXgxeVpya0xQbGpqelRPUXljcExKaUtWd1hjeG0vRXpLSm0xczhNNVd4dVJFV0VsZFhjb2FOMC9tc0twa1JNVCtlTGI1aWg5ckFIVUxUb21VTnNVd0pldTBqckplNEdMYXp5N3NQNUt0akVPRUJLUXNYVWZvUTdmRGtaT1FVSFppOXp6UU40aEZObVlKVWYzT1ZxZjlUUU5kQzErbS8vSEFaUGZXbUF5Nkh0RHZtN2tnQkprQ0VvaUZubTd0bi9QWDFoWEdQUmdXYzZCVFhVTXIyV0E4YUt5cWdnUlViTUgyK0pZdjVpV1pkZENjMXVwMU5icjNzRjdXUGNvdDlaQmh5SFFYcjY5MnplSms2U0pneWdJVnJBU2JNbHkyYkJ4VFdKQjYyM2dWZ3pwQ21WU0xaRnBrS1hnUGNwOGhHeURLT3ZGNlNpUVNIcUdSeFFvZnI3VnMxYVBoZlppeFJPSkdEK01scmtnZ1ZBbkdNeVlJRFVacGF2dENlaVUxSm1CU3owMDg0YlhQdFQvdG5wc0tPblNQWVpRbzNCMEJYS2JxQS9Xc0tuZzgvVjNaOFV1YWpvNklWWjJoZ0ExV0RkdXFRT1FrRXhHVTZwRnVMaGViSWp1V3hTTkkvRGZmdlIvL0xOcUNmdmRuZjBodm9QdER4ZngvU2F3SXg0L3NwcTl4YndGekh0TGdvSGcxRjJ3Rnc3Y2xQRGxnMVB5RC92MnlKRzNWOVQ4R0w2akdQRTZyaGZpbC91NEhOdmY3QlcwZFF6Q1RUWGNxWnk0bkJ5eTArcCtTN1p4Zk1sNEtweGEyZ09oaFNOamZwZnVYMFFwTDRrTUU4SjFubGlGTjFkOEtxRVB1REkxOEJRSFVoRC9VZlkyOW9BMHg1MTBGak0vQnNMWjRuNXNwcGtvSWk3Q0ZvUXEyYUFLVk5mQzFLaWxoSXBSRCsxZHlEdFpRa2xOOGdacE9FU0h3Nk1MMVR0VTB5ZkFjYm1xb3hKNmhoanp3SDZDTXNUUUVFeThNYlJCTStRS0trSVJkUkNQSEdGVzJMa3VSNTArSjBwRi90cjZwcVZ2SmVvemZpTW5yRWlsQjI3VFpQNG5LVEpHOHJkLy92Q1dlbjB5N0NBLzhkQko5Z2RwNXR4N05WQTQ1azFnZGN4RVIxZ1lvdU5YM24xdkczL0IxRjQrOTN3WFc5TXFvNlVzNTlUUW1yYjlOcWE3cFNRTlNySDA2dzFHeHlCQWxrRmZxQWxhZmsxNmFNcHE0MTZUSllwR3k0M1dDVWlDY0xXbGhpV2d6MnNnT0xBWFA5YTV3ZVdBS1JDOEo2amtjVWtEZVhuVytIRlNpUE02KzlCbzZrVmhMVWhONXJjNG1IWk9acEY3cHRQcHNRTjRYQ3FMUHNxcFJILzhvNVhweUM2Wm5lVGVGMkV4dGpod1pWalhEd01iSXpWWUlidzF5MlBQR3c4ZEM3cGM0c0lWU25JNG9RQUgwdjY1MGdoZmhtTWV2V1c0aDRQeWxidy9ERzRmZGVUMys4ejBmOE1PU1R0ZmlweDA1L1F0dEhsQXZPRFJ4YTV5REN2T3o2bjNmMUJPME4vNE1iR3lPTTI3cVc0eDhtQlZvcnlVWldmc21oZmkwL0dmTnZCOCt5bFRHYllOWDI3T2NFRjVXWnROcWJXRXk3VjVHYm1aVVN4c3BjZzdGQlozajMvM0w3bXV2WlhvWmkxZjVlcnE0eTNsU0grb0Y3cVBGbnZpZXlMT0N3dWJjUXkrb3lvd2o0Y0QwaWRSZWNucCtzKzUyRDcrbXpGMWdwVytmK2NKYXNveGlnYnhrT2xvUzR5ZWlLdmdNNG5XU3BHRWoxMlJQeDVvODBFbXppWE45dDR6SnpXMWh0QTVoWEpKZUZ5bXBNWXR5Q1VDZXZSV1ZtclJZa3RRN1VuQ2E2dEwvQVZQaTFuaTVYQ1hIZkxPdXFmSzVLTFl4andWdGRoaGZtQ0x2cGFLVk5DRU9DZjRuTkdIcUQ5OHdVYjBlaldOd0RZQm5KR2tCUnc5UlZzcGZ3TmRvV29WY24ycXY4eXdHVXA3eWMvc2syTHBJNlZ6aVJ5dldxYkdORzFaSnFWVnV0UmM0NitzT0U0VlNlUzIvQ1ROS05hNk5NQ2YvaHh4YVVWc1lmS2RQK1MxTGM0aWFuS3Awc1V1Z1BwbXRveWRDMENQMlJ3am5ITGNuenhDbnZxeGxMUVdYTU5QVHN1MnlCY0hsV0pMdGNtZGcvTWNMYWd5N2w1a0c2c043WnhKTEQva0F4OEU2dXFvaFArSy9qTVNyWXVRSm5ocjR0ZjRiaVdGWTNKemYvS3BBWXlWU3ErcGJmRktYTWJMOW5IRW9aUVQxNVd5YWJkNkRNQXEzUVpPdGhNNXBVemROcVVYa2NySTFQd2pBU0FnNGRWVS8ybys5OG1UWG55aFA3VmxFa1RuZGRwd3lKRitZQ0NjMDFwcWE2Wlg5OXBuUitZT3BJcmI1RHRQY0txMHQ1Y0ZDRHlReE4xbFV1cVpjWXVqdlgvR0M3KzZJcFJUei9Oa0VZLzVjbnRPZlpKeWQ2U2tVeC9KUjVBSklSUENOZWliMHJjV25uRm4wWEs4czIyeDQwd0xqRXR1ZlhETzkreXJhZjRrSHhsQkkzU0IwbWVNamVwWDUwQ0poQXg3TmVra1JGbndvZWNMZnFJeXJqWHJnQVpzVE1QaVg5NHNuamRyemlzQVh0QkthbnVsaWxTeE9mVzJIU21mbmswTDJHWEhKSjNTK3ZDYmdSRW1sOWRla2ZtSGhtd0VZdHYza2ZodGg3RVJJd1ZUOGo2OU90SUJDbWVUSUp1QTdvNzNQWGMxTEoyd2h1Wks3RlplNk5UT3M2MU1QN0pWbEljNnRiaHRPQmszTlhVOHBCUE9DWTZRNTVkNjNGdHBVVS9LQ2JRMEsyV1E5bzVqTkd3Tk5KNkN3NUZEQ3QreDVZSmVQMWQyUXpEU2lxK2NTWnF3cUNaVmNOb0FmdEdHemdyUlo0dmtwYlAweFFaSEN0V3NVd0hWVGpyWTROZlJEakhkVlBSbjhKOVh0b29jL2hJV2txekZhY3pZVGxhcHUyUkNKcWU4YUVvM2p6V1pDRUo3QWlCekJZcmFZRGdPZmVpUXllSlVzYmJYSGRHS2lpdzIyQmRwNE0rWXo2NzFNWjBpZTlFV1JrczBRMkRIR1VUbmU5UzR0QUFKUEdiVWY0eHRDTW9KNDBRNFFXMDVRTFNaeTN3TkZBVUZZbVhFNEVDREZCOCtJRWVORkZGL3RnQW5maDhHdkZoLzF4WStMRStvek1taW5qU1J3aHEwOXpRQUVrTkZ5dTBJS2d2RGt5T0kraWNjRjdkb1dMVFVhc1B4TEgxeEdQQlNLa2VXQ25Tb1gvNEMyYzlkdTY2QmJSWGFtSHFhZStWQ2lTNkJOOTUzaVlLT3U4ck1rbmVXK1RGalYwQ3pBZjhQTkc5TEJheVVnWTVVdzl4MFNhcDZ6STRQcGFaS0g0ZDlrdGtGVSs5MXlCYk41U1J0ZGQ4L0VRYXRKWG1CTmZ3N2tScmxsK0xqd1p3ZVdMRWIwalQxUnBHdURxSExkTFRNTWdvd1NjenJwMnpDc0pkUUgvc2VhWXJwd2tDRlI4ZFJqTTdKUlVrVmExQTBTdmRaQVg3NXZ1QnZ2TEJ6TjJjK1JidDVvcXk5M2xxSUJJaktiN3cyVWtSVGI0cEJXY05LTlBVZ3QzYXovUHhReEVhc2FsdGRtdXl2c1hZYnVyM1ZQTm44SnpOLzZKNU4rUUNuejU3MVRjY2FwQ21rUHZGRStnYXlYVStVRjFWS1l5RmNVdmxldjdsc1FsbWpxeksyUUE4WDdRM1R6RmFwcWh2SktsdDdkekJIcmFxWkJnUGdiOU40b0tOYXlpOEZaVDcxK2thWWhJaEZyRVNLWVJIZkhrajVuM2tmaGdMeSt4K2pMRGJLeE1nZ2tnSG5KejlSdUVIWlhCV3RrWUFRPT0iLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsiaWQiOiJ0b29sdV8wMUg2ZlY4SnZSSk52WHk0N2VSbmQzakgiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6e30sIm5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QU1SZFMyMUZlSmZ6UWhNM3MiLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzM5Niwib3V0cHV0X3Rva2VucyI6NzcwLCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjo3Mzl9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InNrZXB0aWMiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtNGFlOS03NmMwLTg5YTYtNGYwYjExNGUzMDMwLTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoidHJhbnNmZXJfdG9fdmVyaWZpZXIiLCJhcmdzIjp7fSwiaWQiOiJ0b29sdV8wMUg2ZlY4SnZSSk52WHk0N2VSbmQzakgiLCJ0eXBlIjoidG9vbF9jYWxsIn1dLCJ1c2FnZV9tZXRhZGF0YSI6eyJpbnB1dF90b2tlbnMiOjczOTYsIm91dHB1dF90b2tlbnMiOjc3MCwidG90YWxfdG9rZW5zIjo4MTY2LCJpbnB1dF90b2tlbl9kZXRhaWxzIjp7ImNhY2hlX3JlYWQiOjAsImNhY2hlX2NyZWF0aW9uIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VuX2RldGFpbHMiOnsicmVhc29uaW5nIjo3Mzl9fSwiaW52YWxpZF90b29sX2NhbGxzIjpbXX19LHsibGMiOjEsInR5cGUiOiJjb25zdHJ1Y3RvciIsImlkIjpbImxhbmdjaGFpbiIsInNjaGVtYSIsIm1lc3NhZ2VzIiwiVG9vbE1lc3NhZ2UiXSwia3dhcmdzIjp7ImNvbnRlbnQiOiJTdWNjZXNzZnVsbHkgdHJhbnNmZXJyZWQgdG8gdmVyaWZpZXIiLCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsImlkIjoiYzA2NDU2M2EtOTc0Zi00ZjBmLWFiNGEtMDgwZTYzYzAxY2Y1IiwidG9vbF9jYWxsX2lkIjoidG9vbHVfMDFINmZWOEp2UkpOdlh5NDdlUm5kM2pIIiwic3RhdHVzIjoic3VjY2VzcyJ9fSx7ImxjIjoxLCJ0eXBlIjoiY29uc3RydWN0b3IiLCJpZCI6WyJsYW5nY2hhaW4iLCJzY2hlbWEiLCJtZXNzYWdlcyIsIkFJTWVzc2FnZSJdLCJrd2FyZ3MiOnsiY29udGVudCI6W3sic2lnbmF0dXJlIjoiQ0FRUzlnVUtFQWdTR0FJNEFVSUlkR2hwYm10cGJtY1NER0Z5SjFNWG9ZK0FpNk5rYnhvTVJkMlRyVWZVVTdXclBwQTJJakRneThkVXBsY1pLYzdEbkp3TkxUQmZINVVOUituR25ha1NQUWlhOFVWSXpyamtPbkoyZkxyV1VzQTJvRU84eVJzcWt3WGtrRENzeWhCOTdkd2o1N1JNdDkxWHpnV2hMOWx1TkVHTzkyeVcwck5GR29NU1VWT3hQSXJGYzVSanlyaEtFYzMrMVJucHhDa3lscStMTmxRNU5GeExuNnYyY0Y1RHpxcHZCSE5SN1hBaWcrdXJKSVUzZ1h5ZENrYUdaWkRldEp0UU9VT1hnWmk5ejkxMDBOcGRBRWtYRG8ySUNIQ0dUR3BWYzBKL3BiWHhXUkJ4YjZCMS9HbDBZam42OFY2SGxlc0g3SW9DN0dSLzNId1J1OU5TREJHNHR0ZlFqRVdjZytQZUFlYTRSbjdqengrVG05SDdMMWpvVEtWZ0RiUWc1aUtNTGZqdlBxbG5rc0NMdFhPdDhqU0lTNU9ZTE43YS83VFg2L3c1ZDM0dm53OHZQRG1VMVVZaTBRNzJxVVdoVWEydnc4T1hOcGNLTDdCcDBsTmZCUzhydkhZK3ZCWTgvaGFBb1RMM3d2TEZVMWZUKzR6L3ZBM0Jjc2lnWkRZbGZKZURZY3hYbVJ1cXh6eEVHcFhDUmU5czFIV3RJRCs0MXBIdTZscFpIa1hhZ2hwYzZUWkhMeE53Y1ZoVGI2Zk1RN1FvKzJMNlB2a2xoMW0zMnBlb2R4ZWcyWWlQaGRwbDN3L3hrT0V1NS9KRUY3WkJ4cGRVTCt1M3drTzVwVWxzcWZnWDd1aHp2N0hDMldwU1dWaW1yd2lRd1FPVXgwK0pQckx0dytsWWZxN0kzanJ1bzBHVzBndVd2bnVmN0pNM1lMajg0cStKZ0hvbW5TU1publptckwwVWFSdnJWeUxvODc3RjM5Y2NMUzdYb1pubnVpOEo0UDU4WlRtaUxrRUplR2N4MUl1YWcyZTMzMzM5TzdZZXcvNTl0TGJRdElHaDlWV013SEJuRngzOVM4LzRMRUZySzVIbGRQUWhPcmxveVR0a1JzNjZpUVJLdHhNS1MwVkk2UVRLWkRBYnM3YTdIMXlCdittSmk5WGc0dUtteVU2cW5uN3pFS2pqWTJiSWxDd2V6eDRMTEdQNUhEM1pyNmZac21kRzFVSHpEc0J2WUcyb0NJMG9xRUtNK3ZhbHNUNmhVOE1SamJRQkQvSko4SkpOL2lCR2xoQkFVSG8wVW1oaHRSMmV1VFZwWldsTW5obXBYQWRLRFhTZlBkYVBvaGdCIiwidGhpbmtpbmciOiIiLCJ0eXBlIjoidGhpbmtpbmcifSx7ImlkIjoidG9vbHVfMDFYRUF1aFFIQ0JkeUN3dHltWndGZkRpIiwiY2FsbGVyIjp7InR5cGUiOiJkaXJlY3QifSwiaW5wdXQiOnt9LCJuYW1lIjoidHJhbnNmZXJfdG9fcmVkX3RlYW0iLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2Zkd0FzYWJEcXVlZ0hpNllVcENuIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjc1MDksIm91dHB1dF90b2tlbnMiOjg4LCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjo1NX0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoidmVyaWZpZXIiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtNjY4Mi03ZDIxLThjMTUtZmZiY2U5NDIyYzlmLTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoidHJhbnNmZXJfdG9fcmVkX3RlYW0iLCJhcmdzIjp7fSwiaWQiOiJ0b29sdV8wMVhFQXVoUUhDQmR5Q3d0eW1ad0ZmRGkiLCJ0eXBlIjoidG9vbF9jYWxsIn1dLCJ1c2FnZV9tZXRhZGF0YSI6eyJpbnB1dF90b2tlbnMiOjc1MDksIm91dHB1dF90b2tlbnMiOjg4LCJ0b3RhbF90b2tlbnMiOjc1OTcsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjU1fX0sImludmFsaWRfdG9vbF9jYWxscyI6W119fSx7ImxjIjoxLCJ0eXBlIjoiY29uc3RydWN0b3IiLCJpZCI6WyJsYW5nY2hhaW4iLCJzY2hlbWEiLCJtZXNzYWdlcyIsIlRvb2xNZXNzYWdlIl0sImt3YXJncyI6eyJjb250ZW50IjoiU3VjY2Vzc2Z1bGx5IHRyYW5zZmVycmVkIHRvIHJlZF90ZWFtIiwidHlwZSI6InRvb2wiLCJuYW1lIjoidHJhbnNmZXJfdG9fcmVkX3RlYW0iLCJpZCI6Ijk4MmMwMTI2LWU2YWItNDU4YS05MjJlLTdiMjY2ZTgzYzJiMiIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxWEVBdWhRSENCZHlDd3R5bVp3RmZEaSIsInN0YXR1cyI6InN1Y2Nlc3MifX1dXX0=" + } + }, + { + "key": "gen_ai.usage.input_tokens", + "value": { + "intValue": "7416" + } + }, + { + "key": "gen_ai.usage.output_tokens", + "value": { + "intValue": "151" + } + }, + { + "key": "gen_ai.usage.total_tokens", + "value": { + "intValue": "7567" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJnZW5lcmF0aW9ucyI6W1t7InRleHQiOiIiLCJnZW5lcmF0aW9uX2luZm8iOm51bGwsInR5cGUiOiJDaGF0R2VuZXJhdGlvbiIsIm1lc3NhZ2UiOnsibGMiOjEsInR5cGUiOiJjb25zdHJ1Y3RvciIsImlkIjpbImxhbmdjaGFpbiIsInNjaGVtYSIsIm1lc3NhZ2VzIiwiQUlNZXNzYWdlIl0sImt3YXJncyI6eyJjb250ZW50IjpbeyJzaWduYXR1cmUiOiJDQVFTd2djS0VBZ1NHQUk0QVVJSWRHaHBibXRwYm1jU0RPUlhnZTl1TDE4MzJSTVF3Qm9NalF3QXdBSGI5aVAxb2pQR0lqRERTY2FsNDg2YmVDSmJ0RzgvRDJZOWc5UzV4SVBmam9IZy9keHdYNUh5cFNRRUJiakZpVFNiUEVDWFNGelY2WUlxM3diTGNteVBidnVzR25EZXhGZ3grM3NJWHp2cXlGVi82OXhoaWhNblNwZlRDSHkwKzB6U3UxV3BZVS9XVXBJZm9hU1hUY1RhMXFrTTh4OEVrL3kvMWprS01aVWZERmE5M3VoVUp4T0M0K1FnUFdCbC8rWHkydDgyTDJ4L2VicTdRTFFUcXRSMGpYZWFVUlFtM0NvNkp2T2dXbUR2TXBFZFRlVFdFM2ZEaDF0enpWS3NmbHVtNGQ4OWI2Q2FLdG44bmxRZDdSaE45OXZOUU1RSEc1VjRPdVZOZHJIUFpacEZPdWhkZWpzTUZXWHlCKzU1c2xHQWQ4d0VYZ0ZLYkkva25PWFZHekwvTkRGOEd0eTF0TlpsdTJmZFNDQXNZV01zRlFQMWNid0xlUUdzenlYekpNVlpKMkk5eEJQa3l6MUhPWENQeWg1RitqMmdSVFBpa1ZCM0FSQzZ0ZkIvbERTVWVLQlZaUFFTOHBZL0I0ZUNzK0lqamFCTldydUx6ZnFDc2M2eEwwQ3VRUFNZeVFYYVlGallyU1JOY0M3MFdRQldUNVZWaHAwbUE5eVpER1liUlp0OUk1T0s5UWtRbXVKNXpERmFNRU9YelNyc0M0bHRlUDZNZGltaU1panZzSDNpcStjNldVWW9sUmozbmVJODhFYUhJekxBR1JxNjRCTkNMemRBWUwycEVVMnNiNXVHNzhQS1ZvVVVDYlZKTDVqcERPaExPVE8xYlFzVHVvVXBuUlJyTFFEL2tJelYrbGpwVkJ3d2ZKY3lxNzVUSkEvQ3RQci94cjJTWjlCMWIyc2JqRzZqRDBsaHRvQ2RkZURnY3FnbC9MU1NrY3VhWVk4eFBXQ3krTHROaGNxVGhmbWZ2bkhMSklwTDZnamJqVy9PUXBSZlZVd2Z3MWpPYjliV1Z3Y2dIaE5ZZktZR1B0TThvdGg1eHdMWVJSVmd6cU1LT3ZzN2ZhbFhSaFZ6WjJTUDZJdzBCTW1BQVo2SGpmQ2w5TCt0UHFKTmxuUVFPSHBvV2p1bTRCK1hzWWxkRkVLR1FXUXFzZDU5aU5QdmFvOXAvc0NPdDdyR3FDb3pjbWdpRW00dzY2ZXovUUlEeWI0Nm4wK3RjTkpQVWNvQkt2US9qRHdFNlZ3bWtRL25FQ2pGcDNVZERUK2tXWVFTTmQ4ajhWQlpPcEc4TGxxVVlyRVpDdjBxN0JzMXZjTnJkWWlhRm9TMzJvc2F5bldrK0h0M09IdUZXYkVGOEpsc0ZUUUFPMDVWdllrZWNmMFYvb1RIc0c1aHlPT3M3VUVhREZxN3VhSnV6V1ZoaDVwQW9qSWFCY1I4ZlZsbjJQTFJUWDdpczJuc0ErUXJkaWRlUGNadEZ0Z3hBaUtZZTFpMnNTRmk0dm04RU9rQ2JRNjN3ZFFlWTRVcnI1UHVvTUV3eFVVTGpnS2FnaUFtc0pEcTRSclZtN3NPNCs2WXVBOWo2bDJ1eWxrT0VjNjd2Zi81ZmtYdUNzaUFwTkw2a2ViOGMrdkwwbjRGWEdkemorYXBTRGZGR0JnQiIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJpZCI6InRvb2x1XzAxVkJyV2RXRVdZZ2l0NGtOWWVoQk1VNSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX2VkaXRvciIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QXpmanFVb3BvRW1aTGFxbTEiLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzQxNiwib3V0cHV0X3Rva2VucyI6MTUxLCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjoxMTl9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTZjZTYtNzI1MS1iNzk2LTRmNjQzMzNmNjdjNi0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6InRyYW5zZmVyX3RvX2VkaXRvciIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxVkJyV2RXRVdZZ2l0NGtOWWVoQk1VNSIsInR5cGUiOiJ0b29sX2NhbGwifV0sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzQxNiwib3V0cHV0X3Rva2VucyI6MTUxLCJ0b3RhbF90b2tlbnMiOjc1NjcsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjExOX19LCJpbnZhbGlkX3Rvb2xfY2FsbHMiOltdfX19XV0sImxsbV9vdXRwdXQiOnsiaWQiOiJtc2dfMDExQ2Zkd0F6ZmpxVW9wb0VtWkxhcW0xIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjc0MTYsIm91dHB1dF90b2tlbnMiOjE1MSwib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6MTE5fSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCJ9LCJydW4iOm51bGwsInR5cGUiOiJMTE1SZXN1bHQifQ==" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "59d91c7c0791b7de", + "parentSpanId": "ad48a4e078446202", + "name": "red_team", + "kind": 1, + "startTimeUnixNano": "1790968032481995008", + "endTimeUnixNano": "1790968035039021056", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "e30=" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "red_team" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "4" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "red_team" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:red_team\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"red_team\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "red_team:15ccef64-4162-9e02-e87c-4cb0eb6ea16b" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "red_team" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "red_team:15ccef64-4162-9e02-e87c-4cb0eb6ea16b" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "seq:step:1" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W3siY29udGVudCI6IkZvciBhIHN5bnRoZXRpYyB0cmFjaW5nIGZpeHR1cmUsIGV4cGxhaW4gUHl0aG9uIHR1cGxlIHZlcnN1cyBsaXN0IGltbXV0YWJpbGl0eSBpbiBhdCBtb3N0IDEyMCB3b3Jkcy4gVXNlIG9ubHkgb2ZmaWNpYWwgUHl0aG9uIGRvY3VtZW50YXRpb24gaWYgeW91IHNlYXJjaC4gSGF2ZSB0aGUgc2tlcHRpYyBhbmQgdmVyaWZpZXIgY2hlY2sgaXQsIHRoZW4gaGF2ZSB0aGUgZWRpdG9yIHdyaXRlIC9hbnN3ZXIubWQuIEtlZXAgdGhlIHJ1biBzaG9ydCBhbmQgZG8gbm90IGFzayBmb2xsb3ctdXAgcXVlc3Rpb25zLiIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoiaHVtYW4iLCJpZCI6IjQ2MjA0MzA1LWY4ZWUtNDM4NC05Y2U4LWZjZDgwYmZjZGY3NSJ9LHsiY29udGVudCI6W3siaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9LCJuYW1lIjoid2ViX3NlYXJjaCIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHc5YVc5YnJlblBxZ3liZk53UCIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjoyODQ1LCJvdXRwdXRfdG9rZW5zIjo3Mywib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6MH0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoicmVzZWFyY2hlciIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy0yMWEzLTcwMDAtYTM4MS0yODRhNmEwYzAzZjctMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ3ZWJfc2VhcmNoIiwiYXJncyI6eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9LCJpZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6Mjg0NSwib3V0cHV0X3Rva2VucyI6NzMsInRvdGFsX3Rva2VucyI6MjkxOCwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fSx7ImNvbnRlbnQiOiJbe1widGl0bGVcIjogXCI1LiBEYXRhIFN0cnVjdHVyZXMgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3R1dG9yaWFsL2RhdGFzdHJ1Y3R1cmVzLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcIllvdSBtaWdodCBoYXZlIG5vdGljZWQgdGhhdCBtZXRob2RzIGxpa2VgaW5zZXJ0YCxgcmVtb3ZlYCBvcmBzb3J0YCB0aGF0IG9ubHkgbW9kaWZ5IHRoZSBsaXN0IGhhdmUgbm8gcmV0dXJuIHZhbHVlIHByaW50ZWQgXFx1MjAxMyB0aGV5IHJldHVybiB0aGUgZGVmYXVsdGBOb25lYC4gWzFdIFRoaXMgaXMgYSBkZXNpZ24gcHJpbmNpcGxlIGZvciBhbGwgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgaW4gUHl0aG9uLlxcbi4uLlxcbiMjIDUuMy4gVHVwbGVzIGFuZCBTZXF1ZW5jZXNcXHUwMGI2XFxuXFxuV2Ugc2F3IHRoYXQgbGlzdHMgYW5kIHN0cmluZ3MgaGF2ZSBtYW55IGNvbW1vbiBwcm9wZXJ0aWVzLCBzdWNoIGFzIGluZGV4aW5nIGFuZCBzbGljaW5nIG9wZXJhdGlvbnMuIFRoZXkgYXJlIHR3byBleGFtcGxlcyBvZiBzZXF1ZW5jZSBkYXRhIHR5cGVzIChzZWUgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBsaXN0LCB0dXBsZSwgcmFuZ2UpLiBTaW5jZSBQeXRob24gaXMgYW4gZXZvbHZpbmcgbGFuZ3VhZ2UsIG90aGVyIHNlcXVlbmNlIGRhdGEgdHlwZXMgbWF5IGJlIGFkZGVkLiBUaGVyZSBpcyBhbHNvIGFub3RoZXIgc3RhbmRhcmQgc2VxdWVuY2UgZGF0YSB0eXBlOiB0aGUgdHVwbGUuXFxuXFxuQSB0dXBsZSBjb25zaXN0cyBvZiBhIG51bWJlciBvZiB2YWx1ZXMgc2VwYXJhdGVkIGJ5IGNvbW1hcywgZm9yIGluc3RhbmNlOlxcbi4uLlxcbj4+PiAjIFR1cGxlcyBhcmUgaW1tdXRhYmxlOlxcbj4+PiB0WzBdID0gODg4ODhcXG5UcmFjZWJhY2sgKG1vc3QgcmVjZW50IGNhbGwgbGFzdCk6XFxuICBGaWxlIFxcXCI8c3RkaW4+XFxcIiwgbGluZSAxLCBpbiA8bW9kdWxlPlxcblR5cGVFcnJvcjogJ3R1cGxlJyBvYmplY3QgZG9lcyBub3Qgc3VwcG9ydCBpdGVtIGFzc2lnbm1lbnRcXG4+Pj4gIyBidXQgdGhleSBjYW4gY29udGFpbiBtdXRhYmxlIG9iamVjdHM6XFxuPj4+IHYgPSAoWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuPj4+IHZcXG4oWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuLi4uXFxuQXMgeW91IHNlZSwgb24gb3V0cHV0IHR1cGxlcyBhcmUgYWx3YXlzIGVuY2xvc2VkIGluIHBhcmVudGhlc2VzLCBzbyB0aGF0IG5lc3RlZCB0dXBsZXMgYXJlIGludGVycHJldGVkIGNvcnJlY3RseTsgdGhleSBtYXkgYmUgaW5wdXQgd2l0aCBvciB3aXRob3V0IHN1cnJvdW5kaW5nIHBhcmVudGhlc2VzLCBhbHRob3VnaCBvZnRlbiBwYXJlbnRoZXNlcyBhcmUgbmVjZXNzYXJ5IGFueXdheSAoaWYgdGhlIHR1cGxlIGlzIHBhcnQgb2YgYSBsYXJnZXIgZXhwcmVzc2lvbikuIEl0IGlzIG5vdCBwb3NzaWJsZSB0byBhc3NpZ24gdG8gdGhlIGluZGl2aWR1YWwgaXRlbXMgb2YgYSB0dXBsZSwgaG93ZXZlciBpdCBpcyBwb3NzaWJsZSB0byBjcmVhdGUgdHVwbGVzIHdoaWNoIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzLCBzdWNoIGFzIGxpc3RzLlxcblxcblRob3VnaCB0dXBsZXMgbWF5IHNlZW0gc2ltaWxhciB0byBsaXN0cywgdGhleSBhcmUgb2Z0ZW4gdXNlZCBpbiBkaWZmZXJlbnQgc2l0dWF0aW9ucyBhbmQgZm9yIGRpZmZlcmVudCBwdXJwb3Nlcy4gVHVwbGVzIGFyZSBpbW11dGFibGUsIGFuZCB1c3VhbGx5IGNvbnRhaW4gYSBoZXRlcm9nZW5lb3VzIHNlcXVlbmNlIG9mIGVsZW1lbnRzIHRoYXQgYXJlIGFjY2Vzc2VkIHZpYSB1bnBhY2tpbmcgKHNlZSBsYXRlciBpbiB0aGlzIHNlY3Rpb24pIG9yIGluZGV4aW5nIChvciBldmVuIGJ5IGF0dHJpYnV0ZSBpbiB0aGUgY2FzZSBvZiBuYW1lZHR1cGxlcykuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG5Bbm90aGVyIHVzZWZ1bCBkYXRhIHR5cGUgYnVpbHQgaW50byBQeXRob24gaXMgdGhlIGRpY3Rpb25hcnkgKHNlZSBNYXBwaW5nIFR5cGVzIFxcdTIwMTQgZGljdCkuIERpY3Rpb25hcmllcyBhcmUgc29tZXRpbWVzIGZvdW5kIGluIG90aGVyIGxhbmd1YWdlcyBhcyBcXHUyMDFjYXNzb2NpYXRpdmUgbWVtb3JpZXNcXHUyMDFkIG9yIFxcdTIwMWNhc3NvY2lhdGl2ZSBhcnJheXNcXHUyMDFkLiBVbmxpa2Ugc2VxdWVuY2VzLCB3aGljaCBhcmUgaW5kZXhlZCBieSBhIHJhbmdlIG9mIG51bWJlcnMsIGRpY3Rpb25hcmllcyBhcmUgaW5kZXhlZCBieSBrZXlzLCB3aGljaCBjYW4gYmUgYW55IGltbXV0YWJsZSB0eXBlOyBzdHJpbmdzIGFuZCBudW1iZXJzIGNhbiBhbHdheXMgYmUga2V5cy4gVHVwbGVzIGNhbiBiZSB1c2VkIGFzIGtleXMgaWYgdGhleSBjb250YWluIG9ubHkgc3RyaW5ncywgbnVtYmVycywgb3IgdHVwbGVzOyBpZiBhIHR1cGxlIGNvbnRhaW5zIGFueSBtdXRhYmxlIG9iamVjdCBlaXRoZXIgZGlyZWN0bHkgb3IgaW5kaXJlY3RseSwgaXQgY2Fubm90IGJlIHVzZWQgYXMgYSBrZXkuIFlvdSBjYW5cXHUyMDE5dCB1c2UgbGlzdHMgYXMga2V5cywgc2luY2UgbGlzdHMgY2FuIGJlIG1vZGlmaWVkIGluIHBsYWNlIHVzaW5nIGluZGV4IGFzc2lnbm1lbnRzLCBzbGljZSBhc3NpZ25tZW50cywgb3IgbWV0aG9kcyBsaWtlIGFwcGVuZCgpIGFuZCBleHRlbmQoKS5cIl19LCB7XCJ0aXRsZVwiOiBcIjMuIERhdGEgbW9kZWwgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3JlZmVyZW5jZS9kYXRhbW9kZWwuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiVGhlIHZhbHVlIG9mIHNvbWUgb2JqZWN0cyBjYW4gY2hhbmdlLiBPYmplY3RzIHdob3NlIHZhbHVlIGNhbiBjaGFuZ2UgYXJlIHNhaWQgdG8gYmUgbXV0YWJsZTsgb2JqZWN0cyB3aG9zZSB2YWx1ZSBpcyB1bmNoYW5nZWFibGUgb25jZSB0aGV5IGFyZSBjcmVhdGVkIGFyZSBjYWxsZWQgaW1tdXRhYmxlLiAoVGhlIHZhbHVlIG9mIGFuIGltbXV0YWJsZSBjb250YWluZXIgb2JqZWN0IHRoYXQgY29udGFpbnMgYSByZWZlcmVuY2UgdG8gYSBtdXRhYmxlIG9iamVjdCBjYW4gY2hhbmdlIHdoZW4gdGhlIGxhdHRlclxcdTIwMTlzIHZhbHVlIGlzIGNoYW5nZWQ7IGhvd2V2ZXIgdGhlIGNvbnRhaW5lciBpcyBzdGlsbCBjb25zaWRlcmVkIGltbXV0YWJsZSwgYmVjYXVzZSB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGl0IGNvbnRhaW5zIGNhbm5vdCBiZSBjaGFuZ2VkLiBTbywgaW1tdXRhYmlsaXR5IGlzIG5vdCBzdHJpY3RseSB0aGUgc2FtZSBhcyBoYXZpbmcgYW4gdW5jaGFuZ2VhYmxlIHZhbHVlLCBpdCBpcyBtb3JlIHN1YnRsZS4pIEFuIG9iamVjdFxcdTIwMTlzIG11dGFiaWxpdHkgaXMgZGV0ZXJtaW5lZCBieSBpdHMgdHlwZTsgZm9yIGluc3RhbmNlLCBudW1iZXJzLCBzdHJpbmdzIGFuZCB0dXBsZXMgYXJlIGltbXV0YWJsZSwgd2hpbGUgZGljdGlvbmFyaWVzIGFuZCBsaXN0cyBhcmUgbXV0YWJsZS5cXG4uLi5cXG5Tb21lIG9iamVjdHMgY29udGFpbiByZWZlcmVuY2VzIHRvIG90aGVyIG9iamVjdHM7IHRoZXNlIGFyZSBjYWxsZWQgY29udGFpbmVycy4gRXhhbXBsZXMgb2YgY29udGFpbmVycyBhcmUgdHVwbGVzLCBsaXN0cyBhbmQgZGljdGlvbmFyaWVzLiBUaGUgcmVmZXJlbmNlcyBhcmUgcGFydCBvZiBhIGNvbnRhaW5lclxcdTIwMTlzIHZhbHVlLiBJbiBtb3N0IGNhc2VzLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIHZhbHVlIG9mIGEgY29udGFpbmVyLCB3ZSBpbXBseSB0aGUgdmFsdWVzLCBub3QgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGNvbnRhaW5lZCBvYmplY3RzOyBob3dldmVyLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIG11dGFiaWxpdHkgb2YgYSBjb250YWluZXIsIG9ubHkgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGltbWVkaWF0ZWx5IGNvbnRhaW5lZCBvYmplY3RzIGFyZSBpbXBsaWVkLiBTbywgaWYgYW4gaW1tdXRhYmxlIGNvbnRhaW5lciAobGlrZSBhIHR1cGxlKSBjb250YWlucyBhIHJlZmVyZW5jZSB0byBhIG11dGFibGUgb2JqZWN0LCBpdHMgdmFsdWUgY2hhbmdlcyBpZiB0aGF0IG11dGFibGUgb2JqZWN0IGlzIGNoYW5nZWQuXFxuLi4uXFxuIyMjIDMuMi41LiBTZXF1ZW5jZXNcXHUwMGI2XFxuLi4uXFxuIyMjIyAzLjIuNS4xLiBJbW11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbkFuIG9iamVjdCBvZiBhbiBpbW11dGFibGUgc2VxdWVuY2UgdHlwZSBjYW5ub3QgY2hhbmdlIG9uY2UgaXQgaXMgY3JlYXRlZC4gKElmIHRoZSBvYmplY3QgY29udGFpbnMgcmVmZXJlbmNlcyB0byBvdGhlciBvYmplY3RzLCB0aGVzZSBvdGhlciBvYmplY3RzIG1heSBiZSBtdXRhYmxlIGFuZCBtYXkgYmUgY2hhbmdlZDsgaG93ZXZlciwgdGhlIGNvbGxlY3Rpb24gb2Ygb2JqZWN0cyBkaXJlY3RseSByZWZlcmVuY2VkIGJ5IGFuIGltbXV0YWJsZSBvYmplY3QgY2Fubm90IGNoYW5nZS4pXFxuXFxuVGhlIGZvbGxvd2luZyB0eXBlcyBhcmUgaW1tdXRhYmxlIHNlcXVlbmNlczpcXG4uLi5cXG5UdXBsZXNcXG46IFRoZSBpdGVtcyBvZiBhIGB0dXBsZWAgYXJlIGFyYml0cmFyeSBQeXRob24gb2JqZWN0cy4gVHVwbGVzIG9mIHR3byBvciBtb3JlIGl0ZW1zIGFyZSBmb3JtZWQgYnkgY29tbWEtc2VwYXJhdGVkIGxpc3RzIG9mIGV4cHJlc3Npb25zLiBBIHR1cGxlIG9mIG9uZSBpdGVtIChhIFxcdTIwMThzaW5nbGV0b25cXHUyMDE5KSBjYW4gYmUgZm9ybWVkIGJ5IGFmZml4aW5nIGEgY29tbWEgdG8gYW4gZXhwcmVzc2lvbiAoYW4gZXhwcmVzc2lvbiBieSBpdHNlbGYgZG9lcyBub3QgY3JlYXRlIGEgdHVwbGUsIHNpbmNlIHBhcmVudGhlc2VzIG11c3QgYmUgdXNhYmxlIGZvciBncm91cGluZyBvZiBleHByZXNzaW9ucykuIEFuIGVtcHR5IHR1cGxlIGNhbiBiZSBmb3JtZWQgYnkgYW4gZW1wdHkgcGFpciBvZiBwYXJlbnRoZXNlcy5cXG4uLi5cXG4jIyMjIDMuMi41LjIuIE11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbk11dGFibGUgc2VxdWVuY2VzIGNhbiBiZSBjaGFuZ2VkIGFmdGVyIHRoZXkgYXJlIGNyZWF0ZWQuIFRoZSBzdWJzY3JpcHRpb24gYW5kIHNsaWNpbmcgbm90YXRpb25zIGNhbiBiZSB1c2VkIGFzIHRoZSB0YXJnZXQgb2YgYXNzaWdubWVudCBhbmQgYGRlbGAgKGRlbGV0ZSkgc3RhdGVtZW50cy5cXG4uLi5cXG5UaGVyZSBhcmUgY3VycmVudGx5IHR3byBpbnRyaW5zaWMgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlczpcXG5cXG5MaXN0c1xcbjogVGhlIGl0ZW1zIG9mIGEgbGlzdCBhcmUgYXJiaXRyYXJ5IFB5dGhvbiBvYmplY3RzLiBMaXN0cyBhcmUgZm9ybWVkIGJ5IHBsYWNpbmcgYSBjb21tYS1zZXBhcmF0ZWQgbGlzdCBvZiBleHByZXNzaW9ucyBpbiBzcXVhcmUgYnJhY2tldHMuIChOb3RlIHRoYXQgdGhlcmUgYXJlIG5vIHNwZWNpYWwgY2FzZXMgbmVlZGVkIHRvIGZvcm0gbGlzdHMgb2YgbGVuZ3RoIDAgb3IgMS4pXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjcgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvYnVpbHRpbnMvc3RkdHlwZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiY29sbGVjdGlvbiBjbGFzc2VzIGFyZSBtdXRhYmxlLiBUaGUgbWV0aG9kcyB0aGF0IGFkZCwgc3VidHJhY3QsIG9yIHJlYXJyYW5nZSB0aGVpciAuLi4gaW4gcGxhY2UsIGFuZCBkb25cXHUyMDE5dCByZXR1cm4gYSAuLi4gLCBuZXZlciByZXR1cm4gdGhlIGNvbGxlY3Rpb24gaW5zdGFuY2UgaXRzZWxmIGJ1dCBgTm9uZWAuXFxuLi4uXFxuIyMgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBgbGlzdGAsIGB0dXBsZWAsIGByYW5nZWBcXHUwMGI2XFxuXFxuVGhlcmUgYXJlIHRocmVlIGJhc2ljIHNlcXVlbmNlIHR5cGVzOiBsaXN0cywgdHVwbGVzLCBhbmQgcmFuZ2Ugb2JqZWN0cy4gQWRkaXRpb25hbCBzZXF1ZW5jZSB0eXBlcyB0YWlsb3JlZCBmb3IgcHJvY2Vzc2luZyBvZiBiaW5hcnkgZGF0YSBhbmQgdGV4dCBzdHJpbmdzIGFyZSBkZXNjcmliZWQgaW4gZGVkaWNhdGVkIHNlY3Rpb25zLlxcbi4uLlxcblRoZSBvcGVyYXRpb25zIGluIHRoZSBmb2xsb3dpbmcgdGFibGUgLi4uIG11dGFibGUgYW5kIGltbXV0YWJsZS4gVGhlIGBjb2xsZWN0aW9ucy4gLi4uIGlzIHByb3ZpZGVkIHRvIG1ha2UgLi4uIGVhc2llciB0byBjb3JyZWN0bHkgaW1wbGVtZW50IHRoZXNlIG9wZXJhdGlvbnMgb24gY3VzdG9tIHNlcXVlbmNlIHR5cGVzXFxuLi4uXFxuIyMjIEltbXV0YWJsZSBTZXF1ZW5jZSBUeXBlc1xcdTAwYjZcXG4uLi5cXG5tdXRhYmxlIHNlcXVlbmNlIHR5cGVzIGlzXFxuLi4uXFxuc3VwcG9ydCBhbGxvd3MgaW1tdXRhYmxlIHNlcXVlbmNlcywgLi4uICwgdG8gYmUgdXNlZCBhcyBgZGljdGAga2V5cyBhbmQgc3RvcmVkIGluIC4uLiBlbnNldGAgaW5zdGFuY2VzXFxuLi4uXFxuIyMjIE11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxuIyMjIExpc3RzXFx1MDBiNlxcblxcbkxpc3RzIGFyZSBtdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaG9tb2dlbmVvdXMgaXRlbXMgKHdoZXJlIHRoZSBwcmVjaXNlIGRlZ3JlZSBvZiBzaW1pbGFyaXR5IHdpbGwgdmFyeSBieSBhcHBsaWNhdGlvbikuXFxuLi4uXFxuIyMjIFR1cGxlc1xcdTAwYjZcXG5cXG5UdXBsZXMgYXJlIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhldGVyb2dlbmVvdXMgZGF0YSAoc3VjaCBhcyB0aGUgMi10dXBsZXMgcHJvZHVjZWQgYnkgdGhlIGBlbnVtZXJhdGUoKWAgYnVpbHQtaW4pLiBUdXBsZXMgYXJlIGFsc28gdXNlZCBmb3IgY2FzZXMgd2hlcmUgYW4gaW1tdXRhYmxlIHNlcXVlbmNlIG9mIGhvbW9nZW5lb3VzIGRhdGEgaXMgbmVlZGVkIChzdWNoIGFzIGFsbG93aW5nIHN0b3JhZ2UgaW4gYSBgc2V0YCBvciBgZGljdGAgaW5zdGFuY2UpLlxcbi4uLlxcbnR1cGxlKGl0ZXJhYmxlPVxcbi4uLlxcblRoZSBjb25zdHJ1Y3RvciBidWlsZHMgYSB0dXBsZSB3aG9zZSBpdGVtcyBhcmUgdGhlIHNhbWUgYW5kIGluIHRoZSBzYW1lIG9yZGVyIGFzIGl0ZXJhYmxlXFx1MjAxOXMgaXRlbXMuIGl0ZXJhYmxlIG1heSBiZSBlaXRoZXIgYSBzZXF1ZW5jZSwgYSBjb250YWluZXIgdGhhdCBzdXBwb3J0cyBpdGVyYXRpb24sIG9yIGFuIGl0ZXJhdG9yIG9iamVjdC4gSWYgaXRlcmFibGUgaXMgYWxyZWFkeSBhIHR1cGxlLCBpdCBpcyByZXR1cm5lZCB1bmNoYW5nZWQuIEZvciBleGFtcGxlLCBgdHVwbGUoJ2FiYycpYCByZXR1cm5zIGAoJ2EnLCAnYicsICdjJylgIGFuZCBgdHVwbGUoIFsxLCAyLCAzXSApYCByZXR1cm5zIGAoMSwgMiwgMylgLiBJZiBubyBhcmd1bWVudCBpcyBnaXZlbiwgdGhlIGNvbnN0cnVjdG9yIGNyZWF0ZXMgYSBuZXcgZW1wdHkgdHVwbGUsIGAoKWAuXFxuLi4uXFxuVHVwbGVzIGltcGxlbWVudCBhbGwgb2YgdGhlIGNvbW1vbiBzZXF1ZW5jZSBvcGVyYXRpb25zLlxcbi4uLlxcbkZvciBoZXRlcm9nZW5lb3VzIGNvbGxlY3Rpb25zIG9mIGRhdGEgd2hlcmUgYWNjZXNzIGJ5IG5hbWUgaXMgY2xlYXJlciB0aGFuIGFjY2VzcyBieSBpbmRleCwgYGNvbGxlY3Rpb25zLm5hbWVkdHVwbGUoKWAgbWF5IGJlIGEgbW9yZSBhcHByb3ByaWF0ZSBjaG9pY2UgdGhhbiBhIHNpbXBsZSB0dXBsZSBvYmplY3QuXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvbGlicmFyeS9zdGR0eXBlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJTb21lIGNvbGxlY3Rpb24gY2xhc3NlcyBhcmUgbXV0YWJsZS4gVGhlIG1ldGhvZHMgdGhhdCBhZGQsIHN1YnRyYWN0LCBvciByZWFycmFuZ2UgdGhlaXIgLi4uIGluIHBsYWNlLCBhbmQgZG9uXFx1MjAxOXQgcmV0dXJuIGEgLi4uICwgbmV2ZXIgcmV0dXJuIHRoZSBjb2xsZWN0aW9uIGluc3RhbmNlIGl0c2VsZiBidXQgYE5vbmVgLlxcbi4uLlxcbiMjIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgYGxpc3RgLCBgdHVwbGVgLCBgcmFuZ2VgXFx1MDBiNlxcblxcblRoZXJlIGFyZSB0aHJlZSBiYXNpYyBzZXF1ZW5jZSB0eXBlczogbGlzdHMsIHR1cGxlcywgYW5kIC4uLiBvYmplY3RzLiBBZGRpdGlvbmFsIHNlcXVlbmNlIHR5cGVzIHRhaWxvcmVkIGZvciBwcm9jZXNzaW5nIG9mIGJpbmFyeSBkYXRhIGFuZCB0ZXh0IHN0cmluZ3MgYXJlIGRlc2NyaWJlZCBpbiBkZWRpY2F0ZWQgc2VjdGlvbnMuXFxuLi4uXFxuVGhlIG9wZXJhdGlvbnMgaW4gdGhlIGZvbGxvd2luZyB0YWJsZSAuLi4gaW1tdXRhYmxlLiBUaGUgYCAuLi4gaXMgcHJvdmlkZWQgdG8gbWFrZSAuLi4gZWFzaWVyIHRvIGNvcnJlY3RseSBpbXBsZW1lbnQgdGhlc2Ugb3BlcmF0aW9ucyBvbiBjdXN0b20gc2VxdWVuY2UgdHlwZXNcXG4uLi5cXG4jIyMgSW1tdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbm9wZXJhdGlvbiB0aGF0IGltbXV0YWJsZSBzZXF1ZW5jZSAuLi4gYnkgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlcyBpcyAuLi4gYGhhc2goKWBcXG4uLi5cXG5UaGlzIHN1cHBvcnQgYWxsb3dzIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHN1Y2ggYXMgYHR1cGxlYCBpbnN0YW5jZXMsIHRvIGJlIHVzZWQgYXMgYGRpY3RgIGtleXMgYW5kIHN0b3JlZCBpbiBgc2V0YCBhbmQgYGZyb3plbnNldGAgaW5zdGFuY2VzLlxcbi4uLlxcbiMjIyBNdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbiMjIyBMaXN0c1xcdTAwYjZcXG5cXG5MaXN0cyBhcmUgbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhvbW9nZW5lb3VzIGl0ZW1zICh3aGVyZSB0aGUgcHJlY2lzZSBkZWdyZWUgb2Ygc2ltaWxhcml0eSB3aWxsIHZhcnkgYnkgYXBwbGljYXRpb24pLlxcbi4uLlxcbiMjIyBUdXBsZXNcXHUwMGI2XFxuXFxuVHVwbGVzIGFyZSBpbW11dGFibGUgc2VxdWVuY2VzLCB0eXBpY2FsbHkgdXNlZCB0byBzdG9yZSBjb2xsZWN0aW9ucyBvZiBoZXRlcm9nZW5lb3VzIGRhdGEgKHN1Y2ggYXMgdGhlIDItdHVwbGVzIHByb2R1Y2VkIGJ5IHRoZSBgZW51bWVyYXRlKClgIGJ1aWx0LWluKS4gVHVwbGVzIGFyZSBhbHNvIHVzZWQgZm9yIGNhc2VzIHdoZXJlIGFuIGltbXV0YWJsZSBzZXF1ZW5jZSBvZiBob21vZ2VuZW91cyBkYXRhIGlzIG5lZWRlZCAoc3VjaCBhcyBhbGxvd2luZyBzdG9yYWdlIGluIGEgYHNldGAgb3IgYGRpY3RgIGluc3RhbmNlKS5cXG4uLi5cXG5UaGUgY29uc3RydWN0b3IgYnVpbGRzIGEgdHVwbGUgd2hvc2UgaXRlbXMgYXJlIHRoZSBzYW1lIGFuZCBpbiB0aGUgc2FtZSBvcmRlciBhcyBpdGVyYWJsZVxcdTIwMTlzIGl0ZW1zLiBpdGVyYWJsZSBtYXkgYmUgZWl0aGVyIGEgc2VxdWVuY2UsIGEgY29udGFpbmVyIHRoYXQgc3VwcG9ydHMgaXRlcmF0aW9uLCBvciBhbiBpdGVyYXRvciBvYmplY3QuIElmIGl0ZXJhYmxlIGlzIGFscmVhZHkgYSB0dXBsZSwgaXQgaXMgcmV0dXJuZWQgdW5jaGFuZ2VkLiBGb3IgZXhhbXBsZSwgYHR1cGxlKCdhYmMnKWAgcmV0dXJucyBgKCdhJywgJ2InLCAnYycpYCBhbmQgYHR1cGxlKCBbMSwgMiwgM10gKWAgcmV0dXJucyBgKDEsIDIsIDMpYC4gSWYgbm8gYXJndW1lbnQgaXMgZ2l2ZW4sIHRoZSBjb25zdHJ1Y3RvciBjcmVhdGVzIGEgbmV3IGVtcHR5IHR1cGxlLCBgKClgLlxcbi4uLlxcblR1cGxlcyBpbXBsZW1lbnQgYWxsIG9mIHRoZSBjb21tb24gc2VxdWVuY2Ugb3BlcmF0aW9ucy5cXG4uLi5cXG5Gb3IgaGV0ZXJvZ2VuZW91cyBjb2xsZWN0aW9ucyBvZiBkYXRhIHdoZXJlIGFjY2VzcyBieSBuYW1lIGlzIGNsZWFyZXIgdGhhbiBhY2Nlc3MgYnkgaW5kZXgsIGBjb2xsZWN0aW9ucy5uYW1lZHR1cGxlKClgIG1heSBiZSBhIG1vcmUgYXBwcm9wcmlhdGUgY2hvaWNlIHRoYW4gYSBzaW1wbGUgdHVwbGUgb2JqZWN0LlwiXX0sIHtcInRpdGxlXCI6IFwiNS4gRGF0YSBTdHJ1Y3R1cmVzIFxcdTIwMTQgUHl0aG9uIDMuMTAuMjAgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMuMTAvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiWW91IG1pZ2h0IGhhdmUgbm90aWNlZCB0aGF0IG1ldGhvZHMgbGlrZSBgaW5zZXJ0YCwgYHJlbW92ZWAgb3IgYHNvcnRgIHRoYXQgb25seSBtb2RpZnkgdGhlIGxpc3QgaGF2ZSBubyByZXR1cm4gdmFsdWUgcHJpbnRlZCBcXHUyMDEzIHRoZXkgcmV0dXJuIHRoZSBkZWZhdWx0IGBOb25lYC4gMSBUaGlzIGlzIGEgZGVzaWduIHByaW5jaXBsZSBmb3IgYWxsIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIGluIFB5dGhvbi5cXG4uLi5cXG4jIyA1LjMuIFR1cGxlcyBhbmQgU2VxdWVuY2VzXFx1MDBiNlxcblxcbldlIHNhdyB0aGF0IGxpc3RzIGFuZCBzdHJpbmdzIGhhdmUgbWFueSBjb21tb24gcHJvcGVydGllcywgc3VjaCBhcyBpbmRleGluZyBhbmQgc2xpY2luZyBvcGVyYXRpb25zLiBUaGV5IGFyZSB0d28gZXhhbXBsZXMgb2Ygc2VxdWVuY2UgZGF0YSB0eXBlcyAoc2VlIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgbGlzdCwgdHVwbGUsIHJhbmdlKS4gU2luY2UgUHl0aG9uIGlzIGFuIGV2b2x2aW5nIGxhbmd1YWdlLCBvdGhlciBzZXF1ZW5jZSBkYXRhIHR5cGVzIG1heSBiZSBhZGRlZC4gVGhlcmUgaXMgYWxzbyBhbm90aGVyIHN0YW5kYXJkIHNlcXVlbmNlIGRhdGEgdHlwZTogdGhlIHR1cGxlLlxcblxcbkEgdHVwbGUgY29uc2lzdHMgb2YgYSBudW1iZXIgb2YgdmFsdWVzIHNlcGFyYXRlZCBieSBjb21tYXMsIGZvciBpbnN0YW5jZTpcXG4uLi5cXG4+Pj4gIyBUdXBsZXMgYXJlIGltbXV0YWJsZTpcXG4uLi4gdFswXSA9IDg4ODg4XFxuVHJhY2ViYWNrIChtb3N0IHJlY2VudCBjYWxsIGxhc3QpOlxcbiAgRmlsZSBcXFwiPHN0ZGluPlxcXCIsIGxpbmUgMSwgaW4gPG1vZHVsZT5cXG5UeXBlRXJyb3I6ICd0dXBsZScgb2JqZWN0IGRvZXMgbm90IHN1cHBvcnQgaXRlbSBhc3NpZ25tZW50XFxuPj4+ICMgYnV0IHRoZXkgY2FuIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzOlxcbi4uLiB2ID0gKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbj4+PiB2XFxuKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbi4uLlxcbkFzIHlvdSBzZWUsIG9uIG91dHB1dCB0dXBsZXMgYXJlIGFsd2F5cyBlbmNsb3NlZCBpbiBwYXJlbnRoZXNlcywgc28gdGhhdCBuZXN0ZWQgdHVwbGVzIGFyZSBpbnRlcnByZXRlZCBjb3JyZWN0bHk7IHRoZXkgbWF5IGJlIGlucHV0IHdpdGggb3Igd2l0aG91dCBzdXJyb3VuZGluZyBwYXJlbnRoZXNlcywgYWx0aG91Z2ggb2Z0ZW4gcGFyZW50aGVzZXMgYXJlIG5lY2Vzc2FyeSBhbnl3YXkgKGlmIHRoZSB0dXBsZSBpcyBwYXJ0IG9mIGEgbGFyZ2VyIGV4cHJlc3Npb24pLiBJdCBpcyBub3QgcG9zc2libGUgdG8gYXNzaWduIHRvIHRoZSBpbmRpdmlkdWFsIGl0ZW1zIG9mIGEgdHVwbGUsIGhvd2V2ZXIgaXQgaXMgcG9zc2libGUgdG8gY3JlYXRlIHR1cGxlcyB3aGljaCBjb250YWluIG11dGFibGUgb2JqZWN0cywgc3VjaCBhcyBsaXN0cy5cXG5cXG5UaG91Z2ggdHVwbGVzIG1heSBzZWVtIHNpbWlsYXIgdG8gbGlzdHMsIHRoZXkgYXJlIG9mdGVuIHVzZWQgaW4gZGlmZmVyZW50IHNpdHVhdGlvbnMgYW5kIGZvciBkaWZmZXJlbnQgcHVycG9zZXMuIFR1cGxlcyBhcmUgaW1tdXRhYmxlLCBhbmQgdXN1YWxseSBjb250YWluIGEgaGV0ZXJvZ2VuZW91cyBzZXF1ZW5jZSBvZiBlbGVtZW50cyB0aGF0IGFyZSBhY2Nlc3NlZCB2aWEgdW5wYWNraW5nIChzZWUgbGF0ZXIgaW4gdGhpcyBzZWN0aW9uKSBvciBpbmRleGluZyAob3IgZXZlbiBieSBhdHRyaWJ1dGUgaW4gdGhlIGNhc2Ugb2YgYG5hbWVkdHVwbGVzYCkuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG4jIyA1LjQuXFxuLi4uXFxuLiBTZXQgb2JqZWN0c1xcbi4uLlxcbiMjIDUuNS4gRGljdGlvbmFyaWVzXFx1MDBiNlxcblxcbkFub3RoZXIgdXNlZnVsIGRhdGEgdHlwZSBidWlsdCBpbnRvIFB5dGhvbiBpcyB0aGUgZGljdGlvbmFyeSAoc2VlIE1hcHBpbmcgVHlwZXMgXFx1MjAxNCBkaWN0KS4gRGljdGlvbmFyaWVzIGFyZSBzb21ldGltZXMgZm91bmQgaW4gb3RoZXIgbGFuZ3VhZ2VzIGFzIFxcdTIwMWNhc3NvY2lhdGl2ZSBtZW1vcmllc1xcdTIwMWQgb3IgXFx1MjAxY2Fzc29jaWF0aXZlIGFycmF5c1xcdTIwMWQuIFVubGlrZSBzZXF1ZW5jZXMsIHdoaWNoIGFyZSBpbmRleGVkIGJ5IGEgcmFuZ2Ugb2YgbnVtYmVycywgZGljdGlvbmFyaWVzIGFyZSBpbmRleGVkIGJ5IGtleXMsIHdoaWNoIGNhbiBiZSBhbnkgaW1tdXRhYmxlIHR5cGU7IHN0cmluZ3MgYW5kIG51bWJlcnMgY2FuIGFsd2F5cyBiZSBrZXlzLiBUdXBsZXMgY2FuIGJlIHVzZWQgYXMga2V5cyBpZiB0aGV5IGNvbnRhaW4gb25seSBzdHJpbmdzLCBudW1iZXJzLCBvciB0dXBsZXM7IGlmIGEgdHVwbGUgY29udGFpbnMgYW55IG11dGFibGUgb2JqZWN0IGVpdGhlciBkaXJlY3RseSBvciBpbmRpcmVjdGx5LCBpdCBjYW5ub3QgYmUgdXNlZCBhcyBhIGtleS4gWW91IGNhblxcdTIwMTl0IHVzZSBsaXN0cyBhcyBrZXlzLCBzaW5jZSBsaXN0cyBjYW4gYmUgbW9kaWZpZWQgaW4gcGxhY2UgdXNpbmcgaW5kZXggYXNzaWdubWVudHMsIHNsaWNlIGFzc2lnbm1lbnRzLCBvciBtZXRob2RzIGxpa2UgYGFwcGVuZCgpYCBhbmQgYGV4dGVuZCgpYC5cIl19XSIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ3ZWJfc2VhcmNoIiwiaWQiOiI2ZTUxMjVhYy0zMTY0LTRhNzYtOWM1Mi1hYzM0NjBjZTM2ZTQiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJzdGF0dXMiOiJzdWNjZXNzIn0seyJjb250ZW50IjpbeyJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3NrZXB0aWMiLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QTZGNGNDTjV5aEQ0Q3JzOU0iLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjB9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InJlc2VhcmNoZXIiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtM2NkOC03OWYxLTg4YTEtY2U2Y2VlMjk0ZTc5LTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoidHJhbnNmZXJfdG9fc2tlcHRpYyIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsInRvdGFsX3Rva2VucyI6NzQxMSwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fSx7ImNvbnRlbnQiOiJTdWNjZXNzZnVsbHkgdHJhbnNmZXJyZWQgdG8gc2tlcHRpYyIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ0cmFuc2Zlcl90b19za2VwdGljIiwiaWQiOiIzODkwNThkMS0wODNmLTQ2MzItYmJjNS0wOWJmZDNhMGZmZDIiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMVhqajNhUmRXeWhCSmZQbVRKQnhRYTkiLCJzdGF0dXMiOiJzdWNjZXNzIn0seyJjb250ZW50IjpbeyJzaWduYXR1cmUiOiJDQVFTMVFnS0VBZ1NHQUk0QVVJSWRHaHBibXRwYm1jU0RONlRmVTZORE05NzFsRWJ6eG9NdlJVQTFKOW94M2ZKUVlUOUlqQXJDUVFHSEJQNHBSSFBhdlh6UmxiTWJhWUhDNTdZaitHYnhncjBoZWx2ZDJPSUwvbzZSYnQ0UEZBM0ZhSUZVbGdxOGdjN0tRSTlGdEdudlNBa0xCUnpJc1VwLzJkNVJkMnpnRXNma0ZWZVhzbjhXWVlaa2luQVFWVnk2QTdCM0xhRUpXMHhiL3g4OERpNjdmZWR3eDBRK0ZsWnR3Y21UM0RjYWExMUp2ZXlIUnIzZG0rekdTWlA2Q1hlWE5KOG5yVFo3VzFZUlR5c0Q2dzdvN0xwOGgxUndxNGY0bzhqK1FVZlkwdmplK3pKWnN4SFBtUU50M3lrZUtoOTloZ242ZGJpejYvRUhxbWJIalNBdTFNNFNxRURmcUlLVGhIWWVuYW1BWm1jUTRYQU9vNm1kUTYrNzdWZ3BWMWxDMS9rNTJJaVVvSmprR2xRcm5MQ3Z1dGlJN0tOK3NnSzhjMisyS0NKclBSQ3ZWVitCa1N0dlA1aldXcXp5dHBLUStuSHpoTDQ0OE9ySUE5Z2RKQzM5QzhlRHJYZVJuSTd0NEZxbGtYR29NbFpBVjlVTWNyUUhSeE9NaTh0T3lUemk5b3lCRkRUU3NRK0xKcWVyMHR6MUZ0OWdlazZ2emc1MFNwVkFGUWRMRVJ2OFIxWXFNYWVJK3hzdlRBRVc5TVJndlJNa3pMdXpSVDJndXczakQrRmppcDM3eGdLNVFJMFIrQ3NPQW1RUG9UWml6emNJeUFVQXVDSkZDZ3EvZ0QvQ2liTHhveW1pVlVJMWFuOVNudDNLcE5Sb1dkdldJbmUzM1YwcHY4UEhnOXlXMkp1TE9xUUdpbExDc0svUTNvUXIzdXBFTVh4WEdqQ0w3V1VpenZSWkdGRUVWK1ZwbFl0N3Y0dEtaTWo1cGszdmRlTTBNSjVYWmc3UHZVc1J6eWN4Y25yVDNLbm9jM3FNK3R0cVpmVHNiOGQ2VS9wUWlmcTdHdWFoRk9IcnZyb3FnSkxqKy84MVl5MXBaRmtqZkpnWTBYa2x6QW5BNkNGcS9ZVDBneGJWK1RINUlTK1dhWU9VekszNlM4czVxcU13aG5ndjRJekUzY3RYWHFuaElaWE56bnlWQnFiWWEvR3gyYkNqY0VqMm41UnlHWVJTLytNY3VsZ0xCem5ROU1rRzJEN1FQM01xL253SFhLTUlhTFM4Vks4aEczZDByQWpsdm5hdVhTaE5ySnh0a2o0cWt3bnhENExyRWprMXVlK1BMY2JMcVlXd0FyOG4wcys0MWpyc0lQVnl5Szl2RmFFWnJBOUthQnhrSnlpcXg1MEdaTm43Y2t4K21rSTE4Z0g1Mml5VlB0VThGS01YanVnQ2w2bVBMaUhZaURRZCtHNjNMdWRtT0t5czlpdFZuQkRCTk1QMmIvSTcrYklWUmxoZUdhWldyNk1BVEtsODZCM3krMGt1T3JMUk15V2NkaWJjMmlRa1ptKzhmN2RUVXdiS2JCSnNLT0ZZNzMvOG5sTC9KaVlwano2blVJaytJcVNqYUZ5SzZYRmxDY3VrV3BXSHBESm5TdmJFemV5N3FIeUNxSitJK29uOGlQOEdPdGVtcFFhUzAvNGtaR1N3bHJFNnhPajZFd0dwZmpiRFpLSGtQLzJSQWROWVBRcTN6N2JkVkpNSk5uRk16NmUzN216TXBDQmlod1BkU0ZUUkUzU0NZNjZ3dlRnSnpHQ1V5aEhiU2k1aTFhenpPWGQ4dUhDU3BULzZMMW82VlUwcGpvUjhLYWE0d0NXSEdmN1AvV0VkK2F2eXAwMkpudTFjbXBXMCtHcSt2SzloMHRkYjJJQ2xNL2VOZmRvdVFQK2VwOE1KVWpyYURSSUNtQUpRWTVvYlM3MXZuaU9vK3JlYUZ5MHpSZ0IiLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsic2lnbmF0dXJlIjoiQ0FRUy9SRUtFUWdTR0FJNEFVSUpibUZ5Y21GMGFXOXVFZ3dGaDNBVkpaUDNzOHkxSHEwYURHNzZ1aEhPTmFTY1hBR1NoaUl3R2dqTFhIVXgxeVpya0xQbGpqelRPUXljcExKaUtWd1hjeG0vRXpLSm0xczhNNVd4dVJFV0VsZFhjb2FOMC9tc0twa1JNVCtlTGI1aWg5ckFIVUxUb21VTnNVd0pldTBqckplNEdMYXp5N3NQNUt0akVPRUJLUXNYVWZvUTdmRGtaT1FVSFppOXp6UU40aEZObVlKVWYzT1ZxZjlUUU5kQzErbS8vSEFaUGZXbUF5Nkh0RHZtN2tnQkprQ0VvaUZubTd0bi9QWDFoWEdQUmdXYzZCVFhVTXIyV0E4YUt5cWdnUlViTUgyK0pZdjVpV1pkZENjMXVwMU5icjNzRjdXUGNvdDlaQmh5SFFYcjY5MnplSms2U0pneWdJVnJBU2JNbHkyYkJ4VFdKQjYyM2dWZ3pwQ21WU0xaRnBrS1hnUGNwOGhHeURLT3ZGNlNpUVNIcUdSeFFvZnI3VnMxYVBoZlppeFJPSkdEK01scmtnZ1ZBbkdNeVlJRFVacGF2dENlaVUxSm1CU3owMDg0YlhQdFQvdG5wc0tPblNQWVpRbzNCMEJYS2JxQS9Xc0tuZzgvVjNaOFV1YWpvNklWWjJoZ0ExV0RkdXFRT1FrRXhHVTZwRnVMaGViSWp1V3hTTkkvRGZmdlIvL0xOcUNmdmRuZjBodm9QdER4ZngvU2F3SXg0L3NwcTl4YndGekh0TGdvSGcxRjJ3Rnc3Y2xQRGxnMVB5RC92MnlKRzNWOVQ4R0w2akdQRTZyaGZpbC91NEhOdmY3QlcwZFF6Q1RUWGNxWnk0bkJ5eTArcCtTN1p4Zk1sNEtweGEyZ09oaFNOamZwZnVYMFFwTDRrTUU4SjFubGlGTjFkOEtxRVB1REkxOEJRSFVoRC9VZlkyOW9BMHg1MTBGak0vQnNMWjRuNXNwcGtvSWk3Q0ZvUXEyYUFLVk5mQzFLaWxoSXBSRCsxZHlEdFpRa2xOOGdacE9FU0h3Nk1MMVR0VTB5ZkFjYm1xb3hKNmhoanp3SDZDTXNUUUVFeThNYlJCTStRS0trSVJkUkNQSEdGVzJMa3VSNTArSjBwRi90cjZwcVZ2SmVvemZpTW5yRWlsQjI3VFpQNG5LVEpHOHJkLy92Q1dlbjB5N0NBLzhkQko5Z2RwNXR4N05WQTQ1azFnZGN4RVIxZ1lvdU5YM24xdkczL0IxRjQrOTN3WFc5TXFvNlVzNTlUUW1yYjlOcWE3cFNRTlNySDA2dzFHeHlCQWxrRmZxQWxhZmsxNmFNcHE0MTZUSllwR3k0M1dDVWlDY0xXbGhpV2d6MnNnT0xBWFA5YTV3ZVdBS1JDOEo2amtjVWtEZVhuVytIRlNpUE02KzlCbzZrVmhMVWhONXJjNG1IWk9acEY3cHRQcHNRTjRYQ3FMUHNxcFJILzhvNVhweUM2Wm5lVGVGMkV4dGpod1pWalhEd01iSXpWWUlidzF5MlBQR3c4ZEM3cGM0c0lWU25JNG9RQUgwdjY1MGdoZmhtTWV2V1c0aDRQeWxidy9ERzRmZGVUMys4ejBmOE1PU1R0ZmlweDA1L1F0dEhsQXZPRFJ4YTV5REN2T3o2bjNmMUJPME4vNE1iR3lPTTI3cVc0eDhtQlZvcnlVWldmc21oZmkwL0dmTnZCOCt5bFRHYllOWDI3T2NFRjVXWnROcWJXRXk3VjVHYm1aVVN4c3BjZzdGQlozajMvM0w3bXV2WlhvWmkxZjVlcnE0eTNsU0grb0Y3cVBGbnZpZXlMT0N3dWJjUXkrb3lvd2o0Y0QwaWRSZWNucCtzKzUyRDcrbXpGMWdwVytmK2NKYXNveGlnYnhrT2xvUzR5ZWlLdmdNNG5XU3BHRWoxMlJQeDVvODBFbXppWE45dDR6SnpXMWh0QTVoWEpKZUZ5bXBNWXR5Q1VDZXZSV1ZtclJZa3RRN1VuQ2E2dEwvQVZQaTFuaTVYQ1hIZkxPdXFmSzVLTFl4andWdGRoaGZtQ0x2cGFLVk5DRU9DZjRuTkdIcUQ5OHdVYjBlaldOd0RZQm5KR2tCUnc5UlZzcGZ3TmRvV29WY24ycXY4eXdHVXA3eWMvc2syTHBJNlZ6aVJ5dldxYkdORzFaSnFWVnV0UmM0NitzT0U0VlNlUzIvQ1ROS05hNk5NQ2YvaHh4YVVWc1lmS2RQK1MxTGM0aWFuS3Awc1V1Z1BwbXRveWRDMENQMlJ3am5ITGNuenhDbnZxeGxMUVdYTU5QVHN1MnlCY0hsV0pMdGNtZGcvTWNMYWd5N2w1a0c2c043WnhKTEQva0F4OEU2dXFvaFArSy9qTVNyWXVRSm5ocjR0ZjRiaVdGWTNKemYvS3BBWXlWU3ErcGJmRktYTWJMOW5IRW9aUVQxNVd5YWJkNkRNQXEzUVpPdGhNNXBVemROcVVYa2NySTFQd2pBU0FnNGRWVS8ybys5OG1UWG55aFA3VmxFa1RuZGRwd3lKRitZQ0NjMDFwcWE2Wlg5OXBuUitZT3BJcmI1RHRQY0txMHQ1Y0ZDRHlReE4xbFV1cVpjWXVqdlgvR0M3KzZJcFJUei9Oa0VZLzVjbnRPZlpKeWQ2U2tVeC9KUjVBSklSUENOZWliMHJjV25uRm4wWEs4czIyeDQwd0xqRXR1ZlhETzkreXJhZjRrSHhsQkkzU0IwbWVNamVwWDUwQ0poQXg3TmVra1JGbndvZWNMZnFJeXJqWHJnQVpzVE1QaVg5NHNuamRyemlzQVh0QkthbnVsaWxTeE9mVzJIU21mbmswTDJHWEhKSjNTK3ZDYmdSRW1sOWRla2ZtSGhtd0VZdHYza2ZodGg3RVJJd1ZUOGo2OU90SUJDbWVUSUp1QTdvNzNQWGMxTEoyd2h1Wks3RlplNk5UT3M2MU1QN0pWbEljNnRiaHRPQmszTlhVOHBCUE9DWTZRNTVkNjNGdHBVVS9LQ2JRMEsyV1E5bzVqTkd3Tk5KNkN3NUZEQ3QreDVZSmVQMWQyUXpEU2lxK2NTWnF3cUNaVmNOb0FmdEdHemdyUlo0dmtwYlAweFFaSEN0V3NVd0hWVGpyWTROZlJEakhkVlBSbjhKOVh0b29jL2hJV2txekZhY3pZVGxhcHUyUkNKcWU4YUVvM2p6V1pDRUo3QWlCekJZcmFZRGdPZmVpUXllSlVzYmJYSGRHS2lpdzIyQmRwNE0rWXo2NzFNWjBpZTlFV1JrczBRMkRIR1VUbmU5UzR0QUFKUEdiVWY0eHRDTW9KNDBRNFFXMDVRTFNaeTN3TkZBVUZZbVhFNEVDREZCOCtJRWVORkZGL3RnQW5maDhHdkZoLzF4WStMRStvek1taW5qU1J3aHEwOXpRQUVrTkZ5dTBJS2d2RGt5T0kraWNjRjdkb1dMVFVhc1B4TEgxeEdQQlNLa2VXQ25Tb1gvNEMyYzlkdTY2QmJSWGFtSHFhZStWQ2lTNkJOOTUzaVlLT3U4ck1rbmVXK1RGalYwQ3pBZjhQTkc5TEJheVVnWTVVdzl4MFNhcDZ6STRQcGFaS0g0ZDlrdGtGVSs5MXlCYk41U1J0ZGQ4L0VRYXRKWG1CTmZ3N2tScmxsK0xqd1p3ZVdMRWIwalQxUnBHdURxSExkTFRNTWdvd1NjenJwMnpDc0pkUUgvc2VhWXJwd2tDRlI4ZFJqTTdKUlVrVmExQTBTdmRaQVg3NXZ1QnZ2TEJ6TjJjK1JidDVvcXk5M2xxSUJJaktiN3cyVWtSVGI0cEJXY05LTlBVZ3QzYXovUHhReEVhc2FsdGRtdXl2c1hZYnVyM1ZQTm44SnpOLzZKNU4rUUNuejU3MVRjY2FwQ21rUHZGRStnYXlYVStVRjFWS1l5RmNVdmxldjdsc1FsbWpxeksyUUE4WDdRM1R6RmFwcWh2SktsdDdkekJIcmFxWkJnUGdiOU40b0tOYXlpOEZaVDcxK2thWWhJaEZyRVNLWVJIZkhrajVuM2tmaGdMeSt4K2pMRGJLeE1nZ2tnSG5KejlSdUVIWlhCV3RrWUFRPT0iLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsiaWQiOiJ0b29sdV8wMUg2ZlY4SnZSSk52WHk0N2VSbmQzakgiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6e30sIm5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHdBTVJkUzIxRmVKZnpRaE0zcyIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo3Mzk2LCJvdXRwdXRfdG9rZW5zIjo3NzAsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjczOX0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoic2tlcHRpYyIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy00YWU5LTc2YzAtODlhNi00ZjBiMTE0ZTMwMzAtMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzM5Niwib3V0cHV0X3Rva2VucyI6NzcwLCJ0b3RhbF90b2tlbnMiOjgxNjYsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjczOX19fSx7ImNvbnRlbnQiOiJTdWNjZXNzZnVsbHkgdHJhbnNmZXJyZWQgdG8gdmVyaWZpZXIiLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7fSwidHlwZSI6InRvb2wiLCJuYW1lIjoidHJhbnNmZXJfdG9fdmVyaWZpZXIiLCJpZCI6ImMwNjQ1NjNhLTk3NGYtNGYwZi1hYjRhLTA4MGU2M2MwMWNmNSIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsInN0YXR1cyI6InN1Y2Nlc3MifSx7ImNvbnRlbnQiOlt7InNpZ25hdHVyZSI6IkNBUVM5Z1VLRUFnU0dBSTRBVUlJZEdocGJtdHBibWNTREdGeUoxTVhvWStBaTZOa2J4b01SZDJUclVmVVU3V3JQcEEySWpEZ3k4ZFVwbGNaS2M3RG5Kd05MVEJmSDVVTlIrbkduYWtTUFFpYThVVkl6cmprT25KMmZMcldVc0Eyb0VPOHlSc3Frd1hra0RDc3loQjk3ZHdqNTdSTXQ5MVh6Z1doTDlsdU5FR085MnlXMHJORkdvTVNVVk94UElyRmM1Ump5cmhLRWMzKzFSbnB4Q2t5bHErTE5sUTVORnhMbjZ2MmNGNUR6cXB2QkhOUjdYQWlnK3VySklVM2dYeWRDa2FHWlpEZXRKdFFPVU9YZ1ppOXo5MTAwTnBkQUVrWERvMklDSENHVEdwVmMwSi9wYlh4V1JCeGI2QjEvR2wwWWpuNjhWNkhsZXNIN0lvQzdHUi8zSHdSdTlOU0RCRzR0dGZRakVXY2crUGVBZWE0Um43anp4K1RtOUg3TDFqb1RLVmdEYlFnNWlLTUxmanZQcWxua3NDTHRYT3Q4alNJUzVPWUxON2EvN1RYNi93NWQzNHZudzh2UERtVTFVWWkwUTcycVVXaFVhMnZ3OE9YTnBjS0w3QnAwbE5mQlM4cnZIWSt2Qlk4L2hhQW9UTDN3dkxGVTFmVCs0ei92QTNCY3NpZ1pEWWxmSmVEWWN4WG1SdXF4enhFR3BYQ1JlOXMxSFd0SUQrNDFwSHU2bHBaSGtYYWdocGM2VFpITHhOd2NWaFRiNmZNUTdRbysyTDZQdmtsaDFtMzJwZW9keGVnMllpUGhkcGwzdy94a09FdTUvSkVGN1pCeHBkVUwrdTN3a081cFVsc3FmZ1g3dWh6djdIQzJXcFNXVmltcndpUXdRT1V4MCtKUHJMdHcrbFlmcTdJM2pydW8wR1cwZ3VXdm51ZjdKTTNZTGo4NHErSmdIb21uU1Nabm5abXJMMFVhUnZyVnlMbzg3N0YzOWNjTFM3WG9abm51aThKNFA1OFpUbWlMa0VKZUdjeDFJdWFnMmUzMzMzOU83WWV3LzU5dExiUXRJR2g5VldNd0hCbkZ4MzlTOC80TEVGcks1SGxkUFFoT3Jsb3lUdGtSczY2aVFSS3R4TUtTMFZJNlFUS1pEQWJzN2E3SDF5QnYrbUppOVhnNHVLbXlVNnFubjd6RUtqalkyYklsQ3dleng0TExHUDVIRDNacjZmWnNtZEcxVUh6RHNCdllHMm9DSTBvcUVLTSt2YWxzVDZoVThNUmpiUUJEL0pKOEpKTi9pQkdsaEJBVUhvMFVtaGh0UjJldVRWcFpXbE1uaG1wWEFkS0RYU2ZQZGFQb2hnQiIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJpZCI6InRvb2x1XzAxWEVBdWhRSENCZHlDd3R5bVp3RmZEaSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3JlZF90ZWFtIiwidHlwZSI6InRvb2xfdXNlIiwidG9vbHNldF9uYW1lIjpudWxsfV0sImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2Zkd0FzYWJEcXVlZ0hpNllVcENuIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjc1MDksIm91dHB1dF90b2tlbnMiOjg4LCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjo1NX0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoidmVyaWZpZXIiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtNjY4Mi03ZDIxLThjMTUtZmZiY2U5NDIyYzlmLTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoidHJhbnNmZXJfdG9fcmVkX3RlYW0iLCJhcmdzIjp7fSwiaWQiOiJ0b29sdV8wMVhFQXVoUUhDQmR5Q3d0eW1ad0ZmRGkiLCJ0eXBlIjoidG9vbF9jYWxsIn1dLCJpbnZhbGlkX3Rvb2xfY2FsbHMiOltdLCJ1c2FnZV9tZXRhZGF0YSI6eyJpbnB1dF90b2tlbnMiOjc1MDksIm91dHB1dF90b2tlbnMiOjg4LCJ0b3RhbF90b2tlbnMiOjc1OTcsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjU1fX19LHsiY29udGVudCI6IlN1Y2Nlc3NmdWxseSB0cmFuc2ZlcnJlZCB0byByZWRfdGVhbSIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ0cmFuc2Zlcl90b19yZWRfdGVhbSIsImlkIjoiOTgyYzAxMjYtZTZhYi00NThhLTkyMmUtN2IyNjZlODNjMmIyIiwidG9vbF9jYWxsX2lkIjoidG9vbHVfMDFYRUF1aFFIQ0JkeUN3dHltWndGZkRpIiwic3RhdHVzIjoic3VjY2VzcyJ9XSwiYWN0aXZlX2FnZW50IjoicmVkX3RlYW0ifQ==" + } + } + ], + "events": [ + { + "timeUnixNano": "1790968035109079000", + "name": "exception", + "attributes": [ + { + "key": "exception.type", + "value": { + "stringValue": "Exception" + } + }, + { + "key": "exception.message", + "value": { + "stringValue": "ParentCommand(Command(graph='red_team:15ccef64-4162-9e02-e87c-4cb0eb6ea16b', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', id='389058d1-083f-4632-bbc5-09bfd3a0ffd2', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9'), AIMessage(content=[{'signature': 'CAQS1QgKEAgSGAI4AUIIdGhpbmtpbmcSDN6TfU6NDM971lEbzxoMvRUA1J9ox3fJQYT9IjArCQQGHBP4pRHPavXzRlbMbaYHC57Yj+Gbxgr0helvd2OIL/o6Rbt4PFA3FaIFUlgq8gc7KQI9FtGnvSAkLBRzIsUp/2d5Rd2zgEsfkFVeXsn8WYYZkinAQVVy6A7B3LaEJW0xb/x88Di67fedwx0Q+FlZtwcmT3Dcaa11JveyHRr3dm+zGSZP6CXeXNJ8nrTZ7W1YRTysD6w7o7Lp8h1Rwq4f4o8j+QUfY0vje+zJZsxHPmQNt3ykeKh99hgn6dbiz6/EHqmbHjSAu1M4SqEDfqIKThHYenamAZmcQ4XAOo6mdQ6+77VgpV1lC1/k52IiUoJjkGlQrnLCvutiI7KN+sgK8c2+2KCJrPRCvVV+BkStvP5jWWqzytpKQ+nHzhL448OrIA9gdJC39C8eDrXeRnI7t4FqlkXGoMlZAV9UMcrQHRxOMi8tOyTzi9oyBFDTSsQ+LJqer0tz1Ft9gek6vzg50SpVAFQdLERv8R1YqMaeI+xsvTAEW9MRgvRMkzLuzRT2guw3jD+Fjip37xgK5QI0R+CsOAmQPoTZizzcIyAUAuCJFCgq/gD/CibLxoymiVUI1an9Snt3KpNRoWdvWIne33V0pv8PHg9yW2JuLOqQGilLCsK/Q3oQr3upEMXxXGjCL7WUizvRZGFEEV+VplYt7v4tKZMj5pk3vdeM0MJ5XZg7PvUsRzycxcnrT3Knoc3qM+ttqZfTsb8d6U/pQifq7GuahFOHrvroqgJLj+/81Yy1pZFkjfJgY0XklzAnA6CFq/YT0gxbV+TH5IS+WaYOUzK36S8s5qqMwhngv4IzE3ctXXqnhIZXNznyVBqbYa/Gx2bCjcEj2n5RyGYRS/+MculgLBznQ9MkG2D7QP3Mq/nwHXKMIaLS8VK8hG3d0rAjlvnauXShNrJxtkj4qkwnxD4LrEjk1ue+PLcbLqYWwAr8n0s+41jrsIPVyyK9vFaEZrA9KaBxkJyiqx50GZNn7ckx+mkI18gH52iyVPtU8FKMXjugCl6mPLiHYiDQd+G63LudmOKys9itVnBDBNMP2b/I7+bIVRlheGaZWr6MATKl86B3y+0kuOrLRMyWcdibc2iQkZm+8f7dTUwbKbBJsKOFY73/8nlL/JiYpjz6nUIk+IqSjaFyK6XFlCcukWpWHpDJnSvbEzey7qHyCqJ+I+on8iP8GOtempQaS0/4kZGSwlrE6xOj6EwGpfjbDZKHkP/2RAdNYPQq3z7bdVJMJNnFMz6e37mzMpCBihwPdSFTRE3SCY66wvTgJzGCUyhHbSi5i1azzOXd8uHCSpT/6L1o6VU0pjoR8Kaa4wCWHGf7P/WEd+avyp02Jnu1cmpW0+Gq+vK9h0tdb2IClM/eNfdouQP+ep8MJUjraDRICmAJQY5obS71vniOo+reaFy0zRgB', 'thinking': '', 'type': 'thinking'}, {'signature': 'CAQS/REKEQgSGAI4AUIJbmFycmF0aW9uEgwFh3AVJZP3s8y1Hq0aDG76uhHONaScXAGShiIwGgjLXHUx1yZrkLPljjzTOQycpLJiKVwXcxm/EzKJm1s8M5WxuREWEldXcoaN0/msKpkRMT+eLb5ih9rAHULTomUNsUwJeu0jrJe4GLazy7sP5KtjEOEBKQsXUfoQ7fDkZOQUHZi9zzQN4hFNmYJUf3OVqf9TQNdC1+m//HAZPfWmAy6HtDvm7kgBJkCEoiFnm7tn/PX1hXGPRgWc6BTXUMr2WA8aKyqggRUbMH2+JYv5iWZddCc1up1Nbr3sF7WPcot9ZBhyHQXr692zeJk6SJgygIVrASbMly2bBxTWJB623gVgzpCmVSLZFpkKXgPcp8hGyDKOvF6SiQSHqGRxQofr7Vs1aPhfZixROJGD+MlrkggVAnGMyYIDUZpavtCeiU1JmBSz0084bXPtT/tnpsKOnSPYZQo3B0BXKbqA/WsKng8/V3Z8Uuajo6IVZ2hgA1WDduqQOQkExGU6pFuLhebIjuWxSNI/DffvR//LNqCfvdnf0hvoPtDxfx/SawIx4/spq9xbwFzHtLgoHg1F2wFw7clPDlg1PyD/v2yJG3V9T8GL6jGPE6rhfil/u4HNvf7BW0dQzCTTXcqZy4nByy0+p+S7ZxfMl4Kpxa2gOhhSNjfpfuX0QpL4kME8J1nliFN1d8KqEPuDI18BQHUhD/UfY29oA0x510FjM/BsLZ4n5sppkoIi7CFoQq2aAKVNfC1KilhIpRD+1dyDtZQklN8gZpOESHw6ML1TtU0yfAcbmqoxJ6hhjzwH6CMsTQEEy8MbRBM+QKKkIRdRCPHGFW2LkuR50+J0pF/tr6pqVvJeozfiMnrEilB27TZP4nKTJG8rd//vCWen0y7CA/8dBJ9gdp5tx7NVA45k1gdcxER1gYouNX3n1vG3/B1F4+93wXW9Mqo6Us59TQmrb9Nqa7pSQNSrH06w1GxyBAlkFfqAlafk16aMpq416TJYpGy43WCUiCcLWlhiWgz2sgOLAXP9a5weWAKRC8J6jkcUkDeXnW+HFSiPM6+9Bo6kVhLUhN5rc4mHZOZpF7ptPpsQN4XCqLPsqpRH/8o5XpyC6ZneTeF2ExtjhwZVjXDwMbIzVYIbw1y2PPGw8dC7pc4sIVSnI4oQAH0v650ghfhmMevWW4h4Pylbw/DG4fdeT3+8z0f8MOSTtfipx05/QttHlAvODRxa5yDCvOz6n3f1BO0N/4MbGyOM27qW4x8mBVoryUZWfsmhfi0/GfNvB8+ylTGbYNX27OcEF5WZtNqbWEy7V5GbmZUSxspcg7FBZ3j3/3L7muvZXoZi1f5erq4y3lSH+oF7qPFnvieyLOCwubcQy+oyowj4cD0idRecnp+s+52D7+mzF1gpW+f+cJasoxigbxkOloS4yeiKvgM4nWSpGEj12RPx5o80EmziXN9t4zJzW1htA5hXJJeFympMYtyCUCevRWVmrRYktQ7UnCa6tL/AVPi1ni5XCXHfLOuqfK5KLYxjwVtdhhfmCLvpaKVNCEOCf4nNGHqD98wUb0ejWNwDYBnJGkBRw9RVspfwNdoWoVcn2qv8ywGUp7yc/sk2LpI6VziRyvWqbGNG1ZJqVVutRc46+sOE4VSeS2/CTNKNa6NMCf/hxxaUVsYfKdP+S1Lc4ianKp0sUugPpmtoydC0CP2RwjnHLcnzxCnvqxlLQWXMNPTsu2yBcHlWJLtcmdg/McLagy7l5kG6sN7ZxJLD/kAx8E6uqohP+K/jMSrYuQJnhr4tf4biWFY3Jzf/KpAYyVSq+pbfFKXMbL9nHEoZQT15Wyabd6DMAq3QZOthM5pUzdNqUXkcrI1PwjASAg4dVU/2o+98mTXnyhP7VlEkTnddpwyJF+YCCc01pqa6ZX99pnR+YOpIrb5DtPcKq0t5cFCDyQxN1lUuqZcYujvX/GC7+6IpRTz/NkEY/5cntOfZJyd6SkUx/JR5AJIRPCNeib0rcWnnFn0XK8s22x40wLjEtufXDO9+yraf4kHxlBI3SB0meMjepX50CJhAx7NekkRFnwoecLfqIyrjXrgAZsTMPiX94snjdrzisAXtBKanulilSxOfW2HSmfnk0L2GXHJJ3S+vCbgREml9dekfmHhmwEYtv3kfhth7ERIwVT8j69OtIBCmeTIJuA7o73PXc1LJ2whuZK7FZe6NTOs61MP7JVlIc6tbhtOBk3NXU8pBPOCY6Q55d63FtpUU/KCbQ0K2WQ9o5jNGwNNJ6Cw5FDCt+x5YJeP1d2QzDSiq+cSZqwqCZVcNoAftGGzgrRZ4vkpbP0xQZHCtWsUwHVTjrY4NfRDjHdVPRn8J9Xtooc/hIWkqzFaczYTlapu2RCJqe8aEo3jzWZCEJ7AiBzBYraYDgOfeiQyeJUsbbXHdGKiiw22Bdp4M+Yz671MZ0ie9EWRks0Q2DHGUTne9S4tAAJPGbUf4xtCMoJ40Q4QW05QLSZy3wNFAUFYmXE4ECDFB8+IEeNFFF/tgAnfh8GvFh/1xY+LE+ozMminjSRwhq09zQAEkNFyu0IKgvDkyOI+iccF7doWLTUasPxLH1xGPBSKkeWCnSoX/4C2c9du66BbRXamHqae+VCiS6BN953iYKOu8rMkneW+TFjV0CzAf8PNG9LBayUgY5Uw9x0Sap6zI4PpaZKH4d9ktkFU+91yBbN5SRtdd8/EQatJXmBNfw7kRrll+LjwZweWLEb0jT1RpGuDqHLdLTMMgowSczrp2zCsJdQH/seaYrpwkCFR8dRjM7JRUkVa1A0SvdZAX75vuBvvLBzN2c+Rbt5oqy93lqIBIjKb7w2UkRTb4pBWcNKNPUgt3az/PxQxEasaltdmuyvsXYbur3VPNn8JzN/6J5N+QCnz571TccapCmkPvFE+gayXU+UF1VKYyFcUvlev7lsQlmjqzK2QA8X7Q3TzFapqhvJKlt7dzBHraqZBgPgb9N4oKNayi8FZT71+kaYhIhFrESKYRHfHkj5n3kfhgLy+x+jLDbKxMggkgHnJz9RuEHZXBWtkYAQ==', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_verifier', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAMRdS21FeJfzQhM3s', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7396, 'output_tokens': 770, 'output_tokens_details': {'thinking_tokens': 739}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='skeptic', id='lc_run--01a0fe03-4ae9-76c0-89a6-4f0b114e3030-0', tool_calls=[{'name': 'transfer_to_verifier', 'args': {}, 'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7396, 'output_tokens': 770, 'total_tokens': 8166, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 739}}), ToolMessage(content='Successfully transferred to verifier', name='transfer_to_verifier', id='c064563a-974f-4f0f-ab4a-080e63c01cf5', tool_call_id='toolu_01H6fV8JvRJNvXy47eRnd3jH'), AIMessage(content=[{'signature': 'CAQS9gUKEAgSGAI4AUIIdGhpbmtpbmcSDGFyJ1MXoY+Ai6NkbxoMRd2TrUfUU7WrPpA2IjDgy8dUplcZKc7DnJwNLTBfH5UNR+nGnakSPQia8UVIzrjkOnJ2fLrWUsA2oEO8yRsqkwXkkDCsyhB97dwj57RMt91XzgWhL9luNEGO92yW0rNFGoMSUVOxPIrFc5RjyrhKEc3+1RnpxCkylq+LNlQ5NFxLn6v2cF5DzqpvBHNR7XAig+urJIU3gXydCkaGZZDetJtQOUOXgZi9z9100NpdAEkXDo2ICHCGTGpVc0J/pbXxWRBxb6B1/Gl0Yjn68V6HlesH7IoC7GR/3HwRu9NSDBG4ttfQjEWcg+PeAea4Rn7jzx+Tm9H7L1joTKVgDbQg5iKMLfjvPqlnksCLtXOt8jSIS5OYLN7a/7TX6/w5d34vnw8vPDmU1UYi0Q72qUWhUa2vw8OXNpcKL7Bp0lNfBS8rvHY+vBY8/haAoTL3wvLFU1fT+4z/vA3BcsigZDYlfJeDYcxXmRuqxzxEGpXCRe9s1HWtID+41pHu6lpZHkXaghpc6TZHLxNwcVhTb6fMQ7Qo+2L6Pvklh1m32peodxeg2YiPhdpl3w/xkOEu5/JEF7ZBxpdUL+u3wkO5pUlsqfgX7uhzv7HC2WpSWVimrwiQwQOUx0+JPrLtw+lYfq7I3jruo0GW0guWvnuf7JM3YLj84q+JgHomnSSZnnZmrL0UaRvrVyLo877F39ccLS7XoZnnui8J4P58ZTmiLkEJeGcx1Iuag2e33339O7Yew/59tLbQtIGh9VWMwHBnFx39S8/4LEFrK5HldPQhOrloyTtkRs66iQRKtxMKS0VI6QTKZDAbs7a7H1yBv+mJi9Xg4uKmyU6qnn7zEKjjY2bIlCwezx4LLGP5HD3Zr6fZsmdG1UHzDsBvYG2oCI0oqEKM+valsT6hU8MRjbQBD/JJ8JJN/iBGlhBAUHo0UmhhtR2euTVpZWlMnhmpXAdKDXSfPdaPohgB', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_red_team', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAsabDquegHi6YUpCn', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7509, 'output_tokens': 88, 'output_tokens_details': {'thinking_tokens': 55}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='verifier', id='lc_run--01a0fe03-6682-7d21-8c15-ffbce9422c9f-0', tool_calls=[{'name': 'transfer_to_red_team', 'args': {}, 'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7509, 'output_tokens': 88, 'total_tokens': 7597, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 55}}), ToolMessage(content='Successfully transferred to red_team', name='transfer_to_red_team', id='982c0126-e6ab-458a-922e-7b266e83c2b2', tool_call_id='toolu_01XEAuhQHCBdyCwtymZwFfDi'), AIMessage(content=[{'signature': 'CAQSwgcKEAgSGAI4AUIIdGhpbmtpbmcSDORXge9uL1832RMQwBoMjQwAwAHb9iP1ojPGIjDDScal486beCJbtG8/D2Y9g9S5xIPfjoHg/dxwX5HypSQEBbjFiTSbPECXSFzV6YIq3wbLcmyPbvusGnDexFgx+3sIXzvqyFV/69xhihMnSpfTCHy0+0zSu1WpYU/WUpIfoaSXTcTa1qkM8x8Ek/y/1jkKMZUfDFa93uhUJxOC4+QgPWBl/+Xy2t82L2x/ebq7QLQTqtR0jXeaURQm3Co6JvOgWmDvMpEdTeTWE3fDh1tzzVKsflum4d89b6CaKtn8nlQd7RhN99vNQMQHG5V4OuVNdrHPZZpFOuhdejsMFWXyB+55slGAd8wEXgFKbI/knOXVGzL/NDF8Gty1tNZlu2fdSCAsYWMsFQP1cbwLeQGszyXzJMVZJ2I9xBPkyz1HOXCPyh5F+j2gRTPikVB3ARC6tfB/lDSUeKBVZPQS8pY/B4eCs+IjjaBNWruLzfqCsc6xL0CuQPSYyQXaYFjYrSRNcC70WQBWT5VVhp0mA9yZDGYbRZt9I5OK9QkQmuJ5zDFaMEOXzSrsC4lteP6MdimiMijvsH3iq+c6WUYolRj3neI88EaHIzLAGRq64BNCLzdAYL2pEU2sb5uG78PKVoUUCbVJL5jpDOhLOTO1bQsTuoUpnRRrLQD/kIzV+ljpVBwwfJcyq75TJA/CtPr/xr2SZ9B1b2sbjG6jD0lhtoCddeDgcqgl/LSSkcuaYY8xPWCy+LtNhcqThfmfvnHLJIpL6gjbjW/OQpRfVUwfw1jOb9bWVwcgHhNYfKYGPtM8oth5xwLYRRVgzqMKOvs7falXRhVzZ2SP6Iw0BMmAAZ6HjfCl9L+tPqJNlnQQOHpoWjum4B+XsYldFEKGQWQqsd59iNPvao9p/sCOt7rGqCozcmgiEm4w66ez/QIDyb46n0+tcNJPUcoBKvQ/jDwE6VwmkQ/nECjFp3UdDT+kWYQSNd8j8VBZOpG8LlqUYrEZCv0q7Bs1vcNrdYiaFoS32osaynWk+Ht3OHuFWbEF8JlsFTQAO05VvYkecf0V/oTHsG5hyOOs7UEaDFq7uaJuzWVhh5pAojIaBcR8fVln2PLRTX7is2nsA+QrdidePcZtFtgxAiKYe1i2sSFi4vm8EOkCbQ63wdQeY4Urr5PuoMEwxUULjgKagiAmsJDq4RrVm7sO4+6YuA9j6l2uylkOEc67vf/5fkXuCsiApNL6keb8c+vL0n4FXGdzj+apSDfFGBgB', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01VBrWdWEWYgit4kNYehBMU5', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_editor', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAzfjqUopoEmZLaqm1', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7416, 'output_tokens': 151, 'output_tokens_details': {'thinking_tokens': 119}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='red_team', id='lc_run--01a0fe03-6ce6-7251-b796-4f64333f67c6-0', tool_calls=[{'name': 'transfer_to_editor', 'args': {}, 'id': 'toolu_01VBrWdWEWYgit4kNYehBMU5', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7416, 'output_tokens': 151, 'total_tokens': 7567, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 119}}), ToolMessage(content='Successfully transferred to editor', name='transfer_to_editor', tool_call_id='toolu_01VBrWdWEWYgit4kNYehBMU5')], 'active_agent': 'editor'}, goto='editor'))Traceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 2967, in stream\n for _ in runner.tick(\n ~~~~~~~~~~~^\n [t for t in loop.tasks.values() if not t.writes],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<2 lines>...\n schedule_task=loop.accept_push,\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ):\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_runner.py\", line 207, in tick\n run_with_retry(\n ~~~~~~~~~~~~~~^\n t,\n ^^\n ...<10 lines>...\n },\n ^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_retry.py\", line 617, in run_with_retry\n return task.proc.invoke(task.input, config)\n ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 709, in invoke\n input = step.invoke(input, config)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 447, in invoke\n ret = self.func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 84, in _write\n self.do_write(\n ~~~~~~~~~~~~~^\n config,\n ^^^^^^^\n writes,\n ^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 126, in do_write\n write(_assemble_writes(writes))\n ~~~~~~~~~~~~~~~~^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 181, in _assemble_writes\n if ww := w.mapper(w.value):\n ~~~~~~~~^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/graph/state.py\", line 1762, in _control_branch\n raise ParentCommand(command)\n\n\nlanggraph.errors.ParentCommand: Command(graph='red_team:15ccef64-4162-9e02-e87c-4cb0eb6ea16b', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', id='389058d1-083f-4632-bbc5-09bfd3a0ffd2', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9'), AIMessage(content=[{'signature': 'CAQS1QgKEAgSGAI4AUIIdGhpbmtpbmcSDN6TfU6NDM971lEbzxoMvRUA1J9ox3fJQYT9IjArCQQGHBP4pRHPavXzRlbMbaYHC57Yj+Gbxgr0helvd2OIL/o6Rbt4PFA3FaIFUlgq8gc7KQI9FtGnvSAkLBRzIsUp/2d5Rd2zgEsfkFVeXsn8WYYZkinAQVVy6A7B3LaEJW0xb/x88Di67fedwx0Q+FlZtwcmT3Dcaa11JveyHRr3dm+zGSZP6CXeXNJ8nrTZ7W1YRTysD6w7o7Lp8h1Rwq4f4o8j+QUfY0vje+zJZsxHPmQNt3ykeKh99hgn6dbiz6/EHqmbHjSAu1M4SqEDfqIKThHYenamAZmcQ4XAOo6mdQ6+77VgpV1lC1/k52IiUoJjkGlQrnLCvutiI7KN+sgK8c2+2KCJrPRCvVV+BkStvP5jWWqzytpKQ+nHzhL448OrIA9gdJC39C8eDrXeRnI7t4FqlkXGoMlZAV9UMcrQHRxOMi8tOyTzi9oyBFDTSsQ+LJqer0tz1Ft9gek6vzg50SpVAFQdLERv8R1YqMaeI+xsvTAEW9MRgvRMkzLuzRT2guw3jD+Fjip37xgK5QI0R+CsOAmQPoTZizzcIyAUAuCJFCgq/gD/CibLxoymiVUI1an9Snt3KpNRoWdvWIne33V0pv8PHg9yW2JuLOqQGilLCsK/Q3oQr3upEMXxXGjCL7WUizvRZGFEEV+VplYt7v4tKZMj5pk3vdeM0MJ5XZg7PvUsRzycxcnrT3Knoc3qM+ttqZfTsb8d6U/pQifq7GuahFOHrvroqgJLj+/81Yy1pZFkjfJgY0XklzAnA6CFq/YT0gxbV+TH5IS+WaYOUzK36S8s5qqMwhngv4IzE3ctXXqnhIZXNznyVBqbYa/Gx2bCjcEj2n5RyGYRS/+MculgLBznQ9MkG2D7QP3Mq/nwHXKMIaLS8VK8hG3d0rAjlvnauXShNrJxtkj4qkwnxD4LrEjk1ue+PLcbLqYWwAr8n0s+41jrsIPVyyK9vFaEZrA9KaBxkJyiqx50GZNn7ckx+mkI18gH52iyVPtU8FKMXjugCl6mPLiHYiDQd+G63LudmOKys9itVnBDBNMP2b/I7+bIVRlheGaZWr6MATKl86B3y+0kuOrLRMyWcdibc2iQkZm+8f7dTUwbKbBJsKOFY73/8nlL/JiYpjz6nUIk+IqSjaFyK6XFlCcukWpWHpDJnSvbEzey7qHyCqJ+I+on8iP8GOtempQaS0/4kZGSwlrE6xOj6EwGpfjbDZKHkP/2RAdNYPQq3z7bdVJMJNnFMz6e37mzMpCBihwPdSFTRE3SCY66wvTgJzGCUyhHbSi5i1azzOXd8uHCSpT/6L1o6VU0pjoR8Kaa4wCWHGf7P/WEd+avyp02Jnu1cmpW0+Gq+vK9h0tdb2IClM/eNfdouQP+ep8MJUjraDRICmAJQY5obS71vniOo+reaFy0zRgB', 'thinking': '', 'type': 'thinking'}, {'signature': 'CAQS/REKEQgSGAI4AUIJbmFycmF0aW9uEgwFh3AVJZP3s8y1Hq0aDG76uhHONaScXAGShiIwGgjLXHUx1yZrkLPljjzTOQycpLJiKVwXcxm/EzKJm1s8M5WxuREWEldXcoaN0/msKpkRMT+eLb5ih9rAHULTomUNsUwJeu0jrJe4GLazy7sP5KtjEOEBKQsXUfoQ7fDkZOQUHZi9zzQN4hFNmYJUf3OVqf9TQNdC1+m//HAZPfWmAy6HtDvm7kgBJkCEoiFnm7tn/PX1hXGPRgWc6BTXUMr2WA8aKyqggRUbMH2+JYv5iWZddCc1up1Nbr3sF7WPcot9ZBhyHQXr692zeJk6SJgygIVrASbMly2bBxTWJB623gVgzpCmVSLZFpkKXgPcp8hGyDKOvF6SiQSHqGRxQofr7Vs1aPhfZixROJGD+MlrkggVAnGMyYIDUZpavtCeiU1JmBSz0084bXPtT/tnpsKOnSPYZQo3B0BXKbqA/WsKng8/V3Z8Uuajo6IVZ2hgA1WDduqQOQkExGU6pFuLhebIjuWxSNI/DffvR//LNqCfvdnf0hvoPtDxfx/SawIx4/spq9xbwFzHtLgoHg1F2wFw7clPDlg1PyD/v2yJG3V9T8GL6jGPE6rhfil/u4HNvf7BW0dQzCTTXcqZy4nByy0+p+S7ZxfMl4Kpxa2gOhhSNjfpfuX0QpL4kME8J1nliFN1d8KqEPuDI18BQHUhD/UfY29oA0x510FjM/BsLZ4n5sppkoIi7CFoQq2aAKVNfC1KilhIpRD+1dyDtZQklN8gZpOESHw6ML1TtU0yfAcbmqoxJ6hhjzwH6CMsTQEEy8MbRBM+QKKkIRdRCPHGFW2LkuR50+J0pF/tr6pqVvJeozfiMnrEilB27TZP4nKTJG8rd//vCWen0y7CA/8dBJ9gdp5tx7NVA45k1gdcxER1gYouNX3n1vG3/B1F4+93wXW9Mqo6Us59TQmrb9Nqa7pSQNSrH06w1GxyBAlkFfqAlafk16aMpq416TJYpGy43WCUiCcLWlhiWgz2sgOLAXP9a5weWAKRC8J6jkcUkDeXnW+HFSiPM6+9Bo6kVhLUhN5rc4mHZOZpF7ptPpsQN4XCqLPsqpRH/8o5XpyC6ZneTeF2ExtjhwZVjXDwMbIzVYIbw1y2PPGw8dC7pc4sIVSnI4oQAH0v650ghfhmMevWW4h4Pylbw/DG4fdeT3+8z0f8MOSTtfipx05/QttHlAvODRxa5yDCvOz6n3f1BO0N/4MbGyOM27qW4x8mBVoryUZWfsmhfi0/GfNvB8+ylTGbYNX27OcEF5WZtNqbWEy7V5GbmZUSxspcg7FBZ3j3/3L7muvZXoZi1f5erq4y3lSH+oF7qPFnvieyLOCwubcQy+oyowj4cD0idRecnp+s+52D7+mzF1gpW+f+cJasoxigbxkOloS4yeiKvgM4nWSpGEj12RPx5o80EmziXN9t4zJzW1htA5hXJJeFympMYtyCUCevRWVmrRYktQ7UnCa6tL/AVPi1ni5XCXHfLOuqfK5KLYxjwVtdhhfmCLvpaKVNCEOCf4nNGHqD98wUb0ejWNwDYBnJGkBRw9RVspfwNdoWoVcn2qv8ywGUp7yc/sk2LpI6VziRyvWqbGNG1ZJqVVutRc46+sOE4VSeS2/CTNKNa6NMCf/hxxaUVsYfKdP+S1Lc4ianKp0sUugPpmtoydC0CP2RwjnHLcnzxCnvqxlLQWXMNPTsu2yBcHlWJLtcmdg/McLagy7l5kG6sN7ZxJLD/kAx8E6uqohP+K/jMSrYuQJnhr4tf4biWFY3Jzf/KpAYyVSq+pbfFKXMbL9nHEoZQT15Wyabd6DMAq3QZOthM5pUzdNqUXkcrI1PwjASAg4dVU/2o+98mTXnyhP7VlEkTnddpwyJF+YCCc01pqa6ZX99pnR+YOpIrb5DtPcKq0t5cFCDyQxN1lUuqZcYujvX/GC7+6IpRTz/NkEY/5cntOfZJyd6SkUx/JR5AJIRPCNeib0rcWnnFn0XK8s22x40wLjEtufXDO9+yraf4kHxlBI3SB0meMjepX50CJhAx7NekkRFnwoecLfqIyrjXrgAZsTMPiX94snjdrzisAXtBKanulilSxOfW2HSmfnk0L2GXHJJ3S+vCbgREml9dekfmHhmwEYtv3kfhth7ERIwVT8j69OtIBCmeTIJuA7o73PXc1LJ2whuZK7FZe6NTOs61MP7JVlIc6tbhtOBk3NXU8pBPOCY6Q55d63FtpUU/KCbQ0K2WQ9o5jNGwNNJ6Cw5FDCt+x5YJeP1d2QzDSiq+cSZqwqCZVcNoAftGGzgrRZ4vkpbP0xQZHCtWsUwHVTjrY4NfRDjHdVPRn8J9Xtooc/hIWkqzFaczYTlapu2RCJqe8aEo3jzWZCEJ7AiBzBYraYDgOfeiQyeJUsbbXHdGKiiw22Bdp4M+Yz671MZ0ie9EWRks0Q2DHGUTne9S4tAAJPGbUf4xtCMoJ40Q4QW05QLSZy3wNFAUFYmXE4ECDFB8+IEeNFFF/tgAnfh8GvFh/1xY+LE+ozMminjSRwhq09zQAEkNFyu0IKgvDkyOI+iccF7doWLTUasPxLH1xGPBSKkeWCnSoX/4C2c9du66BbRXamHqae+VCiS6BN953iYKOu8rMkneW+TFjV0CzAf8PNG9LBayUgY5Uw9x0Sap6zI4PpaZKH4d9ktkFU+91yBbN5SRtdd8/EQatJXmBNfw7kRrll+LjwZweWLEb0jT1RpGuDqHLdLTMMgowSczrp2zCsJdQH/seaYrpwkCFR8dRjM7JRUkVa1A0SvdZAX75vuBvvLBzN2c+Rbt5oqy93lqIBIjKb7w2UkRTb4pBWcNKNPUgt3az/PxQxEasaltdmuyvsXYbur3VPNn8JzN/6J5N+QCnz571TccapCmkPvFE+gayXU+UF1VKYyFcUvlev7lsQlmjqzK2QA8X7Q3TzFapqhvJKlt7dzBHraqZBgPgb9N4oKNayi8FZT71+kaYhIhFrESKYRHfHkj5n3kfhgLy+x+jLDbKxMggkgHnJz9RuEHZXBWtkYAQ==', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_verifier', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAMRdS21FeJfzQhM3s', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7396, 'output_tokens': 770, 'output_tokens_details': {'thinking_tokens': 739}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='skeptic', id='lc_run--01a0fe03-4ae9-76c0-89a6-4f0b114e3030-0', tool_calls=[{'name': 'transfer_to_verifier', 'args': {}, 'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7396, 'output_tokens': 770, 'total_tokens': 8166, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 739}}), ToolMessage(content='Successfully transferred to verifier', name='transfer_to_verifier', id='c064563a-974f-4f0f-ab4a-080e63c01cf5', tool_call_id='toolu_01H6fV8JvRJNvXy47eRnd3jH'), AIMessage(content=[{'signature': 'CAQS9gUKEAgSGAI4AUIIdGhpbmtpbmcSDGFyJ1MXoY+Ai6NkbxoMRd2TrUfUU7WrPpA2IjDgy8dUplcZKc7DnJwNLTBfH5UNR+nGnakSPQia8UVIzrjkOnJ2fLrWUsA2oEO8yRsqkwXkkDCsyhB97dwj57RMt91XzgWhL9luNEGO92yW0rNFGoMSUVOxPIrFc5RjyrhKEc3+1RnpxCkylq+LNlQ5NFxLn6v2cF5DzqpvBHNR7XAig+urJIU3gXydCkaGZZDetJtQOUOXgZi9z9100NpdAEkXDo2ICHCGTGpVc0J/pbXxWRBxb6B1/Gl0Yjn68V6HlesH7IoC7GR/3HwRu9NSDBG4ttfQjEWcg+PeAea4Rn7jzx+Tm9H7L1joTKVgDbQg5iKMLfjvPqlnksCLtXOt8jSIS5OYLN7a/7TX6/w5d34vnw8vPDmU1UYi0Q72qUWhUa2vw8OXNpcKL7Bp0lNfBS8rvHY+vBY8/haAoTL3wvLFU1fT+4z/vA3BcsigZDYlfJeDYcxXmRuqxzxEGpXCRe9s1HWtID+41pHu6lpZHkXaghpc6TZHLxNwcVhTb6fMQ7Qo+2L6Pvklh1m32peodxeg2YiPhdpl3w/xkOEu5/JEF7ZBxpdUL+u3wkO5pUlsqfgX7uhzv7HC2WpSWVimrwiQwQOUx0+JPrLtw+lYfq7I3jruo0GW0guWvnuf7JM3YLj84q+JgHomnSSZnnZmrL0UaRvrVyLo877F39ccLS7XoZnnui8J4P58ZTmiLkEJeGcx1Iuag2e33339O7Yew/59tLbQtIGh9VWMwHBnFx39S8/4LEFrK5HldPQhOrloyTtkRs66iQRKtxMKS0VI6QTKZDAbs7a7H1yBv+mJi9Xg4uKmyU6qnn7zEKjjY2bIlCwezx4LLGP5HD3Zr6fZsmdG1UHzDsBvYG2oCI0oqEKM+valsT6hU8MRjbQBD/JJ8JJN/iBGlhBAUHo0UmhhtR2euTVpZWlMnhmpXAdKDXSfPdaPohgB', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_red_team', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAsabDquegHi6YUpCn', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7509, 'output_tokens': 88, 'output_tokens_details': {'thinking_tokens': 55}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='verifier', id='lc_run--01a0fe03-6682-7d21-8c15-ffbce9422c9f-0', tool_calls=[{'name': 'transfer_to_red_team', 'args': {}, 'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7509, 'output_tokens': 88, 'total_tokens': 7597, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 55}}), ToolMessage(content='Successfully transferred to red_team', name='transfer_to_red_team', id='982c0126-e6ab-458a-922e-7b266e83c2b2', tool_call_id='toolu_01XEAuhQHCBdyCwtymZwFfDi'), AIMessage(content=[{'signature': 'CAQSwgcKEAgSGAI4AUIIdGhpbmtpbmcSDORXge9uL1832RMQwBoMjQwAwAHb9iP1ojPGIjDDScal486beCJbtG8/D2Y9g9S5xIPfjoHg/dxwX5HypSQEBbjFiTSbPECXSFzV6YIq3wbLcmyPbvusGnDexFgx+3sIXzvqyFV/69xhihMnSpfTCHy0+0zSu1WpYU/WUpIfoaSXTcTa1qkM8x8Ek/y/1jkKMZUfDFa93uhUJxOC4+QgPWBl/+Xy2t82L2x/ebq7QLQTqtR0jXeaURQm3Co6JvOgWmDvMpEdTeTWE3fDh1tzzVKsflum4d89b6CaKtn8nlQd7RhN99vNQMQHG5V4OuVNdrHPZZpFOuhdejsMFWXyB+55slGAd8wEXgFKbI/knOXVGzL/NDF8Gty1tNZlu2fdSCAsYWMsFQP1cbwLeQGszyXzJMVZJ2I9xBPkyz1HOXCPyh5F+j2gRTPikVB3ARC6tfB/lDSUeKBVZPQS8pY/B4eCs+IjjaBNWruLzfqCsc6xL0CuQPSYyQXaYFjYrSRNcC70WQBWT5VVhp0mA9yZDGYbRZt9I5OK9QkQmuJ5zDFaMEOXzSrsC4lteP6MdimiMijvsH3iq+c6WUYolRj3neI88EaHIzLAGRq64BNCLzdAYL2pEU2sb5uG78PKVoUUCbVJL5jpDOhLOTO1bQsTuoUpnRRrLQD/kIzV+ljpVBwwfJcyq75TJA/CtPr/xr2SZ9B1b2sbjG6jD0lhtoCddeDgcqgl/LSSkcuaYY8xPWCy+LtNhcqThfmfvnHLJIpL6gjbjW/OQpRfVUwfw1jOb9bWVwcgHhNYfKYGPtM8oth5xwLYRRVgzqMKOvs7falXRhVzZ2SP6Iw0BMmAAZ6HjfCl9L+tPqJNlnQQOHpoWjum4B+XsYldFEKGQWQqsd59iNPvao9p/sCOt7rGqCozcmgiEm4w66ez/QIDyb46n0+tcNJPUcoBKvQ/jDwE6VwmkQ/nECjFp3UdDT+kWYQSNd8j8VBZOpG8LlqUYrEZCv0q7Bs1vcNrdYiaFoS32osaynWk+Ht3OHuFWbEF8JlsFTQAO05VvYkecf0V/oTHsG5hyOOs7UEaDFq7uaJuzWVhh5pAojIaBcR8fVln2PLRTX7is2nsA+QrdidePcZtFtgxAiKYe1i2sSFi4vm8EOkCbQ63wdQeY4Urr5PuoMEwxUULjgKagiAmsJDq4RrVm7sO4+6YuA9j6l2uylkOEc67vf/5fkXuCsiApNL6keb8c+vL0n4FXGdzj+apSDfFGBgB', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01VBrWdWEWYgit4kNYehBMU5', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_editor', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAzfjqUopoEmZLaqm1', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7416, 'output_tokens': 151, 'output_tokens_details': {'thinking_tokens': 119}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='red_team', id='lc_run--01a0fe03-6ce6-7251-b796-4f64333f67c6-0', tool_calls=[{'name': 'transfer_to_editor', 'args': {}, 'id': 'toolu_01VBrWdWEWYgit4kNYehBMU5', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7416, 'output_tokens': 151, 'total_tokens': 7567, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 119}}), ToolMessage(content='Successfully transferred to editor', name='transfer_to_editor', tool_call_id='toolu_01VBrWdWEWYgit4kNYehBMU5')], 'active_agent': 'editor'}, goto='editor')" + } + }, + { + "key": "exception.stacktrace", + "value": { + "stringValue": "Exception: ParentCommand(Command(graph='red_team:15ccef64-4162-9e02-e87c-4cb0eb6ea16b', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', id='389058d1-083f-4632-bbc5-09bfd3a0ffd2', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9'), AIMessage(content=[{'signature': 'CAQS1QgKEAgSGAI4AUIIdGhpbmtpbmcSDN6TfU6NDM971lEbzxoMvRUA1J9ox3fJQYT9IjArCQQGHBP4pRHPavXzRlbMbaYHC57Yj+Gbxgr0helvd2OIL/o6Rbt4PFA3FaIFUlgq8gc7KQI9FtGnvSAkLBRzIsUp/2d5Rd2zgEsfkFVeXsn8WYYZkinAQVVy6A7B3LaEJW0xb/x88Di67fedwx0Q+FlZtwcmT3Dcaa11JveyHRr3dm+zGSZP6CXeXNJ8nrTZ7W1YRTysD6w7o7Lp8h1Rwq4f4o8j+QUfY0vje+zJZsxHPmQNt3ykeKh99hgn6dbiz6/EHqmbHjSAu1M4SqEDfqIKThHYenamAZmcQ4XAOo6mdQ6+77VgpV1lC1/k52IiUoJjkGlQrnLCvutiI7KN+sgK8c2+2KCJrPRCvVV+BkStvP5jWWqzytpKQ+nHzhL448OrIA9gdJC39C8eDrXeRnI7t4FqlkXGoMlZAV9UMcrQHRxOMi8tOyTzi9oyBFDTSsQ+LJqer0tz1Ft9gek6vzg50SpVAFQdLERv8R1YqMaeI+xsvTAEW9MRgvRMkzLuzRT2guw3jD+Fjip37xgK5QI0R+CsOAmQPoTZizzcIyAUAuCJFCgq/gD/CibLxoymiVUI1an9Snt3KpNRoWdvWIne33V0pv8PHg9yW2JuLOqQGilLCsK/Q3oQr3upEMXxXGjCL7WUizvRZGFEEV+VplYt7v4tKZMj5pk3vdeM0MJ5XZg7PvUsRzycxcnrT3Knoc3qM+ttqZfTsb8d6U/pQifq7GuahFOHrvroqgJLj+/81Yy1pZFkjfJgY0XklzAnA6CFq/YT0gxbV+TH5IS+WaYOUzK36S8s5qqMwhngv4IzE3ctXXqnhIZXNznyVBqbYa/Gx2bCjcEj2n5RyGYRS/+MculgLBznQ9MkG2D7QP3Mq/nwHXKMIaLS8VK8hG3d0rAjlvnauXShNrJxtkj4qkwnxD4LrEjk1ue+PLcbLqYWwAr8n0s+41jrsIPVyyK9vFaEZrA9KaBxkJyiqx50GZNn7ckx+mkI18gH52iyVPtU8FKMXjugCl6mPLiHYiDQd+G63LudmOKys9itVnBDBNMP2b/I7+bIVRlheGaZWr6MATKl86B3y+0kuOrLRMyWcdibc2iQkZm+8f7dTUwbKbBJsKOFY73/8nlL/JiYpjz6nUIk+IqSjaFyK6XFlCcukWpWHpDJnSvbEzey7qHyCqJ+I+on8iP8GOtempQaS0/4kZGSwlrE6xOj6EwGpfjbDZKHkP/2RAdNYPQq3z7bdVJMJNnFMz6e37mzMpCBihwPdSFTRE3SCY66wvTgJzGCUyhHbSi5i1azzOXd8uHCSpT/6L1o6VU0pjoR8Kaa4wCWHGf7P/WEd+avyp02Jnu1cmpW0+Gq+vK9h0tdb2IClM/eNfdouQP+ep8MJUjraDRICmAJQY5obS71vniOo+reaFy0zRgB', 'thinking': '', 'type': 'thinking'}, {'signature': 'CAQS/REKEQgSGAI4AUIJbmFycmF0aW9uEgwFh3AVJZP3s8y1Hq0aDG76uhHONaScXAGShiIwGgjLXHUx1yZrkLPljjzTOQycpLJiKVwXcxm/EzKJm1s8M5WxuREWEldXcoaN0/msKpkRMT+eLb5ih9rAHULTomUNsUwJeu0jrJe4GLazy7sP5KtjEOEBKQsXUfoQ7fDkZOQUHZi9zzQN4hFNmYJUf3OVqf9TQNdC1+m//HAZPfWmAy6HtDvm7kgBJkCEoiFnm7tn/PX1hXGPRgWc6BTXUMr2WA8aKyqggRUbMH2+JYv5iWZddCc1up1Nbr3sF7WPcot9ZBhyHQXr692zeJk6SJgygIVrASbMly2bBxTWJB623gVgzpCmVSLZFpkKXgPcp8hGyDKOvF6SiQSHqGRxQofr7Vs1aPhfZixROJGD+MlrkggVAnGMyYIDUZpavtCeiU1JmBSz0084bXPtT/tnpsKOnSPYZQo3B0BXKbqA/WsKng8/V3Z8Uuajo6IVZ2hgA1WDduqQOQkExGU6pFuLhebIjuWxSNI/DffvR//LNqCfvdnf0hvoPtDxfx/SawIx4/spq9xbwFzHtLgoHg1F2wFw7clPDlg1PyD/v2yJG3V9T8GL6jGPE6rhfil/u4HNvf7BW0dQzCTTXcqZy4nByy0+p+S7ZxfMl4Kpxa2gOhhSNjfpfuX0QpL4kME8J1nliFN1d8KqEPuDI18BQHUhD/UfY29oA0x510FjM/BsLZ4n5sppkoIi7CFoQq2aAKVNfC1KilhIpRD+1dyDtZQklN8gZpOESHw6ML1TtU0yfAcbmqoxJ6hhjzwH6CMsTQEEy8MbRBM+QKKkIRdRCPHGFW2LkuR50+J0pF/tr6pqVvJeozfiMnrEilB27TZP4nKTJG8rd//vCWen0y7CA/8dBJ9gdp5tx7NVA45k1gdcxER1gYouNX3n1vG3/B1F4+93wXW9Mqo6Us59TQmrb9Nqa7pSQNSrH06w1GxyBAlkFfqAlafk16aMpq416TJYpGy43WCUiCcLWlhiWgz2sgOLAXP9a5weWAKRC8J6jkcUkDeXnW+HFSiPM6+9Bo6kVhLUhN5rc4mHZOZpF7ptPpsQN4XCqLPsqpRH/8o5XpyC6ZneTeF2ExtjhwZVjXDwMbIzVYIbw1y2PPGw8dC7pc4sIVSnI4oQAH0v650ghfhmMevWW4h4Pylbw/DG4fdeT3+8z0f8MOSTtfipx05/QttHlAvODRxa5yDCvOz6n3f1BO0N/4MbGyOM27qW4x8mBVoryUZWfsmhfi0/GfNvB8+ylTGbYNX27OcEF5WZtNqbWEy7V5GbmZUSxspcg7FBZ3j3/3L7muvZXoZi1f5erq4y3lSH+oF7qPFnvieyLOCwubcQy+oyowj4cD0idRecnp+s+52D7+mzF1gpW+f+cJasoxigbxkOloS4yeiKvgM4nWSpGEj12RPx5o80EmziXN9t4zJzW1htA5hXJJeFympMYtyCUCevRWVmrRYktQ7UnCa6tL/AVPi1ni5XCXHfLOuqfK5KLYxjwVtdhhfmCLvpaKVNCEOCf4nNGHqD98wUb0ejWNwDYBnJGkBRw9RVspfwNdoWoVcn2qv8ywGUp7yc/sk2LpI6VziRyvWqbGNG1ZJqVVutRc46+sOE4VSeS2/CTNKNa6NMCf/hxxaUVsYfKdP+S1Lc4ianKp0sUugPpmtoydC0CP2RwjnHLcnzxCnvqxlLQWXMNPTsu2yBcHlWJLtcmdg/McLagy7l5kG6sN7ZxJLD/kAx8E6uqohP+K/jMSrYuQJnhr4tf4biWFY3Jzf/KpAYyVSq+pbfFKXMbL9nHEoZQT15Wyabd6DMAq3QZOthM5pUzdNqUXkcrI1PwjASAg4dVU/2o+98mTXnyhP7VlEkTnddpwyJF+YCCc01pqa6ZX99pnR+YOpIrb5DtPcKq0t5cFCDyQxN1lUuqZcYujvX/GC7+6IpRTz/NkEY/5cntOfZJyd6SkUx/JR5AJIRPCNeib0rcWnnFn0XK8s22x40wLjEtufXDO9+yraf4kHxlBI3SB0meMjepX50CJhAx7NekkRFnwoecLfqIyrjXrgAZsTMPiX94snjdrzisAXtBKanulilSxOfW2HSmfnk0L2GXHJJ3S+vCbgREml9dekfmHhmwEYtv3kfhth7ERIwVT8j69OtIBCmeTIJuA7o73PXc1LJ2whuZK7FZe6NTOs61MP7JVlIc6tbhtOBk3NXU8pBPOCY6Q55d63FtpUU/KCbQ0K2WQ9o5jNGwNNJ6Cw5FDCt+x5YJeP1d2QzDSiq+cSZqwqCZVcNoAftGGzgrRZ4vkpbP0xQZHCtWsUwHVTjrY4NfRDjHdVPRn8J9Xtooc/hIWkqzFaczYTlapu2RCJqe8aEo3jzWZCEJ7AiBzBYraYDgOfeiQyeJUsbbXHdGKiiw22Bdp4M+Yz671MZ0ie9EWRks0Q2DHGUTne9S4tAAJPGbUf4xtCMoJ40Q4QW05QLSZy3wNFAUFYmXE4ECDFB8+IEeNFFF/tgAnfh8GvFh/1xY+LE+ozMminjSRwhq09zQAEkNFyu0IKgvDkyOI+iccF7doWLTUasPxLH1xGPBSKkeWCnSoX/4C2c9du66BbRXamHqae+VCiS6BN953iYKOu8rMkneW+TFjV0CzAf8PNG9LBayUgY5Uw9x0Sap6zI4PpaZKH4d9ktkFU+91yBbN5SRtdd8/EQatJXmBNfw7kRrll+LjwZweWLEb0jT1RpGuDqHLdLTMMgowSczrp2zCsJdQH/seaYrpwkCFR8dRjM7JRUkVa1A0SvdZAX75vuBvvLBzN2c+Rbt5oqy93lqIBIjKb7w2UkRTb4pBWcNKNPUgt3az/PxQxEasaltdmuyvsXYbur3VPNn8JzN/6J5N+QCnz571TccapCmkPvFE+gayXU+UF1VKYyFcUvlev7lsQlmjqzK2QA8X7Q3TzFapqhvJKlt7dzBHraqZBgPgb9N4oKNayi8FZT71+kaYhIhFrESKYRHfHkj5n3kfhgLy+x+jLDbKxMggkgHnJz9RuEHZXBWtkYAQ==', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_verifier', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAMRdS21FeJfzQhM3s', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7396, 'output_tokens': 770, 'output_tokens_details': {'thinking_tokens': 739}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='skeptic', id='lc_run--01a0fe03-4ae9-76c0-89a6-4f0b114e3030-0', tool_calls=[{'name': 'transfer_to_verifier', 'args': {}, 'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7396, 'output_tokens': 770, 'total_tokens': 8166, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 739}}), ToolMessage(content='Successfully transferred to verifier', name='transfer_to_verifier', id='c064563a-974f-4f0f-ab4a-080e63c01cf5', tool_call_id='toolu_01H6fV8JvRJNvXy47eRnd3jH'), AIMessage(content=[{'signature': 'CAQS9gUKEAgSGAI4AUIIdGhpbmtpbmcSDGFyJ1MXoY+Ai6NkbxoMRd2TrUfUU7WrPpA2IjDgy8dUplcZKc7DnJwNLTBfH5UNR+nGnakSPQia8UVIzrjkOnJ2fLrWUsA2oEO8yRsqkwXkkDCsyhB97dwj57RMt91XzgWhL9luNEGO92yW0rNFGoMSUVOxPIrFc5RjyrhKEc3+1RnpxCkylq+LNlQ5NFxLn6v2cF5DzqpvBHNR7XAig+urJIU3gXydCkaGZZDetJtQOUOXgZi9z9100NpdAEkXDo2ICHCGTGpVc0J/pbXxWRBxb6B1/Gl0Yjn68V6HlesH7IoC7GR/3HwRu9NSDBG4ttfQjEWcg+PeAea4Rn7jzx+Tm9H7L1joTKVgDbQg5iKMLfjvPqlnksCLtXOt8jSIS5OYLN7a/7TX6/w5d34vnw8vPDmU1UYi0Q72qUWhUa2vw8OXNpcKL7Bp0lNfBS8rvHY+vBY8/haAoTL3wvLFU1fT+4z/vA3BcsigZDYlfJeDYcxXmRuqxzxEGpXCRe9s1HWtID+41pHu6lpZHkXaghpc6TZHLxNwcVhTb6fMQ7Qo+2L6Pvklh1m32peodxeg2YiPhdpl3w/xkOEu5/JEF7ZBxpdUL+u3wkO5pUlsqfgX7uhzv7HC2WpSWVimrwiQwQOUx0+JPrLtw+lYfq7I3jruo0GW0guWvnuf7JM3YLj84q+JgHomnSSZnnZmrL0UaRvrVyLo877F39ccLS7XoZnnui8J4P58ZTmiLkEJeGcx1Iuag2e33339O7Yew/59tLbQtIGh9VWMwHBnFx39S8/4LEFrK5HldPQhOrloyTtkRs66iQRKtxMKS0VI6QTKZDAbs7a7H1yBv+mJi9Xg4uKmyU6qnn7zEKjjY2bIlCwezx4LLGP5HD3Zr6fZsmdG1UHzDsBvYG2oCI0oqEKM+valsT6hU8MRjbQBD/JJ8JJN/iBGlhBAUHo0UmhhtR2euTVpZWlMnhmpXAdKDXSfPdaPohgB', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_red_team', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAsabDquegHi6YUpCn', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7509, 'output_tokens': 88, 'output_tokens_details': {'thinking_tokens': 55}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='verifier', id='lc_run--01a0fe03-6682-7d21-8c15-ffbce9422c9f-0', tool_calls=[{'name': 'transfer_to_red_team', 'args': {}, 'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7509, 'output_tokens': 88, 'total_tokens': 7597, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 55}}), ToolMessage(content='Successfully transferred to red_team', name='transfer_to_red_team', id='982c0126-e6ab-458a-922e-7b266e83c2b2', tool_call_id='toolu_01XEAuhQHCBdyCwtymZwFfDi'), AIMessage(content=[{'signature': 'CAQSwgcKEAgSGAI4AUIIdGhpbmtpbmcSDORXge9uL1832RMQwBoMjQwAwAHb9iP1ojPGIjDDScal486beCJbtG8/D2Y9g9S5xIPfjoHg/dxwX5HypSQEBbjFiTSbPECXSFzV6YIq3wbLcmyPbvusGnDexFgx+3sIXzvqyFV/69xhihMnSpfTCHy0+0zSu1WpYU/WUpIfoaSXTcTa1qkM8x8Ek/y/1jkKMZUfDFa93uhUJxOC4+QgPWBl/+Xy2t82L2x/ebq7QLQTqtR0jXeaURQm3Co6JvOgWmDvMpEdTeTWE3fDh1tzzVKsflum4d89b6CaKtn8nlQd7RhN99vNQMQHG5V4OuVNdrHPZZpFOuhdejsMFWXyB+55slGAd8wEXgFKbI/knOXVGzL/NDF8Gty1tNZlu2fdSCAsYWMsFQP1cbwLeQGszyXzJMVZJ2I9xBPkyz1HOXCPyh5F+j2gRTPikVB3ARC6tfB/lDSUeKBVZPQS8pY/B4eCs+IjjaBNWruLzfqCsc6xL0CuQPSYyQXaYFjYrSRNcC70WQBWT5VVhp0mA9yZDGYbRZt9I5OK9QkQmuJ5zDFaMEOXzSrsC4lteP6MdimiMijvsH3iq+c6WUYolRj3neI88EaHIzLAGRq64BNCLzdAYL2pEU2sb5uG78PKVoUUCbVJL5jpDOhLOTO1bQsTuoUpnRRrLQD/kIzV+ljpVBwwfJcyq75TJA/CtPr/xr2SZ9B1b2sbjG6jD0lhtoCddeDgcqgl/LSSkcuaYY8xPWCy+LtNhcqThfmfvnHLJIpL6gjbjW/OQpRfVUwfw1jOb9bWVwcgHhNYfKYGPtM8oth5xwLYRRVgzqMKOvs7falXRhVzZ2SP6Iw0BMmAAZ6HjfCl9L+tPqJNlnQQOHpoWjum4B+XsYldFEKGQWQqsd59iNPvao9p/sCOt7rGqCozcmgiEm4w66ez/QIDyb46n0+tcNJPUcoBKvQ/jDwE6VwmkQ/nECjFp3UdDT+kWYQSNd8j8VBZOpG8LlqUYrEZCv0q7Bs1vcNrdYiaFoS32osaynWk+Ht3OHuFWbEF8JlsFTQAO05VvYkecf0V/oTHsG5hyOOs7UEaDFq7uaJuzWVhh5pAojIaBcR8fVln2PLRTX7is2nsA+QrdidePcZtFtgxAiKYe1i2sSFi4vm8EOkCbQ63wdQeY4Urr5PuoMEwxUULjgKagiAmsJDq4RrVm7sO4+6YuA9j6l2uylkOEc67vf/5fkXuCsiApNL6keb8c+vL0n4FXGdzj+apSDfFGBgB', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01VBrWdWEWYgit4kNYehBMU5', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_editor', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAzfjqUopoEmZLaqm1', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7416, 'output_tokens': 151, 'output_tokens_details': {'thinking_tokens': 119}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='red_team', id='lc_run--01a0fe03-6ce6-7251-b796-4f64333f67c6-0', tool_calls=[{'name': 'transfer_to_editor', 'args': {}, 'id': 'toolu_01VBrWdWEWYgit4kNYehBMU5', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7416, 'output_tokens': 151, 'total_tokens': 7567, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 119}}), ToolMessage(content='Successfully transferred to editor', name='transfer_to_editor', tool_call_id='toolu_01VBrWdWEWYgit4kNYehBMU5')], 'active_agent': 'editor'}, goto='editor'))Traceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 2967, in stream\n for _ in runner.tick(\n ~~~~~~~~~~~^\n [t for t in loop.tasks.values() if not t.writes],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<2 lines>...\n schedule_task=loop.accept_push,\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ):\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_runner.py\", line 207, in tick\n run_with_retry(\n ~~~~~~~~~~~~~~^\n t,\n ^^\n ...<10 lines>...\n },\n ^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_retry.py\", line 617, in run_with_retry\n return task.proc.invoke(task.input, config)\n ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 709, in invoke\n input = step.invoke(input, config)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 447, in invoke\n ret = self.func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 84, in _write\n self.do_write(\n ~~~~~~~~~~~~~^\n config,\n ^^^^^^^\n writes,\n ^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 126, in do_write\n write(_assemble_writes(writes))\n ~~~~~~~~~~~~~~~~^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 181, in _assemble_writes\n if ww := w.mapper(w.value):\n ~~~~~~~~^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/graph/state.py\", line 1762, in _control_branch\n raise ParentCommand(command)\n\n\nlanggraph.errors.ParentCommand: Command(graph='red_team:15ccef64-4162-9e02-e87c-4cb0eb6ea16b', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', id='389058d1-083f-4632-bbc5-09bfd3a0ffd2', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9'), AIMessage(content=[{'signature': 'CAQS1QgKEAgSGAI4AUIIdGhpbmtpbmcSDN6TfU6NDM971lEbzxoMvRUA1J9ox3fJQYT9IjArCQQGHBP4pRHPavXzRlbMbaYHC57Yj+Gbxgr0helvd2OIL/o6Rbt4PFA3FaIFUlgq8gc7KQI9FtGnvSAkLBRzIsUp/2d5Rd2zgEsfkFVeXsn8WYYZkinAQVVy6A7B3LaEJW0xb/x88Di67fedwx0Q+FlZtwcmT3Dcaa11JveyHRr3dm+zGSZP6CXeXNJ8nrTZ7W1YRTysD6w7o7Lp8h1Rwq4f4o8j+QUfY0vje+zJZsxHPmQNt3ykeKh99hgn6dbiz6/EHqmbHjSAu1M4SqEDfqIKThHYenamAZmcQ4XAOo6mdQ6+77VgpV1lC1/k52IiUoJjkGlQrnLCvutiI7KN+sgK8c2+2KCJrPRCvVV+BkStvP5jWWqzytpKQ+nHzhL448OrIA9gdJC39C8eDrXeRnI7t4FqlkXGoMlZAV9UMcrQHRxOMi8tOyTzi9oyBFDTSsQ+LJqer0tz1Ft9gek6vzg50SpVAFQdLERv8R1YqMaeI+xsvTAEW9MRgvRMkzLuzRT2guw3jD+Fjip37xgK5QI0R+CsOAmQPoTZizzcIyAUAuCJFCgq/gD/CibLxoymiVUI1an9Snt3KpNRoWdvWIne33V0pv8PHg9yW2JuLOqQGilLCsK/Q3oQr3upEMXxXGjCL7WUizvRZGFEEV+VplYt7v4tKZMj5pk3vdeM0MJ5XZg7PvUsRzycxcnrT3Knoc3qM+ttqZfTsb8d6U/pQifq7GuahFOHrvroqgJLj+/81Yy1pZFkjfJgY0XklzAnA6CFq/YT0gxbV+TH5IS+WaYOUzK36S8s5qqMwhngv4IzE3ctXXqnhIZXNznyVBqbYa/Gx2bCjcEj2n5RyGYRS/+MculgLBznQ9MkG2D7QP3Mq/nwHXKMIaLS8VK8hG3d0rAjlvnauXShNrJxtkj4qkwnxD4LrEjk1ue+PLcbLqYWwAr8n0s+41jrsIPVyyK9vFaEZrA9KaBxkJyiqx50GZNn7ckx+mkI18gH52iyVPtU8FKMXjugCl6mPLiHYiDQd+G63LudmOKys9itVnBDBNMP2b/I7+bIVRlheGaZWr6MATKl86B3y+0kuOrLRMyWcdibc2iQkZm+8f7dTUwbKbBJsKOFY73/8nlL/JiYpjz6nUIk+IqSjaFyK6XFlCcukWpWHpDJnSvbEzey7qHyCqJ+I+on8iP8GOtempQaS0/4kZGSwlrE6xOj6EwGpfjbDZKHkP/2RAdNYPQq3z7bdVJMJNnFMz6e37mzMpCBihwPdSFTRE3SCY66wvTgJzGCUyhHbSi5i1azzOXd8uHCSpT/6L1o6VU0pjoR8Kaa4wCWHGf7P/WEd+avyp02Jnu1cmpW0+Gq+vK9h0tdb2IClM/eNfdouQP+ep8MJUjraDRICmAJQY5obS71vniOo+reaFy0zRgB', 'thinking': '', 'type': 'thinking'}, {'signature': 'CAQS/REKEQgSGAI4AUIJbmFycmF0aW9uEgwFh3AVJZP3s8y1Hq0aDG76uhHONaScXAGShiIwGgjLXHUx1yZrkLPljjzTOQycpLJiKVwXcxm/EzKJm1s8M5WxuREWEldXcoaN0/msKpkRMT+eLb5ih9rAHULTomUNsUwJeu0jrJe4GLazy7sP5KtjEOEBKQsXUfoQ7fDkZOQUHZi9zzQN4hFNmYJUf3OVqf9TQNdC1+m//HAZPfWmAy6HtDvm7kgBJkCEoiFnm7tn/PX1hXGPRgWc6BTXUMr2WA8aKyqggRUbMH2+JYv5iWZddCc1up1Nbr3sF7WPcot9ZBhyHQXr692zeJk6SJgygIVrASbMly2bBxTWJB623gVgzpCmVSLZFpkKXgPcp8hGyDKOvF6SiQSHqGRxQofr7Vs1aPhfZixROJGD+MlrkggVAnGMyYIDUZpavtCeiU1JmBSz0084bXPtT/tnpsKOnSPYZQo3B0BXKbqA/WsKng8/V3Z8Uuajo6IVZ2hgA1WDduqQOQkExGU6pFuLhebIjuWxSNI/DffvR//LNqCfvdnf0hvoPtDxfx/SawIx4/spq9xbwFzHtLgoHg1F2wFw7clPDlg1PyD/v2yJG3V9T8GL6jGPE6rhfil/u4HNvf7BW0dQzCTTXcqZy4nByy0+p+S7ZxfMl4Kpxa2gOhhSNjfpfuX0QpL4kME8J1nliFN1d8KqEPuDI18BQHUhD/UfY29oA0x510FjM/BsLZ4n5sppkoIi7CFoQq2aAKVNfC1KilhIpRD+1dyDtZQklN8gZpOESHw6ML1TtU0yfAcbmqoxJ6hhjzwH6CMsTQEEy8MbRBM+QKKkIRdRCPHGFW2LkuR50+J0pF/tr6pqVvJeozfiMnrEilB27TZP4nKTJG8rd//vCWen0y7CA/8dBJ9gdp5tx7NVA45k1gdcxER1gYouNX3n1vG3/B1F4+93wXW9Mqo6Us59TQmrb9Nqa7pSQNSrH06w1GxyBAlkFfqAlafk16aMpq416TJYpGy43WCUiCcLWlhiWgz2sgOLAXP9a5weWAKRC8J6jkcUkDeXnW+HFSiPM6+9Bo6kVhLUhN5rc4mHZOZpF7ptPpsQN4XCqLPsqpRH/8o5XpyC6ZneTeF2ExtjhwZVjXDwMbIzVYIbw1y2PPGw8dC7pc4sIVSnI4oQAH0v650ghfhmMevWW4h4Pylbw/DG4fdeT3+8z0f8MOSTtfipx05/QttHlAvODRxa5yDCvOz6n3f1BO0N/4MbGyOM27qW4x8mBVoryUZWfsmhfi0/GfNvB8+ylTGbYNX27OcEF5WZtNqbWEy7V5GbmZUSxspcg7FBZ3j3/3L7muvZXoZi1f5erq4y3lSH+oF7qPFnvieyLOCwubcQy+oyowj4cD0idRecnp+s+52D7+mzF1gpW+f+cJasoxigbxkOloS4yeiKvgM4nWSpGEj12RPx5o80EmziXN9t4zJzW1htA5hXJJeFympMYtyCUCevRWVmrRYktQ7UnCa6tL/AVPi1ni5XCXHfLOuqfK5KLYxjwVtdhhfmCLvpaKVNCEOCf4nNGHqD98wUb0ejWNwDYBnJGkBRw9RVspfwNdoWoVcn2qv8ywGUp7yc/sk2LpI6VziRyvWqbGNG1ZJqVVutRc46+sOE4VSeS2/CTNKNa6NMCf/hxxaUVsYfKdP+S1Lc4ianKp0sUugPpmtoydC0CP2RwjnHLcnzxCnvqxlLQWXMNPTsu2yBcHlWJLtcmdg/McLagy7l5kG6sN7ZxJLD/kAx8E6uqohP+K/jMSrYuQJnhr4tf4biWFY3Jzf/KpAYyVSq+pbfFKXMbL9nHEoZQT15Wyabd6DMAq3QZOthM5pUzdNqUXkcrI1PwjASAg4dVU/2o+98mTXnyhP7VlEkTnddpwyJF+YCCc01pqa6ZX99pnR+YOpIrb5DtPcKq0t5cFCDyQxN1lUuqZcYujvX/GC7+6IpRTz/NkEY/5cntOfZJyd6SkUx/JR5AJIRPCNeib0rcWnnFn0XK8s22x40wLjEtufXDO9+yraf4kHxlBI3SB0meMjepX50CJhAx7NekkRFnwoecLfqIyrjXrgAZsTMPiX94snjdrzisAXtBKanulilSxOfW2HSmfnk0L2GXHJJ3S+vCbgREml9dekfmHhmwEYtv3kfhth7ERIwVT8j69OtIBCmeTIJuA7o73PXc1LJ2whuZK7FZe6NTOs61MP7JVlIc6tbhtOBk3NXU8pBPOCY6Q55d63FtpUU/KCbQ0K2WQ9o5jNGwNNJ6Cw5FDCt+x5YJeP1d2QzDSiq+cSZqwqCZVcNoAftGGzgrRZ4vkpbP0xQZHCtWsUwHVTjrY4NfRDjHdVPRn8J9Xtooc/hIWkqzFaczYTlapu2RCJqe8aEo3jzWZCEJ7AiBzBYraYDgOfeiQyeJUsbbXHdGKiiw22Bdp4M+Yz671MZ0ie9EWRks0Q2DHGUTne9S4tAAJPGbUf4xtCMoJ40Q4QW05QLSZy3wNFAUFYmXE4ECDFB8+IEeNFFF/tgAnfh8GvFh/1xY+LE+ozMminjSRwhq09zQAEkNFyu0IKgvDkyOI+iccF7doWLTUasPxLH1xGPBSKkeWCnSoX/4C2c9du66BbRXamHqae+VCiS6BN953iYKOu8rMkneW+TFjV0CzAf8PNG9LBayUgY5Uw9x0Sap6zI4PpaZKH4d9ktkFU+91yBbN5SRtdd8/EQatJXmBNfw7kRrll+LjwZweWLEb0jT1RpGuDqHLdLTMMgowSczrp2zCsJdQH/seaYrpwkCFR8dRjM7JRUkVa1A0SvdZAX75vuBvvLBzN2c+Rbt5oqy93lqIBIjKb7w2UkRTb4pBWcNKNPUgt3az/PxQxEasaltdmuyvsXYbur3VPNn8JzN/6J5N+QCnz571TccapCmkPvFE+gayXU+UF1VKYyFcUvlev7lsQlmjqzK2QA8X7Q3TzFapqhvJKlt7dzBHraqZBgPgb9N4oKNayi8FZT71+kaYhIhFrESKYRHfHkj5n3kfhgLy+x+jLDbKxMggkgHnJz9RuEHZXBWtkYAQ==', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_verifier', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAMRdS21FeJfzQhM3s', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7396, 'output_tokens': 770, 'output_tokens_details': {'thinking_tokens': 739}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='skeptic', id='lc_run--01a0fe03-4ae9-76c0-89a6-4f0b114e3030-0', tool_calls=[{'name': 'transfer_to_verifier', 'args': {}, 'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7396, 'output_tokens': 770, 'total_tokens': 8166, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 739}}), ToolMessage(content='Successfully transferred to verifier', name='transfer_to_verifier', id='c064563a-974f-4f0f-ab4a-080e63c01cf5', tool_call_id='toolu_01H6fV8JvRJNvXy47eRnd3jH'), AIMessage(content=[{'signature': 'CAQS9gUKEAgSGAI4AUIIdGhpbmtpbmcSDGFyJ1MXoY+Ai6NkbxoMRd2TrUfUU7WrPpA2IjDgy8dUplcZKc7DnJwNLTBfH5UNR+nGnakSPQia8UVIzrjkOnJ2fLrWUsA2oEO8yRsqkwXkkDCsyhB97dwj57RMt91XzgWhL9luNEGO92yW0rNFGoMSUVOxPIrFc5RjyrhKEc3+1RnpxCkylq+LNlQ5NFxLn6v2cF5DzqpvBHNR7XAig+urJIU3gXydCkaGZZDetJtQOUOXgZi9z9100NpdAEkXDo2ICHCGTGpVc0J/pbXxWRBxb6B1/Gl0Yjn68V6HlesH7IoC7GR/3HwRu9NSDBG4ttfQjEWcg+PeAea4Rn7jzx+Tm9H7L1joTKVgDbQg5iKMLfjvPqlnksCLtXOt8jSIS5OYLN7a/7TX6/w5d34vnw8vPDmU1UYi0Q72qUWhUa2vw8OXNpcKL7Bp0lNfBS8rvHY+vBY8/haAoTL3wvLFU1fT+4z/vA3BcsigZDYlfJeDYcxXmRuqxzxEGpXCRe9s1HWtID+41pHu6lpZHkXaghpc6TZHLxNwcVhTb6fMQ7Qo+2L6Pvklh1m32peodxeg2YiPhdpl3w/xkOEu5/JEF7ZBxpdUL+u3wkO5pUlsqfgX7uhzv7HC2WpSWVimrwiQwQOUx0+JPrLtw+lYfq7I3jruo0GW0guWvnuf7JM3YLj84q+JgHomnSSZnnZmrL0UaRvrVyLo877F39ccLS7XoZnnui8J4P58ZTmiLkEJeGcx1Iuag2e33339O7Yew/59tLbQtIGh9VWMwHBnFx39S8/4LEFrK5HldPQhOrloyTtkRs66iQRKtxMKS0VI6QTKZDAbs7a7H1yBv+mJi9Xg4uKmyU6qnn7zEKjjY2bIlCwezx4LLGP5HD3Zr6fZsmdG1UHzDsBvYG2oCI0oqEKM+valsT6hU8MRjbQBD/JJ8JJN/iBGlhBAUHo0UmhhtR2euTVpZWlMnhmpXAdKDXSfPdaPohgB', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_red_team', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAsabDquegHi6YUpCn', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7509, 'output_tokens': 88, 'output_tokens_details': {'thinking_tokens': 55}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='verifier', id='lc_run--01a0fe03-6682-7d21-8c15-ffbce9422c9f-0', tool_calls=[{'name': 'transfer_to_red_team', 'args': {}, 'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7509, 'output_tokens': 88, 'total_tokens': 7597, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 55}}), ToolMessage(content='Successfully transferred to red_team', name='transfer_to_red_team', id='982c0126-e6ab-458a-922e-7b266e83c2b2', tool_call_id='toolu_01XEAuhQHCBdyCwtymZwFfDi'), AIMessage(content=[{'signature': 'CAQSwgcKEAgSGAI4AUIIdGhpbmtpbmcSDORXge9uL1832RMQwBoMjQwAwAHb9iP1ojPGIjDDScal486beCJbtG8/D2Y9g9S5xIPfjoHg/dxwX5HypSQEBbjFiTSbPECXSFzV6YIq3wbLcmyPbvusGnDexFgx+3sIXzvqyFV/69xhihMnSpfTCHy0+0zSu1WpYU/WUpIfoaSXTcTa1qkM8x8Ek/y/1jkKMZUfDFa93uhUJxOC4+QgPWBl/+Xy2t82L2x/ebq7QLQTqtR0jXeaURQm3Co6JvOgWmDvMpEdTeTWE3fDh1tzzVKsflum4d89b6CaKtn8nlQd7RhN99vNQMQHG5V4OuVNdrHPZZpFOuhdejsMFWXyB+55slGAd8wEXgFKbI/knOXVGzL/NDF8Gty1tNZlu2fdSCAsYWMsFQP1cbwLeQGszyXzJMVZJ2I9xBPkyz1HOXCPyh5F+j2gRTPikVB3ARC6tfB/lDSUeKBVZPQS8pY/B4eCs+IjjaBNWruLzfqCsc6xL0CuQPSYyQXaYFjYrSRNcC70WQBWT5VVhp0mA9yZDGYbRZt9I5OK9QkQmuJ5zDFaMEOXzSrsC4lteP6MdimiMijvsH3iq+c6WUYolRj3neI88EaHIzLAGRq64BNCLzdAYL2pEU2sb5uG78PKVoUUCbVJL5jpDOhLOTO1bQsTuoUpnRRrLQD/kIzV+ljpVBwwfJcyq75TJA/CtPr/xr2SZ9B1b2sbjG6jD0lhtoCddeDgcqgl/LSSkcuaYY8xPWCy+LtNhcqThfmfvnHLJIpL6gjbjW/OQpRfVUwfw1jOb9bWVwcgHhNYfKYGPtM8oth5xwLYRRVgzqMKOvs7falXRhVzZ2SP6Iw0BMmAAZ6HjfCl9L+tPqJNlnQQOHpoWjum4B+XsYldFEKGQWQqsd59iNPvao9p/sCOt7rGqCozcmgiEm4w66ez/QIDyb46n0+tcNJPUcoBKvQ/jDwE6VwmkQ/nECjFp3UdDT+kWYQSNd8j8VBZOpG8LlqUYrEZCv0q7Bs1vcNrdYiaFoS32osaynWk+Ht3OHuFWbEF8JlsFTQAO05VvYkecf0V/oTHsG5hyOOs7UEaDFq7uaJuzWVhh5pAojIaBcR8fVln2PLRTX7is2nsA+QrdidePcZtFtgxAiKYe1i2sSFi4vm8EOkCbQ63wdQeY4Urr5PuoMEwxUULjgKagiAmsJDq4RrVm7sO4+6YuA9j6l2uylkOEc67vf/5fkXuCsiApNL6keb8c+vL0n4FXGdzj+apSDfFGBgB', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01VBrWdWEWYgit4kNYehBMU5', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_editor', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAzfjqUopoEmZLaqm1', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7416, 'output_tokens': 151, 'output_tokens_details': {'thinking_tokens': 119}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='red_team', id='lc_run--01a0fe03-6ce6-7251-b796-4f64333f67c6-0', tool_calls=[{'name': 'transfer_to_editor', 'args': {}, 'id': 'toolu_01VBrWdWEWYgit4kNYehBMU5', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7416, 'output_tokens': 151, 'total_tokens': 7567, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 119}}), ToolMessage(content='Successfully transferred to editor', name='transfer_to_editor', tool_call_id='toolu_01VBrWdWEWYgit4kNYehBMU5')], 'active_agent': 'editor'}, goto='editor')\n" + } + }, + { + "key": "exception.escaped", + "value": { + "stringValue": "False" + } + } + ] + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "ad48a4e078446202", + "parentSpanId": "04ffc0db9ce0f358", + "name": "red_team", + "kind": 1, + "startTimeUnixNano": "1790968032481682176", + "endTimeUnixNano": "1790968035042222080", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "e30=" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "red_team" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langgraph" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "4" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "red_team" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:red_team\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"red_team\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "red_team:15ccef64-4162-9e02-e87c-4cb0eb6ea16b" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "graph:step:4" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W3siY29udGVudCI6IkZvciBhIHN5bnRoZXRpYyB0cmFjaW5nIGZpeHR1cmUsIGV4cGxhaW4gUHl0aG9uIHR1cGxlIHZlcnN1cyBsaXN0IGltbXV0YWJpbGl0eSBpbiBhdCBtb3N0IDEyMCB3b3Jkcy4gVXNlIG9ubHkgb2ZmaWNpYWwgUHl0aG9uIGRvY3VtZW50YXRpb24gaWYgeW91IHNlYXJjaC4gSGF2ZSB0aGUgc2tlcHRpYyBhbmQgdmVyaWZpZXIgY2hlY2sgaXQsIHRoZW4gaGF2ZSB0aGUgZWRpdG9yIHdyaXRlIC9hbnN3ZXIubWQuIEtlZXAgdGhlIHJ1biBzaG9ydCBhbmQgZG8gbm90IGFzayBmb2xsb3ctdXAgcXVlc3Rpb25zLiIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoiaHVtYW4iLCJpZCI6IjQ2MjA0MzA1LWY4ZWUtNDM4NC05Y2U4LWZjZDgwYmZjZGY3NSJ9LHsiY29udGVudCI6W3siaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9LCJuYW1lIjoid2ViX3NlYXJjaCIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHc5YVc5YnJlblBxZ3liZk53UCIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjoyODQ1LCJvdXRwdXRfdG9rZW5zIjo3Mywib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6MH0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoicmVzZWFyY2hlciIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy0yMWEzLTcwMDAtYTM4MS0yODRhNmEwYzAzZjctMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ3ZWJfc2VhcmNoIiwiYXJncyI6eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9LCJpZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6Mjg0NSwib3V0cHV0X3Rva2VucyI6NzMsInRvdGFsX3Rva2VucyI6MjkxOCwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fSx7ImNvbnRlbnQiOiJbe1widGl0bGVcIjogXCI1LiBEYXRhIFN0cnVjdHVyZXMgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3R1dG9yaWFsL2RhdGFzdHJ1Y3R1cmVzLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcIllvdSBtaWdodCBoYXZlIG5vdGljZWQgdGhhdCBtZXRob2RzIGxpa2VgaW5zZXJ0YCxgcmVtb3ZlYCBvcmBzb3J0YCB0aGF0IG9ubHkgbW9kaWZ5IHRoZSBsaXN0IGhhdmUgbm8gcmV0dXJuIHZhbHVlIHByaW50ZWQgXFx1MjAxMyB0aGV5IHJldHVybiB0aGUgZGVmYXVsdGBOb25lYC4gWzFdIFRoaXMgaXMgYSBkZXNpZ24gcHJpbmNpcGxlIGZvciBhbGwgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgaW4gUHl0aG9uLlxcbi4uLlxcbiMjIDUuMy4gVHVwbGVzIGFuZCBTZXF1ZW5jZXNcXHUwMGI2XFxuXFxuV2Ugc2F3IHRoYXQgbGlzdHMgYW5kIHN0cmluZ3MgaGF2ZSBtYW55IGNvbW1vbiBwcm9wZXJ0aWVzLCBzdWNoIGFzIGluZGV4aW5nIGFuZCBzbGljaW5nIG9wZXJhdGlvbnMuIFRoZXkgYXJlIHR3byBleGFtcGxlcyBvZiBzZXF1ZW5jZSBkYXRhIHR5cGVzIChzZWUgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBsaXN0LCB0dXBsZSwgcmFuZ2UpLiBTaW5jZSBQeXRob24gaXMgYW4gZXZvbHZpbmcgbGFuZ3VhZ2UsIG90aGVyIHNlcXVlbmNlIGRhdGEgdHlwZXMgbWF5IGJlIGFkZGVkLiBUaGVyZSBpcyBhbHNvIGFub3RoZXIgc3RhbmRhcmQgc2VxdWVuY2UgZGF0YSB0eXBlOiB0aGUgdHVwbGUuXFxuXFxuQSB0dXBsZSBjb25zaXN0cyBvZiBhIG51bWJlciBvZiB2YWx1ZXMgc2VwYXJhdGVkIGJ5IGNvbW1hcywgZm9yIGluc3RhbmNlOlxcbi4uLlxcbj4+PiAjIFR1cGxlcyBhcmUgaW1tdXRhYmxlOlxcbj4+PiB0WzBdID0gODg4ODhcXG5UcmFjZWJhY2sgKG1vc3QgcmVjZW50IGNhbGwgbGFzdCk6XFxuICBGaWxlIFxcXCI8c3RkaW4+XFxcIiwgbGluZSAxLCBpbiA8bW9kdWxlPlxcblR5cGVFcnJvcjogJ3R1cGxlJyBvYmplY3QgZG9lcyBub3Qgc3VwcG9ydCBpdGVtIGFzc2lnbm1lbnRcXG4+Pj4gIyBidXQgdGhleSBjYW4gY29udGFpbiBtdXRhYmxlIG9iamVjdHM6XFxuPj4+IHYgPSAoWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuPj4+IHZcXG4oWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuLi4uXFxuQXMgeW91IHNlZSwgb24gb3V0cHV0IHR1cGxlcyBhcmUgYWx3YXlzIGVuY2xvc2VkIGluIHBhcmVudGhlc2VzLCBzbyB0aGF0IG5lc3RlZCB0dXBsZXMgYXJlIGludGVycHJldGVkIGNvcnJlY3RseTsgdGhleSBtYXkgYmUgaW5wdXQgd2l0aCBvciB3aXRob3V0IHN1cnJvdW5kaW5nIHBhcmVudGhlc2VzLCBhbHRob3VnaCBvZnRlbiBwYXJlbnRoZXNlcyBhcmUgbmVjZXNzYXJ5IGFueXdheSAoaWYgdGhlIHR1cGxlIGlzIHBhcnQgb2YgYSBsYXJnZXIgZXhwcmVzc2lvbikuIEl0IGlzIG5vdCBwb3NzaWJsZSB0byBhc3NpZ24gdG8gdGhlIGluZGl2aWR1YWwgaXRlbXMgb2YgYSB0dXBsZSwgaG93ZXZlciBpdCBpcyBwb3NzaWJsZSB0byBjcmVhdGUgdHVwbGVzIHdoaWNoIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzLCBzdWNoIGFzIGxpc3RzLlxcblxcblRob3VnaCB0dXBsZXMgbWF5IHNlZW0gc2ltaWxhciB0byBsaXN0cywgdGhleSBhcmUgb2Z0ZW4gdXNlZCBpbiBkaWZmZXJlbnQgc2l0dWF0aW9ucyBhbmQgZm9yIGRpZmZlcmVudCBwdXJwb3Nlcy4gVHVwbGVzIGFyZSBpbW11dGFibGUsIGFuZCB1c3VhbGx5IGNvbnRhaW4gYSBoZXRlcm9nZW5lb3VzIHNlcXVlbmNlIG9mIGVsZW1lbnRzIHRoYXQgYXJlIGFjY2Vzc2VkIHZpYSB1bnBhY2tpbmcgKHNlZSBsYXRlciBpbiB0aGlzIHNlY3Rpb24pIG9yIGluZGV4aW5nIChvciBldmVuIGJ5IGF0dHJpYnV0ZSBpbiB0aGUgY2FzZSBvZiBuYW1lZHR1cGxlcykuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG5Bbm90aGVyIHVzZWZ1bCBkYXRhIHR5cGUgYnVpbHQgaW50byBQeXRob24gaXMgdGhlIGRpY3Rpb25hcnkgKHNlZSBNYXBwaW5nIFR5cGVzIFxcdTIwMTQgZGljdCkuIERpY3Rpb25hcmllcyBhcmUgc29tZXRpbWVzIGZvdW5kIGluIG90aGVyIGxhbmd1YWdlcyBhcyBcXHUyMDFjYXNzb2NpYXRpdmUgbWVtb3JpZXNcXHUyMDFkIG9yIFxcdTIwMWNhc3NvY2lhdGl2ZSBhcnJheXNcXHUyMDFkLiBVbmxpa2Ugc2VxdWVuY2VzLCB3aGljaCBhcmUgaW5kZXhlZCBieSBhIHJhbmdlIG9mIG51bWJlcnMsIGRpY3Rpb25hcmllcyBhcmUgaW5kZXhlZCBieSBrZXlzLCB3aGljaCBjYW4gYmUgYW55IGltbXV0YWJsZSB0eXBlOyBzdHJpbmdzIGFuZCBudW1iZXJzIGNhbiBhbHdheXMgYmUga2V5cy4gVHVwbGVzIGNhbiBiZSB1c2VkIGFzIGtleXMgaWYgdGhleSBjb250YWluIG9ubHkgc3RyaW5ncywgbnVtYmVycywgb3IgdHVwbGVzOyBpZiBhIHR1cGxlIGNvbnRhaW5zIGFueSBtdXRhYmxlIG9iamVjdCBlaXRoZXIgZGlyZWN0bHkgb3IgaW5kaXJlY3RseSwgaXQgY2Fubm90IGJlIHVzZWQgYXMgYSBrZXkuIFlvdSBjYW5cXHUyMDE5dCB1c2UgbGlzdHMgYXMga2V5cywgc2luY2UgbGlzdHMgY2FuIGJlIG1vZGlmaWVkIGluIHBsYWNlIHVzaW5nIGluZGV4IGFzc2lnbm1lbnRzLCBzbGljZSBhc3NpZ25tZW50cywgb3IgbWV0aG9kcyBsaWtlIGFwcGVuZCgpIGFuZCBleHRlbmQoKS5cIl19LCB7XCJ0aXRsZVwiOiBcIjMuIERhdGEgbW9kZWwgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3JlZmVyZW5jZS9kYXRhbW9kZWwuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiVGhlIHZhbHVlIG9mIHNvbWUgb2JqZWN0cyBjYW4gY2hhbmdlLiBPYmplY3RzIHdob3NlIHZhbHVlIGNhbiBjaGFuZ2UgYXJlIHNhaWQgdG8gYmUgbXV0YWJsZTsgb2JqZWN0cyB3aG9zZSB2YWx1ZSBpcyB1bmNoYW5nZWFibGUgb25jZSB0aGV5IGFyZSBjcmVhdGVkIGFyZSBjYWxsZWQgaW1tdXRhYmxlLiAoVGhlIHZhbHVlIG9mIGFuIGltbXV0YWJsZSBjb250YWluZXIgb2JqZWN0IHRoYXQgY29udGFpbnMgYSByZWZlcmVuY2UgdG8gYSBtdXRhYmxlIG9iamVjdCBjYW4gY2hhbmdlIHdoZW4gdGhlIGxhdHRlclxcdTIwMTlzIHZhbHVlIGlzIGNoYW5nZWQ7IGhvd2V2ZXIgdGhlIGNvbnRhaW5lciBpcyBzdGlsbCBjb25zaWRlcmVkIGltbXV0YWJsZSwgYmVjYXVzZSB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGl0IGNvbnRhaW5zIGNhbm5vdCBiZSBjaGFuZ2VkLiBTbywgaW1tdXRhYmlsaXR5IGlzIG5vdCBzdHJpY3RseSB0aGUgc2FtZSBhcyBoYXZpbmcgYW4gdW5jaGFuZ2VhYmxlIHZhbHVlLCBpdCBpcyBtb3JlIHN1YnRsZS4pIEFuIG9iamVjdFxcdTIwMTlzIG11dGFiaWxpdHkgaXMgZGV0ZXJtaW5lZCBieSBpdHMgdHlwZTsgZm9yIGluc3RhbmNlLCBudW1iZXJzLCBzdHJpbmdzIGFuZCB0dXBsZXMgYXJlIGltbXV0YWJsZSwgd2hpbGUgZGljdGlvbmFyaWVzIGFuZCBsaXN0cyBhcmUgbXV0YWJsZS5cXG4uLi5cXG5Tb21lIG9iamVjdHMgY29udGFpbiByZWZlcmVuY2VzIHRvIG90aGVyIG9iamVjdHM7IHRoZXNlIGFyZSBjYWxsZWQgY29udGFpbmVycy4gRXhhbXBsZXMgb2YgY29udGFpbmVycyBhcmUgdHVwbGVzLCBsaXN0cyBhbmQgZGljdGlvbmFyaWVzLiBUaGUgcmVmZXJlbmNlcyBhcmUgcGFydCBvZiBhIGNvbnRhaW5lclxcdTIwMTlzIHZhbHVlLiBJbiBtb3N0IGNhc2VzLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIHZhbHVlIG9mIGEgY29udGFpbmVyLCB3ZSBpbXBseSB0aGUgdmFsdWVzLCBub3QgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGNvbnRhaW5lZCBvYmplY3RzOyBob3dldmVyLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIG11dGFiaWxpdHkgb2YgYSBjb250YWluZXIsIG9ubHkgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGltbWVkaWF0ZWx5IGNvbnRhaW5lZCBvYmplY3RzIGFyZSBpbXBsaWVkLiBTbywgaWYgYW4gaW1tdXRhYmxlIGNvbnRhaW5lciAobGlrZSBhIHR1cGxlKSBjb250YWlucyBhIHJlZmVyZW5jZSB0byBhIG11dGFibGUgb2JqZWN0LCBpdHMgdmFsdWUgY2hhbmdlcyBpZiB0aGF0IG11dGFibGUgb2JqZWN0IGlzIGNoYW5nZWQuXFxuLi4uXFxuIyMjIDMuMi41LiBTZXF1ZW5jZXNcXHUwMGI2XFxuLi4uXFxuIyMjIyAzLjIuNS4xLiBJbW11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbkFuIG9iamVjdCBvZiBhbiBpbW11dGFibGUgc2VxdWVuY2UgdHlwZSBjYW5ub3QgY2hhbmdlIG9uY2UgaXQgaXMgY3JlYXRlZC4gKElmIHRoZSBvYmplY3QgY29udGFpbnMgcmVmZXJlbmNlcyB0byBvdGhlciBvYmplY3RzLCB0aGVzZSBvdGhlciBvYmplY3RzIG1heSBiZSBtdXRhYmxlIGFuZCBtYXkgYmUgY2hhbmdlZDsgaG93ZXZlciwgdGhlIGNvbGxlY3Rpb24gb2Ygb2JqZWN0cyBkaXJlY3RseSByZWZlcmVuY2VkIGJ5IGFuIGltbXV0YWJsZSBvYmplY3QgY2Fubm90IGNoYW5nZS4pXFxuXFxuVGhlIGZvbGxvd2luZyB0eXBlcyBhcmUgaW1tdXRhYmxlIHNlcXVlbmNlczpcXG4uLi5cXG5UdXBsZXNcXG46IFRoZSBpdGVtcyBvZiBhIGB0dXBsZWAgYXJlIGFyYml0cmFyeSBQeXRob24gb2JqZWN0cy4gVHVwbGVzIG9mIHR3byBvciBtb3JlIGl0ZW1zIGFyZSBmb3JtZWQgYnkgY29tbWEtc2VwYXJhdGVkIGxpc3RzIG9mIGV4cHJlc3Npb25zLiBBIHR1cGxlIG9mIG9uZSBpdGVtIChhIFxcdTIwMThzaW5nbGV0b25cXHUyMDE5KSBjYW4gYmUgZm9ybWVkIGJ5IGFmZml4aW5nIGEgY29tbWEgdG8gYW4gZXhwcmVzc2lvbiAoYW4gZXhwcmVzc2lvbiBieSBpdHNlbGYgZG9lcyBub3QgY3JlYXRlIGEgdHVwbGUsIHNpbmNlIHBhcmVudGhlc2VzIG11c3QgYmUgdXNhYmxlIGZvciBncm91cGluZyBvZiBleHByZXNzaW9ucykuIEFuIGVtcHR5IHR1cGxlIGNhbiBiZSBmb3JtZWQgYnkgYW4gZW1wdHkgcGFpciBvZiBwYXJlbnRoZXNlcy5cXG4uLi5cXG4jIyMjIDMuMi41LjIuIE11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbk11dGFibGUgc2VxdWVuY2VzIGNhbiBiZSBjaGFuZ2VkIGFmdGVyIHRoZXkgYXJlIGNyZWF0ZWQuIFRoZSBzdWJzY3JpcHRpb24gYW5kIHNsaWNpbmcgbm90YXRpb25zIGNhbiBiZSB1c2VkIGFzIHRoZSB0YXJnZXQgb2YgYXNzaWdubWVudCBhbmQgYGRlbGAgKGRlbGV0ZSkgc3RhdGVtZW50cy5cXG4uLi5cXG5UaGVyZSBhcmUgY3VycmVudGx5IHR3byBpbnRyaW5zaWMgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlczpcXG5cXG5MaXN0c1xcbjogVGhlIGl0ZW1zIG9mIGEgbGlzdCBhcmUgYXJiaXRyYXJ5IFB5dGhvbiBvYmplY3RzLiBMaXN0cyBhcmUgZm9ybWVkIGJ5IHBsYWNpbmcgYSBjb21tYS1zZXBhcmF0ZWQgbGlzdCBvZiBleHByZXNzaW9ucyBpbiBzcXVhcmUgYnJhY2tldHMuIChOb3RlIHRoYXQgdGhlcmUgYXJlIG5vIHNwZWNpYWwgY2FzZXMgbmVlZGVkIHRvIGZvcm0gbGlzdHMgb2YgbGVuZ3RoIDAgb3IgMS4pXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjcgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvYnVpbHRpbnMvc3RkdHlwZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiY29sbGVjdGlvbiBjbGFzc2VzIGFyZSBtdXRhYmxlLiBUaGUgbWV0aG9kcyB0aGF0IGFkZCwgc3VidHJhY3QsIG9yIHJlYXJyYW5nZSB0aGVpciAuLi4gaW4gcGxhY2UsIGFuZCBkb25cXHUyMDE5dCByZXR1cm4gYSAuLi4gLCBuZXZlciByZXR1cm4gdGhlIGNvbGxlY3Rpb24gaW5zdGFuY2UgaXRzZWxmIGJ1dCBgTm9uZWAuXFxuLi4uXFxuIyMgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBgbGlzdGAsIGB0dXBsZWAsIGByYW5nZWBcXHUwMGI2XFxuXFxuVGhlcmUgYXJlIHRocmVlIGJhc2ljIHNlcXVlbmNlIHR5cGVzOiBsaXN0cywgdHVwbGVzLCBhbmQgcmFuZ2Ugb2JqZWN0cy4gQWRkaXRpb25hbCBzZXF1ZW5jZSB0eXBlcyB0YWlsb3JlZCBmb3IgcHJvY2Vzc2luZyBvZiBiaW5hcnkgZGF0YSBhbmQgdGV4dCBzdHJpbmdzIGFyZSBkZXNjcmliZWQgaW4gZGVkaWNhdGVkIHNlY3Rpb25zLlxcbi4uLlxcblRoZSBvcGVyYXRpb25zIGluIHRoZSBmb2xsb3dpbmcgdGFibGUgLi4uIG11dGFibGUgYW5kIGltbXV0YWJsZS4gVGhlIGBjb2xsZWN0aW9ucy4gLi4uIGlzIHByb3ZpZGVkIHRvIG1ha2UgLi4uIGVhc2llciB0byBjb3JyZWN0bHkgaW1wbGVtZW50IHRoZXNlIG9wZXJhdGlvbnMgb24gY3VzdG9tIHNlcXVlbmNlIHR5cGVzXFxuLi4uXFxuIyMjIEltbXV0YWJsZSBTZXF1ZW5jZSBUeXBlc1xcdTAwYjZcXG4uLi5cXG5tdXRhYmxlIHNlcXVlbmNlIHR5cGVzIGlzXFxuLi4uXFxuc3VwcG9ydCBhbGxvd3MgaW1tdXRhYmxlIHNlcXVlbmNlcywgLi4uICwgdG8gYmUgdXNlZCBhcyBgZGljdGAga2V5cyBhbmQgc3RvcmVkIGluIC4uLiBlbnNldGAgaW5zdGFuY2VzXFxuLi4uXFxuIyMjIE11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxuIyMjIExpc3RzXFx1MDBiNlxcblxcbkxpc3RzIGFyZSBtdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaG9tb2dlbmVvdXMgaXRlbXMgKHdoZXJlIHRoZSBwcmVjaXNlIGRlZ3JlZSBvZiBzaW1pbGFyaXR5IHdpbGwgdmFyeSBieSBhcHBsaWNhdGlvbikuXFxuLi4uXFxuIyMjIFR1cGxlc1xcdTAwYjZcXG5cXG5UdXBsZXMgYXJlIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhldGVyb2dlbmVvdXMgZGF0YSAoc3VjaCBhcyB0aGUgMi10dXBsZXMgcHJvZHVjZWQgYnkgdGhlIGBlbnVtZXJhdGUoKWAgYnVpbHQtaW4pLiBUdXBsZXMgYXJlIGFsc28gdXNlZCBmb3IgY2FzZXMgd2hlcmUgYW4gaW1tdXRhYmxlIHNlcXVlbmNlIG9mIGhvbW9nZW5lb3VzIGRhdGEgaXMgbmVlZGVkIChzdWNoIGFzIGFsbG93aW5nIHN0b3JhZ2UgaW4gYSBgc2V0YCBvciBgZGljdGAgaW5zdGFuY2UpLlxcbi4uLlxcbnR1cGxlKGl0ZXJhYmxlPVxcbi4uLlxcblRoZSBjb25zdHJ1Y3RvciBidWlsZHMgYSB0dXBsZSB3aG9zZSBpdGVtcyBhcmUgdGhlIHNhbWUgYW5kIGluIHRoZSBzYW1lIG9yZGVyIGFzIGl0ZXJhYmxlXFx1MjAxOXMgaXRlbXMuIGl0ZXJhYmxlIG1heSBiZSBlaXRoZXIgYSBzZXF1ZW5jZSwgYSBjb250YWluZXIgdGhhdCBzdXBwb3J0cyBpdGVyYXRpb24sIG9yIGFuIGl0ZXJhdG9yIG9iamVjdC4gSWYgaXRlcmFibGUgaXMgYWxyZWFkeSBhIHR1cGxlLCBpdCBpcyByZXR1cm5lZCB1bmNoYW5nZWQuIEZvciBleGFtcGxlLCBgdHVwbGUoJ2FiYycpYCByZXR1cm5zIGAoJ2EnLCAnYicsICdjJylgIGFuZCBgdHVwbGUoIFsxLCAyLCAzXSApYCByZXR1cm5zIGAoMSwgMiwgMylgLiBJZiBubyBhcmd1bWVudCBpcyBnaXZlbiwgdGhlIGNvbnN0cnVjdG9yIGNyZWF0ZXMgYSBuZXcgZW1wdHkgdHVwbGUsIGAoKWAuXFxuLi4uXFxuVHVwbGVzIGltcGxlbWVudCBhbGwgb2YgdGhlIGNvbW1vbiBzZXF1ZW5jZSBvcGVyYXRpb25zLlxcbi4uLlxcbkZvciBoZXRlcm9nZW5lb3VzIGNvbGxlY3Rpb25zIG9mIGRhdGEgd2hlcmUgYWNjZXNzIGJ5IG5hbWUgaXMgY2xlYXJlciB0aGFuIGFjY2VzcyBieSBpbmRleCwgYGNvbGxlY3Rpb25zLm5hbWVkdHVwbGUoKWAgbWF5IGJlIGEgbW9yZSBhcHByb3ByaWF0ZSBjaG9pY2UgdGhhbiBhIHNpbXBsZSB0dXBsZSBvYmplY3QuXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvbGlicmFyeS9zdGR0eXBlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJTb21lIGNvbGxlY3Rpb24gY2xhc3NlcyBhcmUgbXV0YWJsZS4gVGhlIG1ldGhvZHMgdGhhdCBhZGQsIHN1YnRyYWN0LCBvciByZWFycmFuZ2UgdGhlaXIgLi4uIGluIHBsYWNlLCBhbmQgZG9uXFx1MjAxOXQgcmV0dXJuIGEgLi4uICwgbmV2ZXIgcmV0dXJuIHRoZSBjb2xsZWN0aW9uIGluc3RhbmNlIGl0c2VsZiBidXQgYE5vbmVgLlxcbi4uLlxcbiMjIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgYGxpc3RgLCBgdHVwbGVgLCBgcmFuZ2VgXFx1MDBiNlxcblxcblRoZXJlIGFyZSB0aHJlZSBiYXNpYyBzZXF1ZW5jZSB0eXBlczogbGlzdHMsIHR1cGxlcywgYW5kIC4uLiBvYmplY3RzLiBBZGRpdGlvbmFsIHNlcXVlbmNlIHR5cGVzIHRhaWxvcmVkIGZvciBwcm9jZXNzaW5nIG9mIGJpbmFyeSBkYXRhIGFuZCB0ZXh0IHN0cmluZ3MgYXJlIGRlc2NyaWJlZCBpbiBkZWRpY2F0ZWQgc2VjdGlvbnMuXFxuLi4uXFxuVGhlIG9wZXJhdGlvbnMgaW4gdGhlIGZvbGxvd2luZyB0YWJsZSAuLi4gaW1tdXRhYmxlLiBUaGUgYCAuLi4gaXMgcHJvdmlkZWQgdG8gbWFrZSAuLi4gZWFzaWVyIHRvIGNvcnJlY3RseSBpbXBsZW1lbnQgdGhlc2Ugb3BlcmF0aW9ucyBvbiBjdXN0b20gc2VxdWVuY2UgdHlwZXNcXG4uLi5cXG4jIyMgSW1tdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbm9wZXJhdGlvbiB0aGF0IGltbXV0YWJsZSBzZXF1ZW5jZSAuLi4gYnkgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlcyBpcyAuLi4gYGhhc2goKWBcXG4uLi5cXG5UaGlzIHN1cHBvcnQgYWxsb3dzIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHN1Y2ggYXMgYHR1cGxlYCBpbnN0YW5jZXMsIHRvIGJlIHVzZWQgYXMgYGRpY3RgIGtleXMgYW5kIHN0b3JlZCBpbiBgc2V0YCBhbmQgYGZyb3plbnNldGAgaW5zdGFuY2VzLlxcbi4uLlxcbiMjIyBNdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbiMjIyBMaXN0c1xcdTAwYjZcXG5cXG5MaXN0cyBhcmUgbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhvbW9nZW5lb3VzIGl0ZW1zICh3aGVyZSB0aGUgcHJlY2lzZSBkZWdyZWUgb2Ygc2ltaWxhcml0eSB3aWxsIHZhcnkgYnkgYXBwbGljYXRpb24pLlxcbi4uLlxcbiMjIyBUdXBsZXNcXHUwMGI2XFxuXFxuVHVwbGVzIGFyZSBpbW11dGFibGUgc2VxdWVuY2VzLCB0eXBpY2FsbHkgdXNlZCB0byBzdG9yZSBjb2xsZWN0aW9ucyBvZiBoZXRlcm9nZW5lb3VzIGRhdGEgKHN1Y2ggYXMgdGhlIDItdHVwbGVzIHByb2R1Y2VkIGJ5IHRoZSBgZW51bWVyYXRlKClgIGJ1aWx0LWluKS4gVHVwbGVzIGFyZSBhbHNvIHVzZWQgZm9yIGNhc2VzIHdoZXJlIGFuIGltbXV0YWJsZSBzZXF1ZW5jZSBvZiBob21vZ2VuZW91cyBkYXRhIGlzIG5lZWRlZCAoc3VjaCBhcyBhbGxvd2luZyBzdG9yYWdlIGluIGEgYHNldGAgb3IgYGRpY3RgIGluc3RhbmNlKS5cXG4uLi5cXG5UaGUgY29uc3RydWN0b3IgYnVpbGRzIGEgdHVwbGUgd2hvc2UgaXRlbXMgYXJlIHRoZSBzYW1lIGFuZCBpbiB0aGUgc2FtZSBvcmRlciBhcyBpdGVyYWJsZVxcdTIwMTlzIGl0ZW1zLiBpdGVyYWJsZSBtYXkgYmUgZWl0aGVyIGEgc2VxdWVuY2UsIGEgY29udGFpbmVyIHRoYXQgc3VwcG9ydHMgaXRlcmF0aW9uLCBvciBhbiBpdGVyYXRvciBvYmplY3QuIElmIGl0ZXJhYmxlIGlzIGFscmVhZHkgYSB0dXBsZSwgaXQgaXMgcmV0dXJuZWQgdW5jaGFuZ2VkLiBGb3IgZXhhbXBsZSwgYHR1cGxlKCdhYmMnKWAgcmV0dXJucyBgKCdhJywgJ2InLCAnYycpYCBhbmQgYHR1cGxlKCBbMSwgMiwgM10gKWAgcmV0dXJucyBgKDEsIDIsIDMpYC4gSWYgbm8gYXJndW1lbnQgaXMgZ2l2ZW4sIHRoZSBjb25zdHJ1Y3RvciBjcmVhdGVzIGEgbmV3IGVtcHR5IHR1cGxlLCBgKClgLlxcbi4uLlxcblR1cGxlcyBpbXBsZW1lbnQgYWxsIG9mIHRoZSBjb21tb24gc2VxdWVuY2Ugb3BlcmF0aW9ucy5cXG4uLi5cXG5Gb3IgaGV0ZXJvZ2VuZW91cyBjb2xsZWN0aW9ucyBvZiBkYXRhIHdoZXJlIGFjY2VzcyBieSBuYW1lIGlzIGNsZWFyZXIgdGhhbiBhY2Nlc3MgYnkgaW5kZXgsIGBjb2xsZWN0aW9ucy5uYW1lZHR1cGxlKClgIG1heSBiZSBhIG1vcmUgYXBwcm9wcmlhdGUgY2hvaWNlIHRoYW4gYSBzaW1wbGUgdHVwbGUgb2JqZWN0LlwiXX0sIHtcInRpdGxlXCI6IFwiNS4gRGF0YSBTdHJ1Y3R1cmVzIFxcdTIwMTQgUHl0aG9uIDMuMTAuMjAgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMuMTAvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiWW91IG1pZ2h0IGhhdmUgbm90aWNlZCB0aGF0IG1ldGhvZHMgbGlrZSBgaW5zZXJ0YCwgYHJlbW92ZWAgb3IgYHNvcnRgIHRoYXQgb25seSBtb2RpZnkgdGhlIGxpc3QgaGF2ZSBubyByZXR1cm4gdmFsdWUgcHJpbnRlZCBcXHUyMDEzIHRoZXkgcmV0dXJuIHRoZSBkZWZhdWx0IGBOb25lYC4gMSBUaGlzIGlzIGEgZGVzaWduIHByaW5jaXBsZSBmb3IgYWxsIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIGluIFB5dGhvbi5cXG4uLi5cXG4jIyA1LjMuIFR1cGxlcyBhbmQgU2VxdWVuY2VzXFx1MDBiNlxcblxcbldlIHNhdyB0aGF0IGxpc3RzIGFuZCBzdHJpbmdzIGhhdmUgbWFueSBjb21tb24gcHJvcGVydGllcywgc3VjaCBhcyBpbmRleGluZyBhbmQgc2xpY2luZyBvcGVyYXRpb25zLiBUaGV5IGFyZSB0d28gZXhhbXBsZXMgb2Ygc2VxdWVuY2UgZGF0YSB0eXBlcyAoc2VlIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgbGlzdCwgdHVwbGUsIHJhbmdlKS4gU2luY2UgUHl0aG9uIGlzIGFuIGV2b2x2aW5nIGxhbmd1YWdlLCBvdGhlciBzZXF1ZW5jZSBkYXRhIHR5cGVzIG1heSBiZSBhZGRlZC4gVGhlcmUgaXMgYWxzbyBhbm90aGVyIHN0YW5kYXJkIHNlcXVlbmNlIGRhdGEgdHlwZTogdGhlIHR1cGxlLlxcblxcbkEgdHVwbGUgY29uc2lzdHMgb2YgYSBudW1iZXIgb2YgdmFsdWVzIHNlcGFyYXRlZCBieSBjb21tYXMsIGZvciBpbnN0YW5jZTpcXG4uLi5cXG4+Pj4gIyBUdXBsZXMgYXJlIGltbXV0YWJsZTpcXG4uLi4gdFswXSA9IDg4ODg4XFxuVHJhY2ViYWNrIChtb3N0IHJlY2VudCBjYWxsIGxhc3QpOlxcbiAgRmlsZSBcXFwiPHN0ZGluPlxcXCIsIGxpbmUgMSwgaW4gPG1vZHVsZT5cXG5UeXBlRXJyb3I6ICd0dXBsZScgb2JqZWN0IGRvZXMgbm90IHN1cHBvcnQgaXRlbSBhc3NpZ25tZW50XFxuPj4+ICMgYnV0IHRoZXkgY2FuIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzOlxcbi4uLiB2ID0gKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbj4+PiB2XFxuKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbi4uLlxcbkFzIHlvdSBzZWUsIG9uIG91dHB1dCB0dXBsZXMgYXJlIGFsd2F5cyBlbmNsb3NlZCBpbiBwYXJlbnRoZXNlcywgc28gdGhhdCBuZXN0ZWQgdHVwbGVzIGFyZSBpbnRlcnByZXRlZCBjb3JyZWN0bHk7IHRoZXkgbWF5IGJlIGlucHV0IHdpdGggb3Igd2l0aG91dCBzdXJyb3VuZGluZyBwYXJlbnRoZXNlcywgYWx0aG91Z2ggb2Z0ZW4gcGFyZW50aGVzZXMgYXJlIG5lY2Vzc2FyeSBhbnl3YXkgKGlmIHRoZSB0dXBsZSBpcyBwYXJ0IG9mIGEgbGFyZ2VyIGV4cHJlc3Npb24pLiBJdCBpcyBub3QgcG9zc2libGUgdG8gYXNzaWduIHRvIHRoZSBpbmRpdmlkdWFsIGl0ZW1zIG9mIGEgdHVwbGUsIGhvd2V2ZXIgaXQgaXMgcG9zc2libGUgdG8gY3JlYXRlIHR1cGxlcyB3aGljaCBjb250YWluIG11dGFibGUgb2JqZWN0cywgc3VjaCBhcyBsaXN0cy5cXG5cXG5UaG91Z2ggdHVwbGVzIG1heSBzZWVtIHNpbWlsYXIgdG8gbGlzdHMsIHRoZXkgYXJlIG9mdGVuIHVzZWQgaW4gZGlmZmVyZW50IHNpdHVhdGlvbnMgYW5kIGZvciBkaWZmZXJlbnQgcHVycG9zZXMuIFR1cGxlcyBhcmUgaW1tdXRhYmxlLCBhbmQgdXN1YWxseSBjb250YWluIGEgaGV0ZXJvZ2VuZW91cyBzZXF1ZW5jZSBvZiBlbGVtZW50cyB0aGF0IGFyZSBhY2Nlc3NlZCB2aWEgdW5wYWNraW5nIChzZWUgbGF0ZXIgaW4gdGhpcyBzZWN0aW9uKSBvciBpbmRleGluZyAob3IgZXZlbiBieSBhdHRyaWJ1dGUgaW4gdGhlIGNhc2Ugb2YgYG5hbWVkdHVwbGVzYCkuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG4jIyA1LjQuXFxuLi4uXFxuLiBTZXQgb2JqZWN0c1xcbi4uLlxcbiMjIDUuNS4gRGljdGlvbmFyaWVzXFx1MDBiNlxcblxcbkFub3RoZXIgdXNlZnVsIGRhdGEgdHlwZSBidWlsdCBpbnRvIFB5dGhvbiBpcyB0aGUgZGljdGlvbmFyeSAoc2VlIE1hcHBpbmcgVHlwZXMgXFx1MjAxNCBkaWN0KS4gRGljdGlvbmFyaWVzIGFyZSBzb21ldGltZXMgZm91bmQgaW4gb3RoZXIgbGFuZ3VhZ2VzIGFzIFxcdTIwMWNhc3NvY2lhdGl2ZSBtZW1vcmllc1xcdTIwMWQgb3IgXFx1MjAxY2Fzc29jaWF0aXZlIGFycmF5c1xcdTIwMWQuIFVubGlrZSBzZXF1ZW5jZXMsIHdoaWNoIGFyZSBpbmRleGVkIGJ5IGEgcmFuZ2Ugb2YgbnVtYmVycywgZGljdGlvbmFyaWVzIGFyZSBpbmRleGVkIGJ5IGtleXMsIHdoaWNoIGNhbiBiZSBhbnkgaW1tdXRhYmxlIHR5cGU7IHN0cmluZ3MgYW5kIG51bWJlcnMgY2FuIGFsd2F5cyBiZSBrZXlzLiBUdXBsZXMgY2FuIGJlIHVzZWQgYXMga2V5cyBpZiB0aGV5IGNvbnRhaW4gb25seSBzdHJpbmdzLCBudW1iZXJzLCBvciB0dXBsZXM7IGlmIGEgdHVwbGUgY29udGFpbnMgYW55IG11dGFibGUgb2JqZWN0IGVpdGhlciBkaXJlY3RseSBvciBpbmRpcmVjdGx5LCBpdCBjYW5ub3QgYmUgdXNlZCBhcyBhIGtleS4gWW91IGNhblxcdTIwMTl0IHVzZSBsaXN0cyBhcyBrZXlzLCBzaW5jZSBsaXN0cyBjYW4gYmUgbW9kaWZpZWQgaW4gcGxhY2UgdXNpbmcgaW5kZXggYXNzaWdubWVudHMsIHNsaWNlIGFzc2lnbm1lbnRzLCBvciBtZXRob2RzIGxpa2UgYGFwcGVuZCgpYCBhbmQgYGV4dGVuZCgpYC5cIl19XSIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ3ZWJfc2VhcmNoIiwiaWQiOiI2ZTUxMjVhYy0zMTY0LTRhNzYtOWM1Mi1hYzM0NjBjZTM2ZTQiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJzdGF0dXMiOiJzdWNjZXNzIn0seyJjb250ZW50IjpbeyJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3NrZXB0aWMiLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QTZGNGNDTjV5aEQ0Q3JzOU0iLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjB9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InJlc2VhcmNoZXIiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtM2NkOC03OWYxLTg4YTEtY2U2Y2VlMjk0ZTc5LTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoidHJhbnNmZXJfdG9fc2tlcHRpYyIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsInRvdGFsX3Rva2VucyI6NzQxMSwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fSx7ImNvbnRlbnQiOiJTdWNjZXNzZnVsbHkgdHJhbnNmZXJyZWQgdG8gc2tlcHRpYyIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ0cmFuc2Zlcl90b19za2VwdGljIiwiaWQiOiIzODkwNThkMS0wODNmLTQ2MzItYmJjNS0wOWJmZDNhMGZmZDIiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMVhqajNhUmRXeWhCSmZQbVRKQnhRYTkiLCJzdGF0dXMiOiJzdWNjZXNzIn0seyJjb250ZW50IjpbeyJzaWduYXR1cmUiOiJDQVFTMVFnS0VBZ1NHQUk0QVVJSWRHaHBibXRwYm1jU0RONlRmVTZORE05NzFsRWJ6eG9NdlJVQTFKOW94M2ZKUVlUOUlqQXJDUVFHSEJQNHBSSFBhdlh6UmxiTWJhWUhDNTdZaitHYnhncjBoZWx2ZDJPSUwvbzZSYnQ0UEZBM0ZhSUZVbGdxOGdjN0tRSTlGdEdudlNBa0xCUnpJc1VwLzJkNVJkMnpnRXNma0ZWZVhzbjhXWVlaa2luQVFWVnk2QTdCM0xhRUpXMHhiL3g4OERpNjdmZWR3eDBRK0ZsWnR3Y21UM0RjYWExMUp2ZXlIUnIzZG0rekdTWlA2Q1hlWE5KOG5yVFo3VzFZUlR5c0Q2dzdvN0xwOGgxUndxNGY0bzhqK1FVZlkwdmplK3pKWnN4SFBtUU50M3lrZUtoOTloZ242ZGJpejYvRUhxbWJIalNBdTFNNFNxRURmcUlLVGhIWWVuYW1BWm1jUTRYQU9vNm1kUTYrNzdWZ3BWMWxDMS9rNTJJaVVvSmprR2xRcm5MQ3Z1dGlJN0tOK3NnSzhjMisyS0NKclBSQ3ZWVitCa1N0dlA1aldXcXp5dHBLUStuSHpoTDQ0OE9ySUE5Z2RKQzM5QzhlRHJYZVJuSTd0NEZxbGtYR29NbFpBVjlVTWNyUUhSeE9NaTh0T3lUemk5b3lCRkRUU3NRK0xKcWVyMHR6MUZ0OWdlazZ2emc1MFNwVkFGUWRMRVJ2OFIxWXFNYWVJK3hzdlRBRVc5TVJndlJNa3pMdXpSVDJndXczakQrRmppcDM3eGdLNVFJMFIrQ3NPQW1RUG9UWml6emNJeUFVQXVDSkZDZ3EvZ0QvQ2liTHhveW1pVlVJMWFuOVNudDNLcE5Sb1dkdldJbmUzM1YwcHY4UEhnOXlXMkp1TE9xUUdpbExDc0svUTNvUXIzdXBFTVh4WEdqQ0w3V1VpenZSWkdGRUVWK1ZwbFl0N3Y0dEtaTWo1cGszdmRlTTBNSjVYWmc3UHZVc1J6eWN4Y25yVDNLbm9jM3FNK3R0cVpmVHNiOGQ2VS9wUWlmcTdHdWFoRk9IcnZyb3FnSkxqKy84MVl5MXBaRmtqZkpnWTBYa2x6QW5BNkNGcS9ZVDBneGJWK1RINUlTK1dhWU9VekszNlM4czVxcU13aG5ndjRJekUzY3RYWHFuaElaWE56bnlWQnFiWWEvR3gyYkNqY0VqMm41UnlHWVJTLytNY3VsZ0xCem5ROU1rRzJEN1FQM01xL253SFhLTUlhTFM4Vks4aEczZDByQWpsdm5hdVhTaE5ySnh0a2o0cWt3bnhENExyRWprMXVlK1BMY2JMcVlXd0FyOG4wcys0MWpyc0lQVnl5Szl2RmFFWnJBOUthQnhrSnlpcXg1MEdaTm43Y2t4K21rSTE4Z0g1Mml5VlB0VThGS01YanVnQ2w2bVBMaUhZaURRZCtHNjNMdWRtT0t5czlpdFZuQkRCTk1QMmIvSTcrYklWUmxoZUdhWldyNk1BVEtsODZCM3krMGt1T3JMUk15V2NkaWJjMmlRa1ptKzhmN2RUVXdiS2JCSnNLT0ZZNzMvOG5sTC9KaVlwano2blVJaytJcVNqYUZ5SzZYRmxDY3VrV3BXSHBESm5TdmJFemV5N3FIeUNxSitJK29uOGlQOEdPdGVtcFFhUzAvNGtaR1N3bHJFNnhPajZFd0dwZmpiRFpLSGtQLzJSQWROWVBRcTN6N2JkVkpNSk5uRk16NmUzN216TXBDQmlod1BkU0ZUUkUzU0NZNjZ3dlRnSnpHQ1V5aEhiU2k1aTFhenpPWGQ4dUhDU3BULzZMMW82VlUwcGpvUjhLYWE0d0NXSEdmN1AvV0VkK2F2eXAwMkpudTFjbXBXMCtHcSt2SzloMHRkYjJJQ2xNL2VOZmRvdVFQK2VwOE1KVWpyYURSSUNtQUpRWTVvYlM3MXZuaU9vK3JlYUZ5MHpSZ0IiLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsic2lnbmF0dXJlIjoiQ0FRUy9SRUtFUWdTR0FJNEFVSUpibUZ5Y21GMGFXOXVFZ3dGaDNBVkpaUDNzOHkxSHEwYURHNzZ1aEhPTmFTY1hBR1NoaUl3R2dqTFhIVXgxeVpya0xQbGpqelRPUXljcExKaUtWd1hjeG0vRXpLSm0xczhNNVd4dVJFV0VsZFhjb2FOMC9tc0twa1JNVCtlTGI1aWg5ckFIVUxUb21VTnNVd0pldTBqckplNEdMYXp5N3NQNUt0akVPRUJLUXNYVWZvUTdmRGtaT1FVSFppOXp6UU40aEZObVlKVWYzT1ZxZjlUUU5kQzErbS8vSEFaUGZXbUF5Nkh0RHZtN2tnQkprQ0VvaUZubTd0bi9QWDFoWEdQUmdXYzZCVFhVTXIyV0E4YUt5cWdnUlViTUgyK0pZdjVpV1pkZENjMXVwMU5icjNzRjdXUGNvdDlaQmh5SFFYcjY5MnplSms2U0pneWdJVnJBU2JNbHkyYkJ4VFdKQjYyM2dWZ3pwQ21WU0xaRnBrS1hnUGNwOGhHeURLT3ZGNlNpUVNIcUdSeFFvZnI3VnMxYVBoZlppeFJPSkdEK01scmtnZ1ZBbkdNeVlJRFVacGF2dENlaVUxSm1CU3owMDg0YlhQdFQvdG5wc0tPblNQWVpRbzNCMEJYS2JxQS9Xc0tuZzgvVjNaOFV1YWpvNklWWjJoZ0ExV0RkdXFRT1FrRXhHVTZwRnVMaGViSWp1V3hTTkkvRGZmdlIvL0xOcUNmdmRuZjBodm9QdER4ZngvU2F3SXg0L3NwcTl4YndGekh0TGdvSGcxRjJ3Rnc3Y2xQRGxnMVB5RC92MnlKRzNWOVQ4R0w2akdQRTZyaGZpbC91NEhOdmY3QlcwZFF6Q1RUWGNxWnk0bkJ5eTArcCtTN1p4Zk1sNEtweGEyZ09oaFNOamZwZnVYMFFwTDRrTUU4SjFubGlGTjFkOEtxRVB1REkxOEJRSFVoRC9VZlkyOW9BMHg1MTBGak0vQnNMWjRuNXNwcGtvSWk3Q0ZvUXEyYUFLVk5mQzFLaWxoSXBSRCsxZHlEdFpRa2xOOGdacE9FU0h3Nk1MMVR0VTB5ZkFjYm1xb3hKNmhoanp3SDZDTXNUUUVFeThNYlJCTStRS0trSVJkUkNQSEdGVzJMa3VSNTArSjBwRi90cjZwcVZ2SmVvemZpTW5yRWlsQjI3VFpQNG5LVEpHOHJkLy92Q1dlbjB5N0NBLzhkQko5Z2RwNXR4N05WQTQ1azFnZGN4RVIxZ1lvdU5YM24xdkczL0IxRjQrOTN3WFc5TXFvNlVzNTlUUW1yYjlOcWE3cFNRTlNySDA2dzFHeHlCQWxrRmZxQWxhZmsxNmFNcHE0MTZUSllwR3k0M1dDVWlDY0xXbGhpV2d6MnNnT0xBWFA5YTV3ZVdBS1JDOEo2amtjVWtEZVhuVytIRlNpUE02KzlCbzZrVmhMVWhONXJjNG1IWk9acEY3cHRQcHNRTjRYQ3FMUHNxcFJILzhvNVhweUM2Wm5lVGVGMkV4dGpod1pWalhEd01iSXpWWUlidzF5MlBQR3c4ZEM3cGM0c0lWU25JNG9RQUgwdjY1MGdoZmhtTWV2V1c0aDRQeWxidy9ERzRmZGVUMys4ejBmOE1PU1R0ZmlweDA1L1F0dEhsQXZPRFJ4YTV5REN2T3o2bjNmMUJPME4vNE1iR3lPTTI3cVc0eDhtQlZvcnlVWldmc21oZmkwL0dmTnZCOCt5bFRHYllOWDI3T2NFRjVXWnROcWJXRXk3VjVHYm1aVVN4c3BjZzdGQlozajMvM0w3bXV2WlhvWmkxZjVlcnE0eTNsU0grb0Y3cVBGbnZpZXlMT0N3dWJjUXkrb3lvd2o0Y0QwaWRSZWNucCtzKzUyRDcrbXpGMWdwVytmK2NKYXNveGlnYnhrT2xvUzR5ZWlLdmdNNG5XU3BHRWoxMlJQeDVvODBFbXppWE45dDR6SnpXMWh0QTVoWEpKZUZ5bXBNWXR5Q1VDZXZSV1ZtclJZa3RRN1VuQ2E2dEwvQVZQaTFuaTVYQ1hIZkxPdXFmSzVLTFl4andWdGRoaGZtQ0x2cGFLVk5DRU9DZjRuTkdIcUQ5OHdVYjBlaldOd0RZQm5KR2tCUnc5UlZzcGZ3TmRvV29WY24ycXY4eXdHVXA3eWMvc2syTHBJNlZ6aVJ5dldxYkdORzFaSnFWVnV0UmM0NitzT0U0VlNlUzIvQ1ROS05hNk5NQ2YvaHh4YVVWc1lmS2RQK1MxTGM0aWFuS3Awc1V1Z1BwbXRveWRDMENQMlJ3am5ITGNuenhDbnZxeGxMUVdYTU5QVHN1MnlCY0hsV0pMdGNtZGcvTWNMYWd5N2w1a0c2c043WnhKTEQva0F4OEU2dXFvaFArSy9qTVNyWXVRSm5ocjR0ZjRiaVdGWTNKemYvS3BBWXlWU3ErcGJmRktYTWJMOW5IRW9aUVQxNVd5YWJkNkRNQXEzUVpPdGhNNXBVemROcVVYa2NySTFQd2pBU0FnNGRWVS8ybys5OG1UWG55aFA3VmxFa1RuZGRwd3lKRitZQ0NjMDFwcWE2Wlg5OXBuUitZT3BJcmI1RHRQY0txMHQ1Y0ZDRHlReE4xbFV1cVpjWXVqdlgvR0M3KzZJcFJUei9Oa0VZLzVjbnRPZlpKeWQ2U2tVeC9KUjVBSklSUENOZWliMHJjV25uRm4wWEs4czIyeDQwd0xqRXR1ZlhETzkreXJhZjRrSHhsQkkzU0IwbWVNamVwWDUwQ0poQXg3TmVra1JGbndvZWNMZnFJeXJqWHJnQVpzVE1QaVg5NHNuamRyemlzQVh0QkthbnVsaWxTeE9mVzJIU21mbmswTDJHWEhKSjNTK3ZDYmdSRW1sOWRla2ZtSGhtd0VZdHYza2ZodGg3RVJJd1ZUOGo2OU90SUJDbWVUSUp1QTdvNzNQWGMxTEoyd2h1Wks3RlplNk5UT3M2MU1QN0pWbEljNnRiaHRPQmszTlhVOHBCUE9DWTZRNTVkNjNGdHBVVS9LQ2JRMEsyV1E5bzVqTkd3Tk5KNkN3NUZEQ3QreDVZSmVQMWQyUXpEU2lxK2NTWnF3cUNaVmNOb0FmdEdHemdyUlo0dmtwYlAweFFaSEN0V3NVd0hWVGpyWTROZlJEakhkVlBSbjhKOVh0b29jL2hJV2txekZhY3pZVGxhcHUyUkNKcWU4YUVvM2p6V1pDRUo3QWlCekJZcmFZRGdPZmVpUXllSlVzYmJYSGRHS2lpdzIyQmRwNE0rWXo2NzFNWjBpZTlFV1JrczBRMkRIR1VUbmU5UzR0QUFKUEdiVWY0eHRDTW9KNDBRNFFXMDVRTFNaeTN3TkZBVUZZbVhFNEVDREZCOCtJRWVORkZGL3RnQW5maDhHdkZoLzF4WStMRStvek1taW5qU1J3aHEwOXpRQUVrTkZ5dTBJS2d2RGt5T0kraWNjRjdkb1dMVFVhc1B4TEgxeEdQQlNLa2VXQ25Tb1gvNEMyYzlkdTY2QmJSWGFtSHFhZStWQ2lTNkJOOTUzaVlLT3U4ck1rbmVXK1RGalYwQ3pBZjhQTkc5TEJheVVnWTVVdzl4MFNhcDZ6STRQcGFaS0g0ZDlrdGtGVSs5MXlCYk41U1J0ZGQ4L0VRYXRKWG1CTmZ3N2tScmxsK0xqd1p3ZVdMRWIwalQxUnBHdURxSExkTFRNTWdvd1NjenJwMnpDc0pkUUgvc2VhWXJwd2tDRlI4ZFJqTTdKUlVrVmExQTBTdmRaQVg3NXZ1QnZ2TEJ6TjJjK1JidDVvcXk5M2xxSUJJaktiN3cyVWtSVGI0cEJXY05LTlBVZ3QzYXovUHhReEVhc2FsdGRtdXl2c1hZYnVyM1ZQTm44SnpOLzZKNU4rUUNuejU3MVRjY2FwQ21rUHZGRStnYXlYVStVRjFWS1l5RmNVdmxldjdsc1FsbWpxeksyUUE4WDdRM1R6RmFwcWh2SktsdDdkekJIcmFxWkJnUGdiOU40b0tOYXlpOEZaVDcxK2thWWhJaEZyRVNLWVJIZkhrajVuM2tmaGdMeSt4K2pMRGJLeE1nZ2tnSG5KejlSdUVIWlhCV3RrWUFRPT0iLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsiaWQiOiJ0b29sdV8wMUg2ZlY4SnZSSk52WHk0N2VSbmQzakgiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6e30sIm5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHdBTVJkUzIxRmVKZnpRaE0zcyIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo3Mzk2LCJvdXRwdXRfdG9rZW5zIjo3NzAsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjczOX0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoic2tlcHRpYyIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy00YWU5LTc2YzAtODlhNi00ZjBiMTE0ZTMwMzAtMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzM5Niwib3V0cHV0X3Rva2VucyI6NzcwLCJ0b3RhbF90b2tlbnMiOjgxNjYsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjczOX19fSx7ImNvbnRlbnQiOiJTdWNjZXNzZnVsbHkgdHJhbnNmZXJyZWQgdG8gdmVyaWZpZXIiLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7fSwidHlwZSI6InRvb2wiLCJuYW1lIjoidHJhbnNmZXJfdG9fdmVyaWZpZXIiLCJpZCI6ImMwNjQ1NjNhLTk3NGYtNGYwZi1hYjRhLTA4MGU2M2MwMWNmNSIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsInN0YXR1cyI6InN1Y2Nlc3MifSx7ImNvbnRlbnQiOlt7InNpZ25hdHVyZSI6IkNBUVM5Z1VLRUFnU0dBSTRBVUlJZEdocGJtdHBibWNTREdGeUoxTVhvWStBaTZOa2J4b01SZDJUclVmVVU3V3JQcEEySWpEZ3k4ZFVwbGNaS2M3RG5Kd05MVEJmSDVVTlIrbkduYWtTUFFpYThVVkl6cmprT25KMmZMcldVc0Eyb0VPOHlSc3Frd1hra0RDc3loQjk3ZHdqNTdSTXQ5MVh6Z1doTDlsdU5FR085MnlXMHJORkdvTVNVVk94UElyRmM1Ump5cmhLRWMzKzFSbnB4Q2t5bHErTE5sUTVORnhMbjZ2MmNGNUR6cXB2QkhOUjdYQWlnK3VySklVM2dYeWRDa2FHWlpEZXRKdFFPVU9YZ1ppOXo5MTAwTnBkQUVrWERvMklDSENHVEdwVmMwSi9wYlh4V1JCeGI2QjEvR2wwWWpuNjhWNkhsZXNIN0lvQzdHUi8zSHdSdTlOU0RCRzR0dGZRakVXY2crUGVBZWE0Um43anp4K1RtOUg3TDFqb1RLVmdEYlFnNWlLTUxmanZQcWxua3NDTHRYT3Q4alNJUzVPWUxON2EvN1RYNi93NWQzNHZudzh2UERtVTFVWWkwUTcycVVXaFVhMnZ3OE9YTnBjS0w3QnAwbE5mQlM4cnZIWSt2Qlk4L2hhQW9UTDN3dkxGVTFmVCs0ei92QTNCY3NpZ1pEWWxmSmVEWWN4WG1SdXF4enhFR3BYQ1JlOXMxSFd0SUQrNDFwSHU2bHBaSGtYYWdocGM2VFpITHhOd2NWaFRiNmZNUTdRbysyTDZQdmtsaDFtMzJwZW9keGVnMllpUGhkcGwzdy94a09FdTUvSkVGN1pCeHBkVUwrdTN3a081cFVsc3FmZ1g3dWh6djdIQzJXcFNXVmltcndpUXdRT1V4MCtKUHJMdHcrbFlmcTdJM2pydW8wR1cwZ3VXdm51ZjdKTTNZTGo4NHErSmdIb21uU1Nabm5abXJMMFVhUnZyVnlMbzg3N0YzOWNjTFM3WG9abm51aThKNFA1OFpUbWlMa0VKZUdjeDFJdWFnMmUzMzMzOU83WWV3LzU5dExiUXRJR2g5VldNd0hCbkZ4MzlTOC80TEVGcks1SGxkUFFoT3Jsb3lUdGtSczY2aVFSS3R4TUtTMFZJNlFUS1pEQWJzN2E3SDF5QnYrbUppOVhnNHVLbXlVNnFubjd6RUtqalkyYklsQ3dleng0TExHUDVIRDNacjZmWnNtZEcxVUh6RHNCdllHMm9DSTBvcUVLTSt2YWxzVDZoVThNUmpiUUJEL0pKOEpKTi9pQkdsaEJBVUhvMFVtaGh0UjJldVRWcFpXbE1uaG1wWEFkS0RYU2ZQZGFQb2hnQiIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJpZCI6InRvb2x1XzAxWEVBdWhRSENCZHlDd3R5bVp3RmZEaSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3JlZF90ZWFtIiwidHlwZSI6InRvb2xfdXNlIiwidG9vbHNldF9uYW1lIjpudWxsfV0sImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2Zkd0FzYWJEcXVlZ0hpNllVcENuIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjc1MDksIm91dHB1dF90b2tlbnMiOjg4LCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjo1NX0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoidmVyaWZpZXIiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtNjY4Mi03ZDIxLThjMTUtZmZiY2U5NDIyYzlmLTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoidHJhbnNmZXJfdG9fcmVkX3RlYW0iLCJhcmdzIjp7fSwiaWQiOiJ0b29sdV8wMVhFQXVoUUhDQmR5Q3d0eW1ad0ZmRGkiLCJ0eXBlIjoidG9vbF9jYWxsIn1dLCJpbnZhbGlkX3Rvb2xfY2FsbHMiOltdLCJ1c2FnZV9tZXRhZGF0YSI6eyJpbnB1dF90b2tlbnMiOjc1MDksIm91dHB1dF90b2tlbnMiOjg4LCJ0b3RhbF90b2tlbnMiOjc1OTcsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjU1fX19LHsiY29udGVudCI6IlN1Y2Nlc3NmdWxseSB0cmFuc2ZlcnJlZCB0byByZWRfdGVhbSIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ0cmFuc2Zlcl90b19yZWRfdGVhbSIsImlkIjoiOTgyYzAxMjYtZTZhYi00NThhLTkyMmUtN2IyNjZlODNjMmIyIiwidG9vbF9jYWxsX2lkIjoidG9vbHVfMDFYRUF1aFFIQ0JkeUN3dHltWndGZkRpIiwic3RhdHVzIjoic3VjY2VzcyJ9XSwiYWN0aXZlX2FnZW50IjoicmVkX3RlYW0ifQ==" + } + } + ], + "events": [ + { + "timeUnixNano": "1790968035109279000", + "name": "exception", + "attributes": [ + { + "key": "exception.type", + "value": { + "stringValue": "Exception" + } + }, + { + "key": "exception.message", + "value": { + "stringValue": "ParentCommand(Command(graph='red_team:15ccef64-4162-9e02-e87c-4cb0eb6ea16b', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', id='389058d1-083f-4632-bbc5-09bfd3a0ffd2', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9'), AIMessage(content=[{'signature': 'CAQS1QgKEAgSGAI4AUIIdGhpbmtpbmcSDN6TfU6NDM971lEbzxoMvRUA1J9ox3fJQYT9IjArCQQGHBP4pRHPavXzRlbMbaYHC57Yj+Gbxgr0helvd2OIL/o6Rbt4PFA3FaIFUlgq8gc7KQI9FtGnvSAkLBRzIsUp/2d5Rd2zgEsfkFVeXsn8WYYZkinAQVVy6A7B3LaEJW0xb/x88Di67fedwx0Q+FlZtwcmT3Dcaa11JveyHRr3dm+zGSZP6CXeXNJ8nrTZ7W1YRTysD6w7o7Lp8h1Rwq4f4o8j+QUfY0vje+zJZsxHPmQNt3ykeKh99hgn6dbiz6/EHqmbHjSAu1M4SqEDfqIKThHYenamAZmcQ4XAOo6mdQ6+77VgpV1lC1/k52IiUoJjkGlQrnLCvutiI7KN+sgK8c2+2KCJrPRCvVV+BkStvP5jWWqzytpKQ+nHzhL448OrIA9gdJC39C8eDrXeRnI7t4FqlkXGoMlZAV9UMcrQHRxOMi8tOyTzi9oyBFDTSsQ+LJqer0tz1Ft9gek6vzg50SpVAFQdLERv8R1YqMaeI+xsvTAEW9MRgvRMkzLuzRT2guw3jD+Fjip37xgK5QI0R+CsOAmQPoTZizzcIyAUAuCJFCgq/gD/CibLxoymiVUI1an9Snt3KpNRoWdvWIne33V0pv8PHg9yW2JuLOqQGilLCsK/Q3oQr3upEMXxXGjCL7WUizvRZGFEEV+VplYt7v4tKZMj5pk3vdeM0MJ5XZg7PvUsRzycxcnrT3Knoc3qM+ttqZfTsb8d6U/pQifq7GuahFOHrvroqgJLj+/81Yy1pZFkjfJgY0XklzAnA6CFq/YT0gxbV+TH5IS+WaYOUzK36S8s5qqMwhngv4IzE3ctXXqnhIZXNznyVBqbYa/Gx2bCjcEj2n5RyGYRS/+MculgLBznQ9MkG2D7QP3Mq/nwHXKMIaLS8VK8hG3d0rAjlvnauXShNrJxtkj4qkwnxD4LrEjk1ue+PLcbLqYWwAr8n0s+41jrsIPVyyK9vFaEZrA9KaBxkJyiqx50GZNn7ckx+mkI18gH52iyVPtU8FKMXjugCl6mPLiHYiDQd+G63LudmOKys9itVnBDBNMP2b/I7+bIVRlheGaZWr6MATKl86B3y+0kuOrLRMyWcdibc2iQkZm+8f7dTUwbKbBJsKOFY73/8nlL/JiYpjz6nUIk+IqSjaFyK6XFlCcukWpWHpDJnSvbEzey7qHyCqJ+I+on8iP8GOtempQaS0/4kZGSwlrE6xOj6EwGpfjbDZKHkP/2RAdNYPQq3z7bdVJMJNnFMz6e37mzMpCBihwPdSFTRE3SCY66wvTgJzGCUyhHbSi5i1azzOXd8uHCSpT/6L1o6VU0pjoR8Kaa4wCWHGf7P/WEd+avyp02Jnu1cmpW0+Gq+vK9h0tdb2IClM/eNfdouQP+ep8MJUjraDRICmAJQY5obS71vniOo+reaFy0zRgB', 'thinking': '', 'type': 'thinking'}, {'signature': 'CAQS/REKEQgSGAI4AUIJbmFycmF0aW9uEgwFh3AVJZP3s8y1Hq0aDG76uhHONaScXAGShiIwGgjLXHUx1yZrkLPljjzTOQycpLJiKVwXcxm/EzKJm1s8M5WxuREWEldXcoaN0/msKpkRMT+eLb5ih9rAHULTomUNsUwJeu0jrJe4GLazy7sP5KtjEOEBKQsXUfoQ7fDkZOQUHZi9zzQN4hFNmYJUf3OVqf9TQNdC1+m//HAZPfWmAy6HtDvm7kgBJkCEoiFnm7tn/PX1hXGPRgWc6BTXUMr2WA8aKyqggRUbMH2+JYv5iWZddCc1up1Nbr3sF7WPcot9ZBhyHQXr692zeJk6SJgygIVrASbMly2bBxTWJB623gVgzpCmVSLZFpkKXgPcp8hGyDKOvF6SiQSHqGRxQofr7Vs1aPhfZixROJGD+MlrkggVAnGMyYIDUZpavtCeiU1JmBSz0084bXPtT/tnpsKOnSPYZQo3B0BXKbqA/WsKng8/V3Z8Uuajo6IVZ2hgA1WDduqQOQkExGU6pFuLhebIjuWxSNI/DffvR//LNqCfvdnf0hvoPtDxfx/SawIx4/spq9xbwFzHtLgoHg1F2wFw7clPDlg1PyD/v2yJG3V9T8GL6jGPE6rhfil/u4HNvf7BW0dQzCTTXcqZy4nByy0+p+S7ZxfMl4Kpxa2gOhhSNjfpfuX0QpL4kME8J1nliFN1d8KqEPuDI18BQHUhD/UfY29oA0x510FjM/BsLZ4n5sppkoIi7CFoQq2aAKVNfC1KilhIpRD+1dyDtZQklN8gZpOESHw6ML1TtU0yfAcbmqoxJ6hhjzwH6CMsTQEEy8MbRBM+QKKkIRdRCPHGFW2LkuR50+J0pF/tr6pqVvJeozfiMnrEilB27TZP4nKTJG8rd//vCWen0y7CA/8dBJ9gdp5tx7NVA45k1gdcxER1gYouNX3n1vG3/B1F4+93wXW9Mqo6Us59TQmrb9Nqa7pSQNSrH06w1GxyBAlkFfqAlafk16aMpq416TJYpGy43WCUiCcLWlhiWgz2sgOLAXP9a5weWAKRC8J6jkcUkDeXnW+HFSiPM6+9Bo6kVhLUhN5rc4mHZOZpF7ptPpsQN4XCqLPsqpRH/8o5XpyC6ZneTeF2ExtjhwZVjXDwMbIzVYIbw1y2PPGw8dC7pc4sIVSnI4oQAH0v650ghfhmMevWW4h4Pylbw/DG4fdeT3+8z0f8MOSTtfipx05/QttHlAvODRxa5yDCvOz6n3f1BO0N/4MbGyOM27qW4x8mBVoryUZWfsmhfi0/GfNvB8+ylTGbYNX27OcEF5WZtNqbWEy7V5GbmZUSxspcg7FBZ3j3/3L7muvZXoZi1f5erq4y3lSH+oF7qPFnvieyLOCwubcQy+oyowj4cD0idRecnp+s+52D7+mzF1gpW+f+cJasoxigbxkOloS4yeiKvgM4nWSpGEj12RPx5o80EmziXN9t4zJzW1htA5hXJJeFympMYtyCUCevRWVmrRYktQ7UnCa6tL/AVPi1ni5XCXHfLOuqfK5KLYxjwVtdhhfmCLvpaKVNCEOCf4nNGHqD98wUb0ejWNwDYBnJGkBRw9RVspfwNdoWoVcn2qv8ywGUp7yc/sk2LpI6VziRyvWqbGNG1ZJqVVutRc46+sOE4VSeS2/CTNKNa6NMCf/hxxaUVsYfKdP+S1Lc4ianKp0sUugPpmtoydC0CP2RwjnHLcnzxCnvqxlLQWXMNPTsu2yBcHlWJLtcmdg/McLagy7l5kG6sN7ZxJLD/kAx8E6uqohP+K/jMSrYuQJnhr4tf4biWFY3Jzf/KpAYyVSq+pbfFKXMbL9nHEoZQT15Wyabd6DMAq3QZOthM5pUzdNqUXkcrI1PwjASAg4dVU/2o+98mTXnyhP7VlEkTnddpwyJF+YCCc01pqa6ZX99pnR+YOpIrb5DtPcKq0t5cFCDyQxN1lUuqZcYujvX/GC7+6IpRTz/NkEY/5cntOfZJyd6SkUx/JR5AJIRPCNeib0rcWnnFn0XK8s22x40wLjEtufXDO9+yraf4kHxlBI3SB0meMjepX50CJhAx7NekkRFnwoecLfqIyrjXrgAZsTMPiX94snjdrzisAXtBKanulilSxOfW2HSmfnk0L2GXHJJ3S+vCbgREml9dekfmHhmwEYtv3kfhth7ERIwVT8j69OtIBCmeTIJuA7o73PXc1LJ2whuZK7FZe6NTOs61MP7JVlIc6tbhtOBk3NXU8pBPOCY6Q55d63FtpUU/KCbQ0K2WQ9o5jNGwNNJ6Cw5FDCt+x5YJeP1d2QzDSiq+cSZqwqCZVcNoAftGGzgrRZ4vkpbP0xQZHCtWsUwHVTjrY4NfRDjHdVPRn8J9Xtooc/hIWkqzFaczYTlapu2RCJqe8aEo3jzWZCEJ7AiBzBYraYDgOfeiQyeJUsbbXHdGKiiw22Bdp4M+Yz671MZ0ie9EWRks0Q2DHGUTne9S4tAAJPGbUf4xtCMoJ40Q4QW05QLSZy3wNFAUFYmXE4ECDFB8+IEeNFFF/tgAnfh8GvFh/1xY+LE+ozMminjSRwhq09zQAEkNFyu0IKgvDkyOI+iccF7doWLTUasPxLH1xGPBSKkeWCnSoX/4C2c9du66BbRXamHqae+VCiS6BN953iYKOu8rMkneW+TFjV0CzAf8PNG9LBayUgY5Uw9x0Sap6zI4PpaZKH4d9ktkFU+91yBbN5SRtdd8/EQatJXmBNfw7kRrll+LjwZweWLEb0jT1RpGuDqHLdLTMMgowSczrp2zCsJdQH/seaYrpwkCFR8dRjM7JRUkVa1A0SvdZAX75vuBvvLBzN2c+Rbt5oqy93lqIBIjKb7w2UkRTb4pBWcNKNPUgt3az/PxQxEasaltdmuyvsXYbur3VPNn8JzN/6J5N+QCnz571TccapCmkPvFE+gayXU+UF1VKYyFcUvlev7lsQlmjqzK2QA8X7Q3TzFapqhvJKlt7dzBHraqZBgPgb9N4oKNayi8FZT71+kaYhIhFrESKYRHfHkj5n3kfhgLy+x+jLDbKxMggkgHnJz9RuEHZXBWtkYAQ==', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_verifier', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAMRdS21FeJfzQhM3s', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7396, 'output_tokens': 770, 'output_tokens_details': {'thinking_tokens': 739}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='skeptic', id='lc_run--01a0fe03-4ae9-76c0-89a6-4f0b114e3030-0', tool_calls=[{'name': 'transfer_to_verifier', 'args': {}, 'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7396, 'output_tokens': 770, 'total_tokens': 8166, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 739}}), ToolMessage(content='Successfully transferred to verifier', name='transfer_to_verifier', id='c064563a-974f-4f0f-ab4a-080e63c01cf5', tool_call_id='toolu_01H6fV8JvRJNvXy47eRnd3jH'), AIMessage(content=[{'signature': 'CAQS9gUKEAgSGAI4AUIIdGhpbmtpbmcSDGFyJ1MXoY+Ai6NkbxoMRd2TrUfUU7WrPpA2IjDgy8dUplcZKc7DnJwNLTBfH5UNR+nGnakSPQia8UVIzrjkOnJ2fLrWUsA2oEO8yRsqkwXkkDCsyhB97dwj57RMt91XzgWhL9luNEGO92yW0rNFGoMSUVOxPIrFc5RjyrhKEc3+1RnpxCkylq+LNlQ5NFxLn6v2cF5DzqpvBHNR7XAig+urJIU3gXydCkaGZZDetJtQOUOXgZi9z9100NpdAEkXDo2ICHCGTGpVc0J/pbXxWRBxb6B1/Gl0Yjn68V6HlesH7IoC7GR/3HwRu9NSDBG4ttfQjEWcg+PeAea4Rn7jzx+Tm9H7L1joTKVgDbQg5iKMLfjvPqlnksCLtXOt8jSIS5OYLN7a/7TX6/w5d34vnw8vPDmU1UYi0Q72qUWhUa2vw8OXNpcKL7Bp0lNfBS8rvHY+vBY8/haAoTL3wvLFU1fT+4z/vA3BcsigZDYlfJeDYcxXmRuqxzxEGpXCRe9s1HWtID+41pHu6lpZHkXaghpc6TZHLxNwcVhTb6fMQ7Qo+2L6Pvklh1m32peodxeg2YiPhdpl3w/xkOEu5/JEF7ZBxpdUL+u3wkO5pUlsqfgX7uhzv7HC2WpSWVimrwiQwQOUx0+JPrLtw+lYfq7I3jruo0GW0guWvnuf7JM3YLj84q+JgHomnSSZnnZmrL0UaRvrVyLo877F39ccLS7XoZnnui8J4P58ZTmiLkEJeGcx1Iuag2e33339O7Yew/59tLbQtIGh9VWMwHBnFx39S8/4LEFrK5HldPQhOrloyTtkRs66iQRKtxMKS0VI6QTKZDAbs7a7H1yBv+mJi9Xg4uKmyU6qnn7zEKjjY2bIlCwezx4LLGP5HD3Zr6fZsmdG1UHzDsBvYG2oCI0oqEKM+valsT6hU8MRjbQBD/JJ8JJN/iBGlhBAUHo0UmhhtR2euTVpZWlMnhmpXAdKDXSfPdaPohgB', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_red_team', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAsabDquegHi6YUpCn', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7509, 'output_tokens': 88, 'output_tokens_details': {'thinking_tokens': 55}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='verifier', id='lc_run--01a0fe03-6682-7d21-8c15-ffbce9422c9f-0', tool_calls=[{'name': 'transfer_to_red_team', 'args': {}, 'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7509, 'output_tokens': 88, 'total_tokens': 7597, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 55}}), ToolMessage(content='Successfully transferred to red_team', name='transfer_to_red_team', id='982c0126-e6ab-458a-922e-7b266e83c2b2', tool_call_id='toolu_01XEAuhQHCBdyCwtymZwFfDi'), AIMessage(content=[{'signature': 'CAQSwgcKEAgSGAI4AUIIdGhpbmtpbmcSDORXge9uL1832RMQwBoMjQwAwAHb9iP1ojPGIjDDScal486beCJbtG8/D2Y9g9S5xIPfjoHg/dxwX5HypSQEBbjFiTSbPECXSFzV6YIq3wbLcmyPbvusGnDexFgx+3sIXzvqyFV/69xhihMnSpfTCHy0+0zSu1WpYU/WUpIfoaSXTcTa1qkM8x8Ek/y/1jkKMZUfDFa93uhUJxOC4+QgPWBl/+Xy2t82L2x/ebq7QLQTqtR0jXeaURQm3Co6JvOgWmDvMpEdTeTWE3fDh1tzzVKsflum4d89b6CaKtn8nlQd7RhN99vNQMQHG5V4OuVNdrHPZZpFOuhdejsMFWXyB+55slGAd8wEXgFKbI/knOXVGzL/NDF8Gty1tNZlu2fdSCAsYWMsFQP1cbwLeQGszyXzJMVZJ2I9xBPkyz1HOXCPyh5F+j2gRTPikVB3ARC6tfB/lDSUeKBVZPQS8pY/B4eCs+IjjaBNWruLzfqCsc6xL0CuQPSYyQXaYFjYrSRNcC70WQBWT5VVhp0mA9yZDGYbRZt9I5OK9QkQmuJ5zDFaMEOXzSrsC4lteP6MdimiMijvsH3iq+c6WUYolRj3neI88EaHIzLAGRq64BNCLzdAYL2pEU2sb5uG78PKVoUUCbVJL5jpDOhLOTO1bQsTuoUpnRRrLQD/kIzV+ljpVBwwfJcyq75TJA/CtPr/xr2SZ9B1b2sbjG6jD0lhtoCddeDgcqgl/LSSkcuaYY8xPWCy+LtNhcqThfmfvnHLJIpL6gjbjW/OQpRfVUwfw1jOb9bWVwcgHhNYfKYGPtM8oth5xwLYRRVgzqMKOvs7falXRhVzZ2SP6Iw0BMmAAZ6HjfCl9L+tPqJNlnQQOHpoWjum4B+XsYldFEKGQWQqsd59iNPvao9p/sCOt7rGqCozcmgiEm4w66ez/QIDyb46n0+tcNJPUcoBKvQ/jDwE6VwmkQ/nECjFp3UdDT+kWYQSNd8j8VBZOpG8LlqUYrEZCv0q7Bs1vcNrdYiaFoS32osaynWk+Ht3OHuFWbEF8JlsFTQAO05VvYkecf0V/oTHsG5hyOOs7UEaDFq7uaJuzWVhh5pAojIaBcR8fVln2PLRTX7is2nsA+QrdidePcZtFtgxAiKYe1i2sSFi4vm8EOkCbQ63wdQeY4Urr5PuoMEwxUULjgKagiAmsJDq4RrVm7sO4+6YuA9j6l2uylkOEc67vf/5fkXuCsiApNL6keb8c+vL0n4FXGdzj+apSDfFGBgB', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01VBrWdWEWYgit4kNYehBMU5', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_editor', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAzfjqUopoEmZLaqm1', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7416, 'output_tokens': 151, 'output_tokens_details': {'thinking_tokens': 119}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='red_team', id='lc_run--01a0fe03-6ce6-7251-b796-4f64333f67c6-0', tool_calls=[{'name': 'transfer_to_editor', 'args': {}, 'id': 'toolu_01VBrWdWEWYgit4kNYehBMU5', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7416, 'output_tokens': 151, 'total_tokens': 7567, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 119}}), ToolMessage(content='Successfully transferred to editor', name='transfer_to_editor', tool_call_id='toolu_01VBrWdWEWYgit4kNYehBMU5')], 'active_agent': 'editor'}, goto='editor'))Traceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 707, in invoke\n input = context.run(step.invoke, input, config, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 3913, in invoke\n for chunk in self.stream(\n ~~~~~~~~~~~^\n input,\n ^^^^^^\n ...<11 lines>...\n **kwargs,\n ^^^^^^^^^\n ):\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 2967, in stream\n for _ in runner.tick(\n ~~~~~~~~~~~^\n [t for t in loop.tasks.values() if not t.writes],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<2 lines>...\n schedule_task=loop.accept_push,\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ):\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_runner.py\", line 207, in tick\n run_with_retry(\n ~~~~~~~~~~~~~~^\n t,\n ^^\n ...<10 lines>...\n },\n ^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_retry.py\", line 617, in run_with_retry\n return task.proc.invoke(task.input, config)\n ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 709, in invoke\n input = step.invoke(input, config)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 447, in invoke\n ret = self.func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 84, in _write\n self.do_write(\n ~~~~~~~~~~~~~^\n config,\n ^^^^^^^\n writes,\n ^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 126, in do_write\n write(_assemble_writes(writes))\n ~~~~~~~~~~~~~~~~^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 181, in _assemble_writes\n if ww := w.mapper(w.value):\n ~~~~~~~~^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/graph/state.py\", line 1762, in _control_branch\n raise ParentCommand(command)\n\n\nlanggraph.errors.ParentCommand: Command(graph='red_team:15ccef64-4162-9e02-e87c-4cb0eb6ea16b', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', id='389058d1-083f-4632-bbc5-09bfd3a0ffd2', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9'), AIMessage(content=[{'signature': 'CAQS1QgKEAgSGAI4AUIIdGhpbmtpbmcSDN6TfU6NDM971lEbzxoMvRUA1J9ox3fJQYT9IjArCQQGHBP4pRHPavXzRlbMbaYHC57Yj+Gbxgr0helvd2OIL/o6Rbt4PFA3FaIFUlgq8gc7KQI9FtGnvSAkLBRzIsUp/2d5Rd2zgEsfkFVeXsn8WYYZkinAQVVy6A7B3LaEJW0xb/x88Di67fedwx0Q+FlZtwcmT3Dcaa11JveyHRr3dm+zGSZP6CXeXNJ8nrTZ7W1YRTysD6w7o7Lp8h1Rwq4f4o8j+QUfY0vje+zJZsxHPmQNt3ykeKh99hgn6dbiz6/EHqmbHjSAu1M4SqEDfqIKThHYenamAZmcQ4XAOo6mdQ6+77VgpV1lC1/k52IiUoJjkGlQrnLCvutiI7KN+sgK8c2+2KCJrPRCvVV+BkStvP5jWWqzytpKQ+nHzhL448OrIA9gdJC39C8eDrXeRnI7t4FqlkXGoMlZAV9UMcrQHRxOMi8tOyTzi9oyBFDTSsQ+LJqer0tz1Ft9gek6vzg50SpVAFQdLERv8R1YqMaeI+xsvTAEW9MRgvRMkzLuzRT2guw3jD+Fjip37xgK5QI0R+CsOAmQPoTZizzcIyAUAuCJFCgq/gD/CibLxoymiVUI1an9Snt3KpNRoWdvWIne33V0pv8PHg9yW2JuLOqQGilLCsK/Q3oQr3upEMXxXGjCL7WUizvRZGFEEV+VplYt7v4tKZMj5pk3vdeM0MJ5XZg7PvUsRzycxcnrT3Knoc3qM+ttqZfTsb8d6U/pQifq7GuahFOHrvroqgJLj+/81Yy1pZFkjfJgY0XklzAnA6CFq/YT0gxbV+TH5IS+WaYOUzK36S8s5qqMwhngv4IzE3ctXXqnhIZXNznyVBqbYa/Gx2bCjcEj2n5RyGYRS/+MculgLBznQ9MkG2D7QP3Mq/nwHXKMIaLS8VK8hG3d0rAjlvnauXShNrJxtkj4qkwnxD4LrEjk1ue+PLcbLqYWwAr8n0s+41jrsIPVyyK9vFaEZrA9KaBxkJyiqx50GZNn7ckx+mkI18gH52iyVPtU8FKMXjugCl6mPLiHYiDQd+G63LudmOKys9itVnBDBNMP2b/I7+bIVRlheGaZWr6MATKl86B3y+0kuOrLRMyWcdibc2iQkZm+8f7dTUwbKbBJsKOFY73/8nlL/JiYpjz6nUIk+IqSjaFyK6XFlCcukWpWHpDJnSvbEzey7qHyCqJ+I+on8iP8GOtempQaS0/4kZGSwlrE6xOj6EwGpfjbDZKHkP/2RAdNYPQq3z7bdVJMJNnFMz6e37mzMpCBihwPdSFTRE3SCY66wvTgJzGCUyhHbSi5i1azzOXd8uHCSpT/6L1o6VU0pjoR8Kaa4wCWHGf7P/WEd+avyp02Jnu1cmpW0+Gq+vK9h0tdb2IClM/eNfdouQP+ep8MJUjraDRICmAJQY5obS71vniOo+reaFy0zRgB', 'thinking': '', 'type': 'thinking'}, {'signature': 'CAQS/REKEQgSGAI4AUIJbmFycmF0aW9uEgwFh3AVJZP3s8y1Hq0aDG76uhHONaScXAGShiIwGgjLXHUx1yZrkLPljjzTOQycpLJiKVwXcxm/EzKJm1s8M5WxuREWEldXcoaN0/msKpkRMT+eLb5ih9rAHULTomUNsUwJeu0jrJe4GLazy7sP5KtjEOEBKQsXUfoQ7fDkZOQUHZi9zzQN4hFNmYJUf3OVqf9TQNdC1+m//HAZPfWmAy6HtDvm7kgBJkCEoiFnm7tn/PX1hXGPRgWc6BTXUMr2WA8aKyqggRUbMH2+JYv5iWZddCc1up1Nbr3sF7WPcot9ZBhyHQXr692zeJk6SJgygIVrASbMly2bBxTWJB623gVgzpCmVSLZFpkKXgPcp8hGyDKOvF6SiQSHqGRxQofr7Vs1aPhfZixROJGD+MlrkggVAnGMyYIDUZpavtCeiU1JmBSz0084bXPtT/tnpsKOnSPYZQo3B0BXKbqA/WsKng8/V3Z8Uuajo6IVZ2hgA1WDduqQOQkExGU6pFuLhebIjuWxSNI/DffvR//LNqCfvdnf0hvoPtDxfx/SawIx4/spq9xbwFzHtLgoHg1F2wFw7clPDlg1PyD/v2yJG3V9T8GL6jGPE6rhfil/u4HNvf7BW0dQzCTTXcqZy4nByy0+p+S7ZxfMl4Kpxa2gOhhSNjfpfuX0QpL4kME8J1nliFN1d8KqEPuDI18BQHUhD/UfY29oA0x510FjM/BsLZ4n5sppkoIi7CFoQq2aAKVNfC1KilhIpRD+1dyDtZQklN8gZpOESHw6ML1TtU0yfAcbmqoxJ6hhjzwH6CMsTQEEy8MbRBM+QKKkIRdRCPHGFW2LkuR50+J0pF/tr6pqVvJeozfiMnrEilB27TZP4nKTJG8rd//vCWen0y7CA/8dBJ9gdp5tx7NVA45k1gdcxER1gYouNX3n1vG3/B1F4+93wXW9Mqo6Us59TQmrb9Nqa7pSQNSrH06w1GxyBAlkFfqAlafk16aMpq416TJYpGy43WCUiCcLWlhiWgz2sgOLAXP9a5weWAKRC8J6jkcUkDeXnW+HFSiPM6+9Bo6kVhLUhN5rc4mHZOZpF7ptPpsQN4XCqLPsqpRH/8o5XpyC6ZneTeF2ExtjhwZVjXDwMbIzVYIbw1y2PPGw8dC7pc4sIVSnI4oQAH0v650ghfhmMevWW4h4Pylbw/DG4fdeT3+8z0f8MOSTtfipx05/QttHlAvODRxa5yDCvOz6n3f1BO0N/4MbGyOM27qW4x8mBVoryUZWfsmhfi0/GfNvB8+ylTGbYNX27OcEF5WZtNqbWEy7V5GbmZUSxspcg7FBZ3j3/3L7muvZXoZi1f5erq4y3lSH+oF7qPFnvieyLOCwubcQy+oyowj4cD0idRecnp+s+52D7+mzF1gpW+f+cJasoxigbxkOloS4yeiKvgM4nWSpGEj12RPx5o80EmziXN9t4zJzW1htA5hXJJeFympMYtyCUCevRWVmrRYktQ7UnCa6tL/AVPi1ni5XCXHfLOuqfK5KLYxjwVtdhhfmCLvpaKVNCEOCf4nNGHqD98wUb0ejWNwDYBnJGkBRw9RVspfwNdoWoVcn2qv8ywGUp7yc/sk2LpI6VziRyvWqbGNG1ZJqVVutRc46+sOE4VSeS2/CTNKNa6NMCf/hxxaUVsYfKdP+S1Lc4ianKp0sUugPpmtoydC0CP2RwjnHLcnzxCnvqxlLQWXMNPTsu2yBcHlWJLtcmdg/McLagy7l5kG6sN7ZxJLD/kAx8E6uqohP+K/jMSrYuQJnhr4tf4biWFY3Jzf/KpAYyVSq+pbfFKXMbL9nHEoZQT15Wyabd6DMAq3QZOthM5pUzdNqUXkcrI1PwjASAg4dVU/2o+98mTXnyhP7VlEkTnddpwyJF+YCCc01pqa6ZX99pnR+YOpIrb5DtPcKq0t5cFCDyQxN1lUuqZcYujvX/GC7+6IpRTz/NkEY/5cntOfZJyd6SkUx/JR5AJIRPCNeib0rcWnnFn0XK8s22x40wLjEtufXDO9+yraf4kHxlBI3SB0meMjepX50CJhAx7NekkRFnwoecLfqIyrjXrgAZsTMPiX94snjdrzisAXtBKanulilSxOfW2HSmfnk0L2GXHJJ3S+vCbgREml9dekfmHhmwEYtv3kfhth7ERIwVT8j69OtIBCmeTIJuA7o73PXc1LJ2whuZK7FZe6NTOs61MP7JVlIc6tbhtOBk3NXU8pBPOCY6Q55d63FtpUU/KCbQ0K2WQ9o5jNGwNNJ6Cw5FDCt+x5YJeP1d2QzDSiq+cSZqwqCZVcNoAftGGzgrRZ4vkpbP0xQZHCtWsUwHVTjrY4NfRDjHdVPRn8J9Xtooc/hIWkqzFaczYTlapu2RCJqe8aEo3jzWZCEJ7AiBzBYraYDgOfeiQyeJUsbbXHdGKiiw22Bdp4M+Yz671MZ0ie9EWRks0Q2DHGUTne9S4tAAJPGbUf4xtCMoJ40Q4QW05QLSZy3wNFAUFYmXE4ECDFB8+IEeNFFF/tgAnfh8GvFh/1xY+LE+ozMminjSRwhq09zQAEkNFyu0IKgvDkyOI+iccF7doWLTUasPxLH1xGPBSKkeWCnSoX/4C2c9du66BbRXamHqae+VCiS6BN953iYKOu8rMkneW+TFjV0CzAf8PNG9LBayUgY5Uw9x0Sap6zI4PpaZKH4d9ktkFU+91yBbN5SRtdd8/EQatJXmBNfw7kRrll+LjwZweWLEb0jT1RpGuDqHLdLTMMgowSczrp2zCsJdQH/seaYrpwkCFR8dRjM7JRUkVa1A0SvdZAX75vuBvvLBzN2c+Rbt5oqy93lqIBIjKb7w2UkRTb4pBWcNKNPUgt3az/PxQxEasaltdmuyvsXYbur3VPNn8JzN/6J5N+QCnz571TccapCmkPvFE+gayXU+UF1VKYyFcUvlev7lsQlmjqzK2QA8X7Q3TzFapqhvJKlt7dzBHraqZBgPgb9N4oKNayi8FZT71+kaYhIhFrESKYRHfHkj5n3kfhgLy+x+jLDbKxMggkgHnJz9RuEHZXBWtkYAQ==', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_verifier', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAMRdS21FeJfzQhM3s', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7396, 'output_tokens': 770, 'output_tokens_details': {'thinking_tokens': 739}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='skeptic', id='lc_run--01a0fe03-4ae9-76c0-89a6-4f0b114e3030-0', tool_calls=[{'name': 'transfer_to_verifier', 'args': {}, 'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7396, 'output_tokens': 770, 'total_tokens': 8166, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 739}}), ToolMessage(content='Successfully transferred to verifier', name='transfer_to_verifier', id='c064563a-974f-4f0f-ab4a-080e63c01cf5', tool_call_id='toolu_01H6fV8JvRJNvXy47eRnd3jH'), AIMessage(content=[{'signature': 'CAQS9gUKEAgSGAI4AUIIdGhpbmtpbmcSDGFyJ1MXoY+Ai6NkbxoMRd2TrUfUU7WrPpA2IjDgy8dUplcZKc7DnJwNLTBfH5UNR+nGnakSPQia8UVIzrjkOnJ2fLrWUsA2oEO8yRsqkwXkkDCsyhB97dwj57RMt91XzgWhL9luNEGO92yW0rNFGoMSUVOxPIrFc5RjyrhKEc3+1RnpxCkylq+LNlQ5NFxLn6v2cF5DzqpvBHNR7XAig+urJIU3gXydCkaGZZDetJtQOUOXgZi9z9100NpdAEkXDo2ICHCGTGpVc0J/pbXxWRBxb6B1/Gl0Yjn68V6HlesH7IoC7GR/3HwRu9NSDBG4ttfQjEWcg+PeAea4Rn7jzx+Tm9H7L1joTKVgDbQg5iKMLfjvPqlnksCLtXOt8jSIS5OYLN7a/7TX6/w5d34vnw8vPDmU1UYi0Q72qUWhUa2vw8OXNpcKL7Bp0lNfBS8rvHY+vBY8/haAoTL3wvLFU1fT+4z/vA3BcsigZDYlfJeDYcxXmRuqxzxEGpXCRe9s1HWtID+41pHu6lpZHkXaghpc6TZHLxNwcVhTb6fMQ7Qo+2L6Pvklh1m32peodxeg2YiPhdpl3w/xkOEu5/JEF7ZBxpdUL+u3wkO5pUlsqfgX7uhzv7HC2WpSWVimrwiQwQOUx0+JPrLtw+lYfq7I3jruo0GW0guWvnuf7JM3YLj84q+JgHomnSSZnnZmrL0UaRvrVyLo877F39ccLS7XoZnnui8J4P58ZTmiLkEJeGcx1Iuag2e33339O7Yew/59tLbQtIGh9VWMwHBnFx39S8/4LEFrK5HldPQhOrloyTtkRs66iQRKtxMKS0VI6QTKZDAbs7a7H1yBv+mJi9Xg4uKmyU6qnn7zEKjjY2bIlCwezx4LLGP5HD3Zr6fZsmdG1UHzDsBvYG2oCI0oqEKM+valsT6hU8MRjbQBD/JJ8JJN/iBGlhBAUHo0UmhhtR2euTVpZWlMnhmpXAdKDXSfPdaPohgB', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_red_team', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAsabDquegHi6YUpCn', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7509, 'output_tokens': 88, 'output_tokens_details': {'thinking_tokens': 55}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='verifier', id='lc_run--01a0fe03-6682-7d21-8c15-ffbce9422c9f-0', tool_calls=[{'name': 'transfer_to_red_team', 'args': {}, 'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7509, 'output_tokens': 88, 'total_tokens': 7597, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 55}}), ToolMessage(content='Successfully transferred to red_team', name='transfer_to_red_team', id='982c0126-e6ab-458a-922e-7b266e83c2b2', tool_call_id='toolu_01XEAuhQHCBdyCwtymZwFfDi'), AIMessage(content=[{'signature': 'CAQSwgcKEAgSGAI4AUIIdGhpbmtpbmcSDORXge9uL1832RMQwBoMjQwAwAHb9iP1ojPGIjDDScal486beCJbtG8/D2Y9g9S5xIPfjoHg/dxwX5HypSQEBbjFiTSbPECXSFzV6YIq3wbLcmyPbvusGnDexFgx+3sIXzvqyFV/69xhihMnSpfTCHy0+0zSu1WpYU/WUpIfoaSXTcTa1qkM8x8Ek/y/1jkKMZUfDFa93uhUJxOC4+QgPWBl/+Xy2t82L2x/ebq7QLQTqtR0jXeaURQm3Co6JvOgWmDvMpEdTeTWE3fDh1tzzVKsflum4d89b6CaKtn8nlQd7RhN99vNQMQHG5V4OuVNdrHPZZpFOuhdejsMFWXyB+55slGAd8wEXgFKbI/knOXVGzL/NDF8Gty1tNZlu2fdSCAsYWMsFQP1cbwLeQGszyXzJMVZJ2I9xBPkyz1HOXCPyh5F+j2gRTPikVB3ARC6tfB/lDSUeKBVZPQS8pY/B4eCs+IjjaBNWruLzfqCsc6xL0CuQPSYyQXaYFjYrSRNcC70WQBWT5VVhp0mA9yZDGYbRZt9I5OK9QkQmuJ5zDFaMEOXzSrsC4lteP6MdimiMijvsH3iq+c6WUYolRj3neI88EaHIzLAGRq64BNCLzdAYL2pEU2sb5uG78PKVoUUCbVJL5jpDOhLOTO1bQsTuoUpnRRrLQD/kIzV+ljpVBwwfJcyq75TJA/CtPr/xr2SZ9B1b2sbjG6jD0lhtoCddeDgcqgl/LSSkcuaYY8xPWCy+LtNhcqThfmfvnHLJIpL6gjbjW/OQpRfVUwfw1jOb9bWVwcgHhNYfKYGPtM8oth5xwLYRRVgzqMKOvs7falXRhVzZ2SP6Iw0BMmAAZ6HjfCl9L+tPqJNlnQQOHpoWjum4B+XsYldFEKGQWQqsd59iNPvao9p/sCOt7rGqCozcmgiEm4w66ez/QIDyb46n0+tcNJPUcoBKvQ/jDwE6VwmkQ/nECjFp3UdDT+kWYQSNd8j8VBZOpG8LlqUYrEZCv0q7Bs1vcNrdYiaFoS32osaynWk+Ht3OHuFWbEF8JlsFTQAO05VvYkecf0V/oTHsG5hyOOs7UEaDFq7uaJuzWVhh5pAojIaBcR8fVln2PLRTX7is2nsA+QrdidePcZtFtgxAiKYe1i2sSFi4vm8EOkCbQ63wdQeY4Urr5PuoMEwxUULjgKagiAmsJDq4RrVm7sO4+6YuA9j6l2uylkOEc67vf/5fkXuCsiApNL6keb8c+vL0n4FXGdzj+apSDfFGBgB', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01VBrWdWEWYgit4kNYehBMU5', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_editor', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAzfjqUopoEmZLaqm1', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7416, 'output_tokens': 151, 'output_tokens_details': {'thinking_tokens': 119}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='red_team', id='lc_run--01a0fe03-6ce6-7251-b796-4f64333f67c6-0', tool_calls=[{'name': 'transfer_to_editor', 'args': {}, 'id': 'toolu_01VBrWdWEWYgit4kNYehBMU5', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7416, 'output_tokens': 151, 'total_tokens': 7567, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 119}}), ToolMessage(content='Successfully transferred to editor', name='transfer_to_editor', tool_call_id='toolu_01VBrWdWEWYgit4kNYehBMU5')], 'active_agent': 'editor'}, goto='editor')" + } + }, + { + "key": "exception.stacktrace", + "value": { + "stringValue": "Exception: ParentCommand(Command(graph='red_team:15ccef64-4162-9e02-e87c-4cb0eb6ea16b', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', id='389058d1-083f-4632-bbc5-09bfd3a0ffd2', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9'), AIMessage(content=[{'signature': 'CAQS1QgKEAgSGAI4AUIIdGhpbmtpbmcSDN6TfU6NDM971lEbzxoMvRUA1J9ox3fJQYT9IjArCQQGHBP4pRHPavXzRlbMbaYHC57Yj+Gbxgr0helvd2OIL/o6Rbt4PFA3FaIFUlgq8gc7KQI9FtGnvSAkLBRzIsUp/2d5Rd2zgEsfkFVeXsn8WYYZkinAQVVy6A7B3LaEJW0xb/x88Di67fedwx0Q+FlZtwcmT3Dcaa11JveyHRr3dm+zGSZP6CXeXNJ8nrTZ7W1YRTysD6w7o7Lp8h1Rwq4f4o8j+QUfY0vje+zJZsxHPmQNt3ykeKh99hgn6dbiz6/EHqmbHjSAu1M4SqEDfqIKThHYenamAZmcQ4XAOo6mdQ6+77VgpV1lC1/k52IiUoJjkGlQrnLCvutiI7KN+sgK8c2+2KCJrPRCvVV+BkStvP5jWWqzytpKQ+nHzhL448OrIA9gdJC39C8eDrXeRnI7t4FqlkXGoMlZAV9UMcrQHRxOMi8tOyTzi9oyBFDTSsQ+LJqer0tz1Ft9gek6vzg50SpVAFQdLERv8R1YqMaeI+xsvTAEW9MRgvRMkzLuzRT2guw3jD+Fjip37xgK5QI0R+CsOAmQPoTZizzcIyAUAuCJFCgq/gD/CibLxoymiVUI1an9Snt3KpNRoWdvWIne33V0pv8PHg9yW2JuLOqQGilLCsK/Q3oQr3upEMXxXGjCL7WUizvRZGFEEV+VplYt7v4tKZMj5pk3vdeM0MJ5XZg7PvUsRzycxcnrT3Knoc3qM+ttqZfTsb8d6U/pQifq7GuahFOHrvroqgJLj+/81Yy1pZFkjfJgY0XklzAnA6CFq/YT0gxbV+TH5IS+WaYOUzK36S8s5qqMwhngv4IzE3ctXXqnhIZXNznyVBqbYa/Gx2bCjcEj2n5RyGYRS/+MculgLBznQ9MkG2D7QP3Mq/nwHXKMIaLS8VK8hG3d0rAjlvnauXShNrJxtkj4qkwnxD4LrEjk1ue+PLcbLqYWwAr8n0s+41jrsIPVyyK9vFaEZrA9KaBxkJyiqx50GZNn7ckx+mkI18gH52iyVPtU8FKMXjugCl6mPLiHYiDQd+G63LudmOKys9itVnBDBNMP2b/I7+bIVRlheGaZWr6MATKl86B3y+0kuOrLRMyWcdibc2iQkZm+8f7dTUwbKbBJsKOFY73/8nlL/JiYpjz6nUIk+IqSjaFyK6XFlCcukWpWHpDJnSvbEzey7qHyCqJ+I+on8iP8GOtempQaS0/4kZGSwlrE6xOj6EwGpfjbDZKHkP/2RAdNYPQq3z7bdVJMJNnFMz6e37mzMpCBihwPdSFTRE3SCY66wvTgJzGCUyhHbSi5i1azzOXd8uHCSpT/6L1o6VU0pjoR8Kaa4wCWHGf7P/WEd+avyp02Jnu1cmpW0+Gq+vK9h0tdb2IClM/eNfdouQP+ep8MJUjraDRICmAJQY5obS71vniOo+reaFy0zRgB', 'thinking': '', 'type': 'thinking'}, {'signature': 'CAQS/REKEQgSGAI4AUIJbmFycmF0aW9uEgwFh3AVJZP3s8y1Hq0aDG76uhHONaScXAGShiIwGgjLXHUx1yZrkLPljjzTOQycpLJiKVwXcxm/EzKJm1s8M5WxuREWEldXcoaN0/msKpkRMT+eLb5ih9rAHULTomUNsUwJeu0jrJe4GLazy7sP5KtjEOEBKQsXUfoQ7fDkZOQUHZi9zzQN4hFNmYJUf3OVqf9TQNdC1+m//HAZPfWmAy6HtDvm7kgBJkCEoiFnm7tn/PX1hXGPRgWc6BTXUMr2WA8aKyqggRUbMH2+JYv5iWZddCc1up1Nbr3sF7WPcot9ZBhyHQXr692zeJk6SJgygIVrASbMly2bBxTWJB623gVgzpCmVSLZFpkKXgPcp8hGyDKOvF6SiQSHqGRxQofr7Vs1aPhfZixROJGD+MlrkggVAnGMyYIDUZpavtCeiU1JmBSz0084bXPtT/tnpsKOnSPYZQo3B0BXKbqA/WsKng8/V3Z8Uuajo6IVZ2hgA1WDduqQOQkExGU6pFuLhebIjuWxSNI/DffvR//LNqCfvdnf0hvoPtDxfx/SawIx4/spq9xbwFzHtLgoHg1F2wFw7clPDlg1PyD/v2yJG3V9T8GL6jGPE6rhfil/u4HNvf7BW0dQzCTTXcqZy4nByy0+p+S7ZxfMl4Kpxa2gOhhSNjfpfuX0QpL4kME8J1nliFN1d8KqEPuDI18BQHUhD/UfY29oA0x510FjM/BsLZ4n5sppkoIi7CFoQq2aAKVNfC1KilhIpRD+1dyDtZQklN8gZpOESHw6ML1TtU0yfAcbmqoxJ6hhjzwH6CMsTQEEy8MbRBM+QKKkIRdRCPHGFW2LkuR50+J0pF/tr6pqVvJeozfiMnrEilB27TZP4nKTJG8rd//vCWen0y7CA/8dBJ9gdp5tx7NVA45k1gdcxER1gYouNX3n1vG3/B1F4+93wXW9Mqo6Us59TQmrb9Nqa7pSQNSrH06w1GxyBAlkFfqAlafk16aMpq416TJYpGy43WCUiCcLWlhiWgz2sgOLAXP9a5weWAKRC8J6jkcUkDeXnW+HFSiPM6+9Bo6kVhLUhN5rc4mHZOZpF7ptPpsQN4XCqLPsqpRH/8o5XpyC6ZneTeF2ExtjhwZVjXDwMbIzVYIbw1y2PPGw8dC7pc4sIVSnI4oQAH0v650ghfhmMevWW4h4Pylbw/DG4fdeT3+8z0f8MOSTtfipx05/QttHlAvODRxa5yDCvOz6n3f1BO0N/4MbGyOM27qW4x8mBVoryUZWfsmhfi0/GfNvB8+ylTGbYNX27OcEF5WZtNqbWEy7V5GbmZUSxspcg7FBZ3j3/3L7muvZXoZi1f5erq4y3lSH+oF7qPFnvieyLOCwubcQy+oyowj4cD0idRecnp+s+52D7+mzF1gpW+f+cJasoxigbxkOloS4yeiKvgM4nWSpGEj12RPx5o80EmziXN9t4zJzW1htA5hXJJeFympMYtyCUCevRWVmrRYktQ7UnCa6tL/AVPi1ni5XCXHfLOuqfK5KLYxjwVtdhhfmCLvpaKVNCEOCf4nNGHqD98wUb0ejWNwDYBnJGkBRw9RVspfwNdoWoVcn2qv8ywGUp7yc/sk2LpI6VziRyvWqbGNG1ZJqVVutRc46+sOE4VSeS2/CTNKNa6NMCf/hxxaUVsYfKdP+S1Lc4ianKp0sUugPpmtoydC0CP2RwjnHLcnzxCnvqxlLQWXMNPTsu2yBcHlWJLtcmdg/McLagy7l5kG6sN7ZxJLD/kAx8E6uqohP+K/jMSrYuQJnhr4tf4biWFY3Jzf/KpAYyVSq+pbfFKXMbL9nHEoZQT15Wyabd6DMAq3QZOthM5pUzdNqUXkcrI1PwjASAg4dVU/2o+98mTXnyhP7VlEkTnddpwyJF+YCCc01pqa6ZX99pnR+YOpIrb5DtPcKq0t5cFCDyQxN1lUuqZcYujvX/GC7+6IpRTz/NkEY/5cntOfZJyd6SkUx/JR5AJIRPCNeib0rcWnnFn0XK8s22x40wLjEtufXDO9+yraf4kHxlBI3SB0meMjepX50CJhAx7NekkRFnwoecLfqIyrjXrgAZsTMPiX94snjdrzisAXtBKanulilSxOfW2HSmfnk0L2GXHJJ3S+vCbgREml9dekfmHhmwEYtv3kfhth7ERIwVT8j69OtIBCmeTIJuA7o73PXc1LJ2whuZK7FZe6NTOs61MP7JVlIc6tbhtOBk3NXU8pBPOCY6Q55d63FtpUU/KCbQ0K2WQ9o5jNGwNNJ6Cw5FDCt+x5YJeP1d2QzDSiq+cSZqwqCZVcNoAftGGzgrRZ4vkpbP0xQZHCtWsUwHVTjrY4NfRDjHdVPRn8J9Xtooc/hIWkqzFaczYTlapu2RCJqe8aEo3jzWZCEJ7AiBzBYraYDgOfeiQyeJUsbbXHdGKiiw22Bdp4M+Yz671MZ0ie9EWRks0Q2DHGUTne9S4tAAJPGbUf4xtCMoJ40Q4QW05QLSZy3wNFAUFYmXE4ECDFB8+IEeNFFF/tgAnfh8GvFh/1xY+LE+ozMminjSRwhq09zQAEkNFyu0IKgvDkyOI+iccF7doWLTUasPxLH1xGPBSKkeWCnSoX/4C2c9du66BbRXamHqae+VCiS6BN953iYKOu8rMkneW+TFjV0CzAf8PNG9LBayUgY5Uw9x0Sap6zI4PpaZKH4d9ktkFU+91yBbN5SRtdd8/EQatJXmBNfw7kRrll+LjwZweWLEb0jT1RpGuDqHLdLTMMgowSczrp2zCsJdQH/seaYrpwkCFR8dRjM7JRUkVa1A0SvdZAX75vuBvvLBzN2c+Rbt5oqy93lqIBIjKb7w2UkRTb4pBWcNKNPUgt3az/PxQxEasaltdmuyvsXYbur3VPNn8JzN/6J5N+QCnz571TccapCmkPvFE+gayXU+UF1VKYyFcUvlev7lsQlmjqzK2QA8X7Q3TzFapqhvJKlt7dzBHraqZBgPgb9N4oKNayi8FZT71+kaYhIhFrESKYRHfHkj5n3kfhgLy+x+jLDbKxMggkgHnJz9RuEHZXBWtkYAQ==', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_verifier', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAMRdS21FeJfzQhM3s', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7396, 'output_tokens': 770, 'output_tokens_details': {'thinking_tokens': 739}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='skeptic', id='lc_run--01a0fe03-4ae9-76c0-89a6-4f0b114e3030-0', tool_calls=[{'name': 'transfer_to_verifier', 'args': {}, 'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7396, 'output_tokens': 770, 'total_tokens': 8166, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 739}}), ToolMessage(content='Successfully transferred to verifier', name='transfer_to_verifier', id='c064563a-974f-4f0f-ab4a-080e63c01cf5', tool_call_id='toolu_01H6fV8JvRJNvXy47eRnd3jH'), AIMessage(content=[{'signature': 'CAQS9gUKEAgSGAI4AUIIdGhpbmtpbmcSDGFyJ1MXoY+Ai6NkbxoMRd2TrUfUU7WrPpA2IjDgy8dUplcZKc7DnJwNLTBfH5UNR+nGnakSPQia8UVIzrjkOnJ2fLrWUsA2oEO8yRsqkwXkkDCsyhB97dwj57RMt91XzgWhL9luNEGO92yW0rNFGoMSUVOxPIrFc5RjyrhKEc3+1RnpxCkylq+LNlQ5NFxLn6v2cF5DzqpvBHNR7XAig+urJIU3gXydCkaGZZDetJtQOUOXgZi9z9100NpdAEkXDo2ICHCGTGpVc0J/pbXxWRBxb6B1/Gl0Yjn68V6HlesH7IoC7GR/3HwRu9NSDBG4ttfQjEWcg+PeAea4Rn7jzx+Tm9H7L1joTKVgDbQg5iKMLfjvPqlnksCLtXOt8jSIS5OYLN7a/7TX6/w5d34vnw8vPDmU1UYi0Q72qUWhUa2vw8OXNpcKL7Bp0lNfBS8rvHY+vBY8/haAoTL3wvLFU1fT+4z/vA3BcsigZDYlfJeDYcxXmRuqxzxEGpXCRe9s1HWtID+41pHu6lpZHkXaghpc6TZHLxNwcVhTb6fMQ7Qo+2L6Pvklh1m32peodxeg2YiPhdpl3w/xkOEu5/JEF7ZBxpdUL+u3wkO5pUlsqfgX7uhzv7HC2WpSWVimrwiQwQOUx0+JPrLtw+lYfq7I3jruo0GW0guWvnuf7JM3YLj84q+JgHomnSSZnnZmrL0UaRvrVyLo877F39ccLS7XoZnnui8J4P58ZTmiLkEJeGcx1Iuag2e33339O7Yew/59tLbQtIGh9VWMwHBnFx39S8/4LEFrK5HldPQhOrloyTtkRs66iQRKtxMKS0VI6QTKZDAbs7a7H1yBv+mJi9Xg4uKmyU6qnn7zEKjjY2bIlCwezx4LLGP5HD3Zr6fZsmdG1UHzDsBvYG2oCI0oqEKM+valsT6hU8MRjbQBD/JJ8JJN/iBGlhBAUHo0UmhhtR2euTVpZWlMnhmpXAdKDXSfPdaPohgB', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_red_team', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAsabDquegHi6YUpCn', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7509, 'output_tokens': 88, 'output_tokens_details': {'thinking_tokens': 55}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='verifier', id='lc_run--01a0fe03-6682-7d21-8c15-ffbce9422c9f-0', tool_calls=[{'name': 'transfer_to_red_team', 'args': {}, 'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7509, 'output_tokens': 88, 'total_tokens': 7597, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 55}}), ToolMessage(content='Successfully transferred to red_team', name='transfer_to_red_team', id='982c0126-e6ab-458a-922e-7b266e83c2b2', tool_call_id='toolu_01XEAuhQHCBdyCwtymZwFfDi'), AIMessage(content=[{'signature': 'CAQSwgcKEAgSGAI4AUIIdGhpbmtpbmcSDORXge9uL1832RMQwBoMjQwAwAHb9iP1ojPGIjDDScal486beCJbtG8/D2Y9g9S5xIPfjoHg/dxwX5HypSQEBbjFiTSbPECXSFzV6YIq3wbLcmyPbvusGnDexFgx+3sIXzvqyFV/69xhihMnSpfTCHy0+0zSu1WpYU/WUpIfoaSXTcTa1qkM8x8Ek/y/1jkKMZUfDFa93uhUJxOC4+QgPWBl/+Xy2t82L2x/ebq7QLQTqtR0jXeaURQm3Co6JvOgWmDvMpEdTeTWE3fDh1tzzVKsflum4d89b6CaKtn8nlQd7RhN99vNQMQHG5V4OuVNdrHPZZpFOuhdejsMFWXyB+55slGAd8wEXgFKbI/knOXVGzL/NDF8Gty1tNZlu2fdSCAsYWMsFQP1cbwLeQGszyXzJMVZJ2I9xBPkyz1HOXCPyh5F+j2gRTPikVB3ARC6tfB/lDSUeKBVZPQS8pY/B4eCs+IjjaBNWruLzfqCsc6xL0CuQPSYyQXaYFjYrSRNcC70WQBWT5VVhp0mA9yZDGYbRZt9I5OK9QkQmuJ5zDFaMEOXzSrsC4lteP6MdimiMijvsH3iq+c6WUYolRj3neI88EaHIzLAGRq64BNCLzdAYL2pEU2sb5uG78PKVoUUCbVJL5jpDOhLOTO1bQsTuoUpnRRrLQD/kIzV+ljpVBwwfJcyq75TJA/CtPr/xr2SZ9B1b2sbjG6jD0lhtoCddeDgcqgl/LSSkcuaYY8xPWCy+LtNhcqThfmfvnHLJIpL6gjbjW/OQpRfVUwfw1jOb9bWVwcgHhNYfKYGPtM8oth5xwLYRRVgzqMKOvs7falXRhVzZ2SP6Iw0BMmAAZ6HjfCl9L+tPqJNlnQQOHpoWjum4B+XsYldFEKGQWQqsd59iNPvao9p/sCOt7rGqCozcmgiEm4w66ez/QIDyb46n0+tcNJPUcoBKvQ/jDwE6VwmkQ/nECjFp3UdDT+kWYQSNd8j8VBZOpG8LlqUYrEZCv0q7Bs1vcNrdYiaFoS32osaynWk+Ht3OHuFWbEF8JlsFTQAO05VvYkecf0V/oTHsG5hyOOs7UEaDFq7uaJuzWVhh5pAojIaBcR8fVln2PLRTX7is2nsA+QrdidePcZtFtgxAiKYe1i2sSFi4vm8EOkCbQ63wdQeY4Urr5PuoMEwxUULjgKagiAmsJDq4RrVm7sO4+6YuA9j6l2uylkOEc67vf/5fkXuCsiApNL6keb8c+vL0n4FXGdzj+apSDfFGBgB', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01VBrWdWEWYgit4kNYehBMU5', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_editor', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAzfjqUopoEmZLaqm1', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7416, 'output_tokens': 151, 'output_tokens_details': {'thinking_tokens': 119}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='red_team', id='lc_run--01a0fe03-6ce6-7251-b796-4f64333f67c6-0', tool_calls=[{'name': 'transfer_to_editor', 'args': {}, 'id': 'toolu_01VBrWdWEWYgit4kNYehBMU5', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7416, 'output_tokens': 151, 'total_tokens': 7567, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 119}}), ToolMessage(content='Successfully transferred to editor', name='transfer_to_editor', tool_call_id='toolu_01VBrWdWEWYgit4kNYehBMU5')], 'active_agent': 'editor'}, goto='editor'))Traceback (most recent call last):\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 707, in invoke\n input = context.run(step.invoke, input, config, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 3913, in invoke\n for chunk in self.stream(\n ~~~~~~~~~~~^\n input,\n ^^^^^^\n ...<11 lines>...\n **kwargs,\n ^^^^^^^^^\n ):\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/main.py\", line 2967, in stream\n for _ in runner.tick(\n ~~~~~~~~~~~^\n [t for t in loop.tasks.values() if not t.writes],\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ...<2 lines>...\n schedule_task=loop.accept_push,\n ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^\n ):\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_runner.py\", line 207, in tick\n run_with_retry(\n ~~~~~~~~~~~~~~^\n t,\n ^^\n ...<10 lines>...\n },\n ^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_retry.py\", line 617, in run_with_retry\n return task.proc.invoke(task.input, config)\n ~~~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 709, in invoke\n input = step.invoke(input, config)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/_internal/_runnable.py\", line 447, in invoke\n ret = self.func(*args, **kwargs)\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 84, in _write\n self.do_write(\n ~~~~~~~~~~~~~^\n config,\n ^^^^^^^\n writes,\n ^^^^^^^\n )\n ^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 126, in do_write\n write(_assemble_writes(writes))\n ~~~~~~~~~~~~~~~~^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/pregel/_write.py\", line 181, in _assemble_writes\n if ww := w.mapper(w.value):\n ~~~~~~~~^^^^^^^^^\n\n\n File \"/workspace/deeplite/.venv/lib/python3.13/site-packages/langgraph/graph/state.py\", line 1762, in _control_branch\n raise ParentCommand(command)\n\n\nlanggraph.errors.ParentCommand: Command(graph='red_team:15ccef64-4162-9e02-e87c-4cb0eb6ea16b', update={'messages': [HumanMessage(content='For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions.', additional_kwargs={}, response_metadata={}, id='46204305-f8ee-4384-9ce8-fcd80bfcdf75'), AIMessage(content=[{'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'caller': {'type': 'direct'}, 'input': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'name': 'web_search', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011Cfdw9aW9brenPqgybfNwP', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 2845, 'output_tokens': 73, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-21a3-7000-a381-284a6a0c03f7-0', tool_calls=[{'name': 'web_search', 'args': {'query': 'docs.python.org tuples immutable sequences lists mutable data structures tutorial'}, 'id': 'toolu_01B2LfHGDjYSFJhmiXNj4CuQ', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 2845, 'output_tokens': 73, 'total_tokens': 2918, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='[{\"title\": \"5. Data Structures \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like`insert`,`remove` or`sort` that only modify the list have no return value printed \\\\u2013 they return the default`None`. [1] This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n>>> t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n>>> v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of namedtuples). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like append() and extend().\"]}, {\"title\": \"3. Data model \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/reference/datamodel.html\", \"highlights\": [\"The value of some objects can change. Objects whose value can change are said to be mutable; objects whose value is unchangeable once they are created are called immutable. (The value of an immutable container object that contains a reference to a mutable object can change when the latter\\\\u2019s value is changed; however the container is still considered immutable, because the collection of objects it contains cannot be changed. So, immutability is not strictly the same as having an unchangeable value, it is more subtle.) An object\\\\u2019s mutability is determined by its type; for instance, numbers, strings and tuples are immutable, while dictionaries and lists are mutable.\\\\n...\\\\nSome objects contain references to other objects; these are called containers. Examples of containers are tuples, lists and dictionaries. The references are part of a container\\\\u2019s value. In most cases, when we talk about the value of a container, we imply the values, not the identities of the contained objects; however, when we talk about the mutability of a container, only the identities of the immediately contained objects are implied. So, if an immutable container (like a tuple) contains a reference to a mutable object, its value changes if that mutable object is changed.\\\\n...\\\\n### 3.2.5. Sequences\\\\u00b6\\\\n...\\\\n#### 3.2.5.1. Immutable sequences\\\\u00b6\\\\n\\\\nAn object of an immutable sequence type cannot change once it is created. (If the object contains references to other objects, these other objects may be mutable and may be changed; however, the collection of objects directly referenced by an immutable object cannot change.)\\\\n\\\\nThe following types are immutable sequences:\\\\n...\\\\nTuples\\\\n: The items of a `tuple` are arbitrary Python objects. Tuples of two or more items are formed by comma-separated lists of expressions. A tuple of one item (a \\\\u2018singleton\\\\u2019) can be formed by affixing a comma to an expression (an expression by itself does not create a tuple, since parentheses must be usable for grouping of expressions). An empty tuple can be formed by an empty pair of parentheses.\\\\n...\\\\n#### 3.2.5.2. Mutable sequences\\\\u00b6\\\\n\\\\nMutable sequences can be changed after they are created. The subscription and slicing notations can be used as the target of assignment and `del` (delete) statements.\\\\n...\\\\nThere are currently two intrinsic mutable sequence types:\\\\n\\\\nLists\\\\n: The items of a list are arbitrary Python objects. Lists are formed by placing a comma-separated list of expressions in square brackets. (Note that there are no special cases needed to form lists of length 0 or 1.)\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.7 documentation\", \"url\": \"https://docs.python.org/3/builtins/stdtypes.html\", \"highlights\": [\"collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and range objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... mutable and immutable. The `collections. ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\nmutable sequence types is\\\\n...\\\\nsupport allows immutable sequences, ... , to be used as `dict` keys and stored in ... enset` instances\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\ntuple(iterable=\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"Built-in Types \\\\u2014 Python 3.14.5 documentation\", \"url\": \"https://docs.python.org/3/library/stdtypes.html\", \"highlights\": [\"Some collection classes are mutable. The methods that add, subtract, or rearrange their ... in place, and don\\\\u2019t return a ... , never return the collection instance itself but `None`.\\\\n...\\\\n## Sequence Types \\\\u2014 `list`, `tuple`, `range`\\\\u00b6\\\\n\\\\nThere are three basic sequence types: lists, tuples, and ... objects. Additional sequence types tailored for processing of binary data and text strings are described in dedicated sections.\\\\n...\\\\nThe operations in the following table ... immutable. The ` ... is provided to make ... easier to correctly implement these operations on custom sequence types\\\\n...\\\\n### Immutable Sequence Types\\\\u00b6\\\\n...\\\\noperation that immutable sequence ... by mutable sequence types is ... `hash()`\\\\n...\\\\nThis support allows immutable sequences, such as `tuple` instances, to be used as `dict` keys and stored in `set` and `frozenset` instances.\\\\n...\\\\n### Mutable Sequence Types\\\\u00b6\\\\n...\\\\n### Lists\\\\u00b6\\\\n\\\\nLists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).\\\\n...\\\\n### Tuples\\\\u00b6\\\\n\\\\nTuples are immutable sequences, typically used to store collections of heterogeneous data (such as the 2-tuples produced by the `enumerate()` built-in). Tuples are also used for cases where an immutable sequence of homogeneous data is needed (such as allowing storage in a `set` or `dict` instance).\\\\n...\\\\nThe constructor builds a tuple whose items are the same and in the same order as iterable\\\\u2019s items. iterable may be either a sequence, a container that supports iteration, or an iterator object. If iterable is already a tuple, it is returned unchanged. For example, `tuple(\\'abc\\')` returns `(\\'a\\', \\'b\\', \\'c\\')` and `tuple( [1, 2, 3] )` returns `(1, 2, 3)`. If no argument is given, the constructor creates a new empty tuple, `()`.\\\\n...\\\\nTuples implement all of the common sequence operations.\\\\n...\\\\nFor heterogeneous collections of data where access by name is clearer than access by index, `collections.namedtuple()` may be a more appropriate choice than a simple tuple object.\"]}, {\"title\": \"5. Data Structures \\\\u2014 Python 3.10.20 documentation\", \"url\": \"https://docs.python.org/3.10/tutorial/datastructures.html\", \"highlights\": [\"You might have noticed that methods like `insert`, `remove` or `sort` that only modify the list have no return value printed \\\\u2013 they return the default `None`. 1 This is a design principle for all mutable data structures in Python.\\\\n...\\\\n## 5.3. Tuples and Sequences\\\\u00b6\\\\n\\\\nWe saw that lists and strings have many common properties, such as indexing and slicing operations. They are two examples of sequence data types (see Sequence Types \\\\u2014 list, tuple, range). Since Python is an evolving language, other sequence data types may be added. There is also another standard sequence data type: the tuple.\\\\n\\\\nA tuple consists of a number of values separated by commas, for instance:\\\\n...\\\\n>>> # Tuples are immutable:\\\\n... t[0] = 88888\\\\nTraceback (most recent call last):\\\\n File \\\\\"\\\\\", line 1, in \\\\nTypeError: \\'tuple\\' object does not support item assignment\\\\n>>> # but they can contain mutable objects:\\\\n... v = ([1, 2, 3], [3, 2, 1])\\\\n>>> v\\\\n([1, 2, 3], [3, 2, 1])\\\\n...\\\\nAs you see, on output tuples are always enclosed in parentheses, so that nested tuples are interpreted correctly; they may be input with or without surrounding parentheses, although often parentheses are necessary anyway (if the tuple is part of a larger expression). It is not possible to assign to the individual items of a tuple, however it is possible to create tuples which contain mutable objects, such as lists.\\\\n\\\\nThough tuples may seem similar to lists, they are often used in different situations and for different purposes. Tuples are immutable, and usually contain a heterogeneous sequence of elements that are accessed via unpacking (see later in this section) or indexing (or even by attribute in the case of `namedtuples`). Lists are mutable, and their elements are usually homogeneous and are accessed by iterating over the list.\\\\n...\\\\n## 5.4.\\\\n...\\\\n. Set objects\\\\n...\\\\n## 5.5. Dictionaries\\\\u00b6\\\\n\\\\nAnother useful data type built into Python is the dictionary (see Mapping Types \\\\u2014 dict). Dictionaries are sometimes found in other languages as \\\\u201cassociative memories\\\\u201d or \\\\u201cassociative arrays\\\\u201d. Unlike sequences, which are indexed by a range of numbers, dictionaries are indexed by keys, which can be any immutable type; strings and numbers can always be keys. Tuples can be used as keys if they contain only strings, numbers, or tuples; if a tuple contains any mutable object either directly or indirectly, it cannot be used as a key. You can\\\\u2019t use lists as keys, since lists can be modified in place using index assignments, slice assignments, or methods like `append()` and `extend()`.\"]}]', name='web_search', id='6e5125ac-3164-4a76-9c52-ac3460ce36e4', tool_call_id='toolu_01B2LfHGDjYSFJhmiXNj4CuQ'), AIMessage(content=[{'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_skeptic', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwA6F4cCN5yhD4Crs9M', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7378, 'output_tokens': 33, 'output_tokens_details': {'thinking_tokens': 0}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='researcher', id='lc_run--01a0fe03-3cd8-79f1-88a1-ce6cee294e79-0', tool_calls=[{'name': 'transfer_to_skeptic', 'args': {}, 'id': 'toolu_01Xjj3aRdWyhBJfPmTJBxQa9', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7378, 'output_tokens': 33, 'total_tokens': 7411, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 0}}), ToolMessage(content='Successfully transferred to skeptic', name='transfer_to_skeptic', id='389058d1-083f-4632-bbc5-09bfd3a0ffd2', tool_call_id='toolu_01Xjj3aRdWyhBJfPmTJBxQa9'), AIMessage(content=[{'signature': 'CAQS1QgKEAgSGAI4AUIIdGhpbmtpbmcSDN6TfU6NDM971lEbzxoMvRUA1J9ox3fJQYT9IjArCQQGHBP4pRHPavXzRlbMbaYHC57Yj+Gbxgr0helvd2OIL/o6Rbt4PFA3FaIFUlgq8gc7KQI9FtGnvSAkLBRzIsUp/2d5Rd2zgEsfkFVeXsn8WYYZkinAQVVy6A7B3LaEJW0xb/x88Di67fedwx0Q+FlZtwcmT3Dcaa11JveyHRr3dm+zGSZP6CXeXNJ8nrTZ7W1YRTysD6w7o7Lp8h1Rwq4f4o8j+QUfY0vje+zJZsxHPmQNt3ykeKh99hgn6dbiz6/EHqmbHjSAu1M4SqEDfqIKThHYenamAZmcQ4XAOo6mdQ6+77VgpV1lC1/k52IiUoJjkGlQrnLCvutiI7KN+sgK8c2+2KCJrPRCvVV+BkStvP5jWWqzytpKQ+nHzhL448OrIA9gdJC39C8eDrXeRnI7t4FqlkXGoMlZAV9UMcrQHRxOMi8tOyTzi9oyBFDTSsQ+LJqer0tz1Ft9gek6vzg50SpVAFQdLERv8R1YqMaeI+xsvTAEW9MRgvRMkzLuzRT2guw3jD+Fjip37xgK5QI0R+CsOAmQPoTZizzcIyAUAuCJFCgq/gD/CibLxoymiVUI1an9Snt3KpNRoWdvWIne33V0pv8PHg9yW2JuLOqQGilLCsK/Q3oQr3upEMXxXGjCL7WUizvRZGFEEV+VplYt7v4tKZMj5pk3vdeM0MJ5XZg7PvUsRzycxcnrT3Knoc3qM+ttqZfTsb8d6U/pQifq7GuahFOHrvroqgJLj+/81Yy1pZFkjfJgY0XklzAnA6CFq/YT0gxbV+TH5IS+WaYOUzK36S8s5qqMwhngv4IzE3ctXXqnhIZXNznyVBqbYa/Gx2bCjcEj2n5RyGYRS/+MculgLBznQ9MkG2D7QP3Mq/nwHXKMIaLS8VK8hG3d0rAjlvnauXShNrJxtkj4qkwnxD4LrEjk1ue+PLcbLqYWwAr8n0s+41jrsIPVyyK9vFaEZrA9KaBxkJyiqx50GZNn7ckx+mkI18gH52iyVPtU8FKMXjugCl6mPLiHYiDQd+G63LudmOKys9itVnBDBNMP2b/I7+bIVRlheGaZWr6MATKl86B3y+0kuOrLRMyWcdibc2iQkZm+8f7dTUwbKbBJsKOFY73/8nlL/JiYpjz6nUIk+IqSjaFyK6XFlCcukWpWHpDJnSvbEzey7qHyCqJ+I+on8iP8GOtempQaS0/4kZGSwlrE6xOj6EwGpfjbDZKHkP/2RAdNYPQq3z7bdVJMJNnFMz6e37mzMpCBihwPdSFTRE3SCY66wvTgJzGCUyhHbSi5i1azzOXd8uHCSpT/6L1o6VU0pjoR8Kaa4wCWHGf7P/WEd+avyp02Jnu1cmpW0+Gq+vK9h0tdb2IClM/eNfdouQP+ep8MJUjraDRICmAJQY5obS71vniOo+reaFy0zRgB', 'thinking': '', 'type': 'thinking'}, {'signature': 'CAQS/REKEQgSGAI4AUIJbmFycmF0aW9uEgwFh3AVJZP3s8y1Hq0aDG76uhHONaScXAGShiIwGgjLXHUx1yZrkLPljjzTOQycpLJiKVwXcxm/EzKJm1s8M5WxuREWEldXcoaN0/msKpkRMT+eLb5ih9rAHULTomUNsUwJeu0jrJe4GLazy7sP5KtjEOEBKQsXUfoQ7fDkZOQUHZi9zzQN4hFNmYJUf3OVqf9TQNdC1+m//HAZPfWmAy6HtDvm7kgBJkCEoiFnm7tn/PX1hXGPRgWc6BTXUMr2WA8aKyqggRUbMH2+JYv5iWZddCc1up1Nbr3sF7WPcot9ZBhyHQXr692zeJk6SJgygIVrASbMly2bBxTWJB623gVgzpCmVSLZFpkKXgPcp8hGyDKOvF6SiQSHqGRxQofr7Vs1aPhfZixROJGD+MlrkggVAnGMyYIDUZpavtCeiU1JmBSz0084bXPtT/tnpsKOnSPYZQo3B0BXKbqA/WsKng8/V3Z8Uuajo6IVZ2hgA1WDduqQOQkExGU6pFuLhebIjuWxSNI/DffvR//LNqCfvdnf0hvoPtDxfx/SawIx4/spq9xbwFzHtLgoHg1F2wFw7clPDlg1PyD/v2yJG3V9T8GL6jGPE6rhfil/u4HNvf7BW0dQzCTTXcqZy4nByy0+p+S7ZxfMl4Kpxa2gOhhSNjfpfuX0QpL4kME8J1nliFN1d8KqEPuDI18BQHUhD/UfY29oA0x510FjM/BsLZ4n5sppkoIi7CFoQq2aAKVNfC1KilhIpRD+1dyDtZQklN8gZpOESHw6ML1TtU0yfAcbmqoxJ6hhjzwH6CMsTQEEy8MbRBM+QKKkIRdRCPHGFW2LkuR50+J0pF/tr6pqVvJeozfiMnrEilB27TZP4nKTJG8rd//vCWen0y7CA/8dBJ9gdp5tx7NVA45k1gdcxER1gYouNX3n1vG3/B1F4+93wXW9Mqo6Us59TQmrb9Nqa7pSQNSrH06w1GxyBAlkFfqAlafk16aMpq416TJYpGy43WCUiCcLWlhiWgz2sgOLAXP9a5weWAKRC8J6jkcUkDeXnW+HFSiPM6+9Bo6kVhLUhN5rc4mHZOZpF7ptPpsQN4XCqLPsqpRH/8o5XpyC6ZneTeF2ExtjhwZVjXDwMbIzVYIbw1y2PPGw8dC7pc4sIVSnI4oQAH0v650ghfhmMevWW4h4Pylbw/DG4fdeT3+8z0f8MOSTtfipx05/QttHlAvODRxa5yDCvOz6n3f1BO0N/4MbGyOM27qW4x8mBVoryUZWfsmhfi0/GfNvB8+ylTGbYNX27OcEF5WZtNqbWEy7V5GbmZUSxspcg7FBZ3j3/3L7muvZXoZi1f5erq4y3lSH+oF7qPFnvieyLOCwubcQy+oyowj4cD0idRecnp+s+52D7+mzF1gpW+f+cJasoxigbxkOloS4yeiKvgM4nWSpGEj12RPx5o80EmziXN9t4zJzW1htA5hXJJeFympMYtyCUCevRWVmrRYktQ7UnCa6tL/AVPi1ni5XCXHfLOuqfK5KLYxjwVtdhhfmCLvpaKVNCEOCf4nNGHqD98wUb0ejWNwDYBnJGkBRw9RVspfwNdoWoVcn2qv8ywGUp7yc/sk2LpI6VziRyvWqbGNG1ZJqVVutRc46+sOE4VSeS2/CTNKNa6NMCf/hxxaUVsYfKdP+S1Lc4ianKp0sUugPpmtoydC0CP2RwjnHLcnzxCnvqxlLQWXMNPTsu2yBcHlWJLtcmdg/McLagy7l5kG6sN7ZxJLD/kAx8E6uqohP+K/jMSrYuQJnhr4tf4biWFY3Jzf/KpAYyVSq+pbfFKXMbL9nHEoZQT15Wyabd6DMAq3QZOthM5pUzdNqUXkcrI1PwjASAg4dVU/2o+98mTXnyhP7VlEkTnddpwyJF+YCCc01pqa6ZX99pnR+YOpIrb5DtPcKq0t5cFCDyQxN1lUuqZcYujvX/GC7+6IpRTz/NkEY/5cntOfZJyd6SkUx/JR5AJIRPCNeib0rcWnnFn0XK8s22x40wLjEtufXDO9+yraf4kHxlBI3SB0meMjepX50CJhAx7NekkRFnwoecLfqIyrjXrgAZsTMPiX94snjdrzisAXtBKanulilSxOfW2HSmfnk0L2GXHJJ3S+vCbgREml9dekfmHhmwEYtv3kfhth7ERIwVT8j69OtIBCmeTIJuA7o73PXc1LJ2whuZK7FZe6NTOs61MP7JVlIc6tbhtOBk3NXU8pBPOCY6Q55d63FtpUU/KCbQ0K2WQ9o5jNGwNNJ6Cw5FDCt+x5YJeP1d2QzDSiq+cSZqwqCZVcNoAftGGzgrRZ4vkpbP0xQZHCtWsUwHVTjrY4NfRDjHdVPRn8J9Xtooc/hIWkqzFaczYTlapu2RCJqe8aEo3jzWZCEJ7AiBzBYraYDgOfeiQyeJUsbbXHdGKiiw22Bdp4M+Yz671MZ0ie9EWRks0Q2DHGUTne9S4tAAJPGbUf4xtCMoJ40Q4QW05QLSZy3wNFAUFYmXE4ECDFB8+IEeNFFF/tgAnfh8GvFh/1xY+LE+ozMminjSRwhq09zQAEkNFyu0IKgvDkyOI+iccF7doWLTUasPxLH1xGPBSKkeWCnSoX/4C2c9du66BbRXamHqae+VCiS6BN953iYKOu8rMkneW+TFjV0CzAf8PNG9LBayUgY5Uw9x0Sap6zI4PpaZKH4d9ktkFU+91yBbN5SRtdd8/EQatJXmBNfw7kRrll+LjwZweWLEb0jT1RpGuDqHLdLTMMgowSczrp2zCsJdQH/seaYrpwkCFR8dRjM7JRUkVa1A0SvdZAX75vuBvvLBzN2c+Rbt5oqy93lqIBIjKb7w2UkRTb4pBWcNKNPUgt3az/PxQxEasaltdmuyvsXYbur3VPNn8JzN/6J5N+QCnz571TccapCmkPvFE+gayXU+UF1VKYyFcUvlev7lsQlmjqzK2QA8X7Q3TzFapqhvJKlt7dzBHraqZBgPgb9N4oKNayi8FZT71+kaYhIhFrESKYRHfHkj5n3kfhgLy+x+jLDbKxMggkgHnJz9RuEHZXBWtkYAQ==', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_verifier', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAMRdS21FeJfzQhM3s', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7396, 'output_tokens': 770, 'output_tokens_details': {'thinking_tokens': 739}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='skeptic', id='lc_run--01a0fe03-4ae9-76c0-89a6-4f0b114e3030-0', tool_calls=[{'name': 'transfer_to_verifier', 'args': {}, 'id': 'toolu_01H6fV8JvRJNvXy47eRnd3jH', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7396, 'output_tokens': 770, 'total_tokens': 8166, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 739}}), ToolMessage(content='Successfully transferred to verifier', name='transfer_to_verifier', id='c064563a-974f-4f0f-ab4a-080e63c01cf5', tool_call_id='toolu_01H6fV8JvRJNvXy47eRnd3jH'), AIMessage(content=[{'signature': 'CAQS9gUKEAgSGAI4AUIIdGhpbmtpbmcSDGFyJ1MXoY+Ai6NkbxoMRd2TrUfUU7WrPpA2IjDgy8dUplcZKc7DnJwNLTBfH5UNR+nGnakSPQia8UVIzrjkOnJ2fLrWUsA2oEO8yRsqkwXkkDCsyhB97dwj57RMt91XzgWhL9luNEGO92yW0rNFGoMSUVOxPIrFc5RjyrhKEc3+1RnpxCkylq+LNlQ5NFxLn6v2cF5DzqpvBHNR7XAig+urJIU3gXydCkaGZZDetJtQOUOXgZi9z9100NpdAEkXDo2ICHCGTGpVc0J/pbXxWRBxb6B1/Gl0Yjn68V6HlesH7IoC7GR/3HwRu9NSDBG4ttfQjEWcg+PeAea4Rn7jzx+Tm9H7L1joTKVgDbQg5iKMLfjvPqlnksCLtXOt8jSIS5OYLN7a/7TX6/w5d34vnw8vPDmU1UYi0Q72qUWhUa2vw8OXNpcKL7Bp0lNfBS8rvHY+vBY8/haAoTL3wvLFU1fT+4z/vA3BcsigZDYlfJeDYcxXmRuqxzxEGpXCRe9s1HWtID+41pHu6lpZHkXaghpc6TZHLxNwcVhTb6fMQ7Qo+2L6Pvklh1m32peodxeg2YiPhdpl3w/xkOEu5/JEF7ZBxpdUL+u3wkO5pUlsqfgX7uhzv7HC2WpSWVimrwiQwQOUx0+JPrLtw+lYfq7I3jruo0GW0guWvnuf7JM3YLj84q+JgHomnSSZnnZmrL0UaRvrVyLo877F39ccLS7XoZnnui8J4P58ZTmiLkEJeGcx1Iuag2e33339O7Yew/59tLbQtIGh9VWMwHBnFx39S8/4LEFrK5HldPQhOrloyTtkRs66iQRKtxMKS0VI6QTKZDAbs7a7H1yBv+mJi9Xg4uKmyU6qnn7zEKjjY2bIlCwezx4LLGP5HD3Zr6fZsmdG1UHzDsBvYG2oCI0oqEKM+valsT6hU8MRjbQBD/JJ8JJN/iBGlhBAUHo0UmhhtR2euTVpZWlMnhmpXAdKDXSfPdaPohgB', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_red_team', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAsabDquegHi6YUpCn', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7509, 'output_tokens': 88, 'output_tokens_details': {'thinking_tokens': 55}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='verifier', id='lc_run--01a0fe03-6682-7d21-8c15-ffbce9422c9f-0', tool_calls=[{'name': 'transfer_to_red_team', 'args': {}, 'id': 'toolu_01XEAuhQHCBdyCwtymZwFfDi', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7509, 'output_tokens': 88, 'total_tokens': 7597, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 55}}), ToolMessage(content='Successfully transferred to red_team', name='transfer_to_red_team', id='982c0126-e6ab-458a-922e-7b266e83c2b2', tool_call_id='toolu_01XEAuhQHCBdyCwtymZwFfDi'), AIMessage(content=[{'signature': 'CAQSwgcKEAgSGAI4AUIIdGhpbmtpbmcSDORXge9uL1832RMQwBoMjQwAwAHb9iP1ojPGIjDDScal486beCJbtG8/D2Y9g9S5xIPfjoHg/dxwX5HypSQEBbjFiTSbPECXSFzV6YIq3wbLcmyPbvusGnDexFgx+3sIXzvqyFV/69xhihMnSpfTCHy0+0zSu1WpYU/WUpIfoaSXTcTa1qkM8x8Ek/y/1jkKMZUfDFa93uhUJxOC4+QgPWBl/+Xy2t82L2x/ebq7QLQTqtR0jXeaURQm3Co6JvOgWmDvMpEdTeTWE3fDh1tzzVKsflum4d89b6CaKtn8nlQd7RhN99vNQMQHG5V4OuVNdrHPZZpFOuhdejsMFWXyB+55slGAd8wEXgFKbI/knOXVGzL/NDF8Gty1tNZlu2fdSCAsYWMsFQP1cbwLeQGszyXzJMVZJ2I9xBPkyz1HOXCPyh5F+j2gRTPikVB3ARC6tfB/lDSUeKBVZPQS8pY/B4eCs+IjjaBNWruLzfqCsc6xL0CuQPSYyQXaYFjYrSRNcC70WQBWT5VVhp0mA9yZDGYbRZt9I5OK9QkQmuJ5zDFaMEOXzSrsC4lteP6MdimiMijvsH3iq+c6WUYolRj3neI88EaHIzLAGRq64BNCLzdAYL2pEU2sb5uG78PKVoUUCbVJL5jpDOhLOTO1bQsTuoUpnRRrLQD/kIzV+ljpVBwwfJcyq75TJA/CtPr/xr2SZ9B1b2sbjG6jD0lhtoCddeDgcqgl/LSSkcuaYY8xPWCy+LtNhcqThfmfvnHLJIpL6gjbjW/OQpRfVUwfw1jOb9bWVwcgHhNYfKYGPtM8oth5xwLYRRVgzqMKOvs7falXRhVzZ2SP6Iw0BMmAAZ6HjfCl9L+tPqJNlnQQOHpoWjum4B+XsYldFEKGQWQqsd59iNPvao9p/sCOt7rGqCozcmgiEm4w66ez/QIDyb46n0+tcNJPUcoBKvQ/jDwE6VwmkQ/nECjFp3UdDT+kWYQSNd8j8VBZOpG8LlqUYrEZCv0q7Bs1vcNrdYiaFoS32osaynWk+Ht3OHuFWbEF8JlsFTQAO05VvYkecf0V/oTHsG5hyOOs7UEaDFq7uaJuzWVhh5pAojIaBcR8fVln2PLRTX7is2nsA+QrdidePcZtFtgxAiKYe1i2sSFi4vm8EOkCbQ63wdQeY4Urr5PuoMEwxUULjgKagiAmsJDq4RrVm7sO4+6YuA9j6l2uylkOEc67vf/5fkXuCsiApNL6keb8c+vL0n4FXGdzj+apSDfFGBgB', 'thinking': '', 'type': 'thinking'}, {'id': 'toolu_01VBrWdWEWYgit4kNYehBMU5', 'caller': {'type': 'direct'}, 'input': {}, 'name': 'transfer_to_editor', 'type': 'tool_use', 'toolset_name': None}], additional_kwargs={}, response_metadata={'id': 'msg_011CfdwAzfjqUopoEmZLaqm1', 'container': None, 'diagnostics': None, 'model': 'claude-sonnet', 'stop_details': None, 'stop_reason': 'tool_use', 'stop_sequence': None, 'usage': {'cache_creation': {'ephemeral_1h_input_tokens': 0, 'ephemeral_5m_input_tokens': 0}, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0, 'inference_geo': 'global', 'input_tokens': 7416, 'output_tokens': 151, 'output_tokens_details': {'thinking_tokens': 119}, 'server_tool_use': None, 'service_tier': 'standard'}, 'model_name': 'claude-sonnet', 'model_provider': 'anthropic'}, name='red_team', id='lc_run--01a0fe03-6ce6-7251-b796-4f64333f67c6-0', tool_calls=[{'name': 'transfer_to_editor', 'args': {}, 'id': 'toolu_01VBrWdWEWYgit4kNYehBMU5', 'type': 'tool_call'}], invalid_tool_calls=[], usage_metadata={'input_tokens': 7416, 'output_tokens': 151, 'total_tokens': 7567, 'input_token_details': {'cache_read': 0, 'cache_creation': 0, 'ephemeral_5m_input_tokens': 0, 'ephemeral_1h_input_tokens': 0}, 'output_token_details': {'reasoning': 119}}), ToolMessage(content='Successfully transferred to editor', name='transfer_to_editor', tool_call_id='toolu_01VBrWdWEWYgit4kNYehBMU5')], 'active_agent': 'editor'}, goto='editor')\n" + } + }, + { + "key": "exception.escaped", + "value": { + "stringValue": "False" + } + } + ] + } + ], + "status": { + "code": 1 + }, + "flags": 256 + } + ] + } + ] + }, + { + "resource": { + "attributes": [ + { + "key": "telemetry.sdk.language", + "value": { + "stringValue": "python" + } + }, + { + "key": "telemetry.sdk.name", + "value": { + "stringValue": "opentelemetry" + } + }, + { + "key": "telemetry.sdk.version", + "value": { + "stringValue": "1.45.0" + } + }, + { + "key": "service.instance.id", + "value": { + "stringValue": "b873df4c-5106-4097-a20b-b9380724f4d9" + } + }, + { + "key": "service.name", + "value": { + "stringValue": "deeplite" + } + } + ] + }, + "scopeSpans": [ + { + "scope": { + "name": "langsmith" + }, + "spans": [ + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "47f7607ab58c542f", + "parentSpanId": "4809a44f637a7046", + "name": "tools", + "kind": 1, + "startTimeUnixNano": "1790968039092067840", + "endTimeUnixNano": "1790968039095913984", + "attributes": [ + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "tools" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "2" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "tools" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"__pregel_push\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_push\",0,false]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "editor:42b8f95e-d208-6d0d-c59e-cc043f552134|tools:4fd8a45a-13d9-1c55-5033-6f6d90c107c0" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "editor" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "editor:42b8f95e-d208-6d0d-c59e-cc043f552134" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "graph:step:2" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJpbnB1dCI6W3sibmFtZSI6IndyaXRlX2ZpbGUiLCJhcmdzIjp7ImZpbGVfcGF0aCI6Ii9hbnN3ZXIubWQiLCJjb250ZW50IjoiKipUdXBsZSB2cy4gbGlzdCBpbW11dGFiaWxpdHkgKFB5dGhvbikqKlxuXG5BIHR1cGxlIGlzIGltbXV0YWJsZTogb25jZSBjcmVhdGVkLCB5b3UgY2FuJ3QgYXNzaWduIHRvIGl0cyBpdGVtcywgYW5kIHRoZSBjb2xsZWN0aW9uIG9mIG9iamVjdHMgaXQgcmVmZXJlbmNlcyBjYW4ndCBjaGFuZ2UuIGB0WzBdID0gMWAgcmFpc2VzIGBUeXBlRXJyb3JgLiBBIGxpc3QgaXMgbXV0YWJsZTogeW91IGNhbiBjaGFuZ2UgaXQgaW4gcGxhY2Ugd2l0aCBpbmRleCBvciBzbGljZSBhc3NpZ25tZW50LCBgYXBwZW5kKClgLCBgZXh0ZW5kKClgLCBgc29ydCgpYCBhbmQgc2ltaWxhciBtZXRob2RzLlxuXG5UdXBsZSBpbW11dGFiaWxpdHkgaXMgc2hhbGxvdy4gSWYgYSB0dXBsZSBob2xkcyBhIG11dGFibGUgb2JqZWN0LCBzdWNoIGFzIGEgbGlzdCwgdGhhdCBpbm5lciBvYmplY3QgY2FuIHN0aWxsIGNoYW5nZSwgYW5kIHNvIHRoZSB0dXBsZSdzIHZhbHVlIGNoYW5nZXMuIEJlY2F1c2Ugb2YgdGhpcywgYSB0dXBsZSBjYW4gYmUgYSBgZGljdGAga2V5IG9yIGBzZXRgIG1lbWJlciBvbmx5IGlmIGV2ZXJ5dGhpbmcgaW5zaWRlIGl0IGlzIGhhc2hhYmxlLiBBIGxpc3QgY2FuIG5ldmVyIGJlIGEga2V5LlxuXG5Tb3VyY2VzOlxuLSBodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3R1dG9yaWFsL2RhdGFzdHJ1Y3R1cmVzLmh0bWxcbi0gaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9yZWZlcmVuY2UvZGF0YW1vZGVsLmh0bWxcbi0gaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9saWJyYXJ5L3N0ZHR5cGVzLmh0bWxcbiJ9LCJpZCI6InRvb2x1XzAxQm04QVU5RXNRVXdxb1ZMWXB4S0E3NyIsInR5cGUiOiJ0b29sX2NhbGwifV19" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W3siY29udGVudCI6IlVwZGF0ZWQgZmlsZSAvYW5zd2VyLm1kIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJ0b29sIiwibmFtZSI6IndyaXRlX2ZpbGUiLCJpZCI6ImUzNDkyZTA4LWM4NjktNDdlYy1hNWY3LWNjNTdlYTVmNjQwMyIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxQm04QVU5RXNRVXdxb1ZMWXB4S0E3NyIsInN0YXR1cyI6InN1Y2Nlc3MifV19" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "848c01c3433c72af", + "parentSpanId": "47f7607ab58c542f", + "name": "FilesystemMiddleware.wrap_tool_call", + "kind": 1, + "startTimeUnixNano": "1790968039093334016", + "endTimeUnixNano": "1790968039095428096", + "attributes": [ + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "FilesystemMiddleware.wrap_tool_call" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "2" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "tools" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"__pregel_push\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_push\",0,false]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "editor:42b8f95e-d208-6d0d-c59e-cc043f552134|tools:4fd8a45a-13d9-1c55-5033-6f6d90c107c0" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "editor" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "editor:42b8f95e-d208-6d0d-c59e-cc043f552134" + } + }, + { + "key": "langsmith.metadata.ls_method", + "value": { + "stringValue": "traceable" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "e30=" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJjb250ZW50IjoiVXBkYXRlZCBmaWxlIC9hbnN3ZXIubWQiLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7fSwidHlwZSI6InRvb2wiLCJuYW1lIjoid3JpdGVfZmlsZSIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxQm04QVU5RXNRVXdxb1ZMWXB4S0E3NyIsInN0YXR1cyI6InN1Y2Nlc3MifQ==" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "cfb36a1a28ffc485", + "parentSpanId": "848c01c3433c72af", + "name": "write_file", + "kind": 1, + "startTimeUnixNano": "1790968039094265088", + "endTimeUnixNano": "1790968039095307008", + "attributes": [ + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "tool" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "write_file" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "execute_tool" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "gen_ai.tool.name", + "value": { + "stringValue": "write_file" + } + }, + { + "key": "gen_ai.tool.call.id", + "value": { + "stringValue": "toolu_01Bm8AU9EsQUwqoVLYpxKA77" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "2" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "tools" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"__pregel_push\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_push\",0,false]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "editor:42b8f95e-d208-6d0d-c59e-cc043f552134|tools:4fd8a45a-13d9-1c55-5033-6f6d90c107c0" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "editor" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "editor:42b8f95e-d208-6d0d-c59e-cc043f552134" + } + }, + { + "key": "langsmith.metadata.ls_method", + "value": { + "stringValue": "traceable" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "seq:step:1" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJmaWxlX3BhdGgiOiIvYW5zd2VyLm1kIiwiY29udGVudCI6IioqVHVwbGUgdnMuIGxpc3QgaW1tdXRhYmlsaXR5IChQeXRob24pKipcblxuQSB0dXBsZSBpcyBpbW11dGFibGU6IG9uY2UgY3JlYXRlZCwgeW91IGNhbid0IGFzc2lnbiB0byBpdHMgaXRlbXMsIGFuZCB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGl0IHJlZmVyZW5jZXMgY2FuJ3QgY2hhbmdlLiBgdFswXSA9IDFgIHJhaXNlcyBgVHlwZUVycm9yYC4gQSBsaXN0IGlzIG11dGFibGU6IHlvdSBjYW4gY2hhbmdlIGl0IGluIHBsYWNlIHdpdGggaW5kZXggb3Igc2xpY2UgYXNzaWdubWVudCwgYGFwcGVuZCgpYCwgYGV4dGVuZCgpYCwgYHNvcnQoKWAgYW5kIHNpbWlsYXIgbWV0aG9kcy5cblxuVHVwbGUgaW1tdXRhYmlsaXR5IGlzIHNoYWxsb3cuIElmIGEgdHVwbGUgaG9sZHMgYSBtdXRhYmxlIG9iamVjdCwgc3VjaCBhcyBhIGxpc3QsIHRoYXQgaW5uZXIgb2JqZWN0IGNhbiBzdGlsbCBjaGFuZ2UsIGFuZCBzbyB0aGUgdHVwbGUncyB2YWx1ZSBjaGFuZ2VzLiBCZWNhdXNlIG9mIHRoaXMsIGEgdHVwbGUgY2FuIGJlIGEgYGRpY3RgIGtleSBvciBgc2V0YCBtZW1iZXIgb25seSBpZiBldmVyeXRoaW5nIGluc2lkZSBpdCBpcyBoYXNoYWJsZS4gQSBsaXN0IGNhbiBuZXZlciBiZSBhIGtleS5cblxuU291cmNlczpcbi0gaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy90dXRvcmlhbC9kYXRhc3RydWN0dXJlcy5odG1sXG4tIGh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvcmVmZXJlbmNlL2RhdGFtb2RlbC5odG1sXG4tIGh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvbGlicmFyeS9zdGR0eXBlcy5odG1sXG4ifQ==" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJvdXRwdXQiOnsiY29udGVudCI6IlVwZGF0ZWQgZmlsZSAvYW5zd2VyLm1kIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJ0b29sIiwibmFtZSI6IndyaXRlX2ZpbGUiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMUJtOEFVOUVzUVV3cW9WTFlweEtBNzciLCJzdGF0dXMiOiJzdWNjZXNzIn19" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "d1e910014914b0a2", + "parentSpanId": "4809a44f637a7046", + "name": "model", + "kind": 1, + "startTimeUnixNano": "1790968035043832064", + "endTimeUnixNano": "1790968039091361024", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "1" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "editor:42b8f95e-d208-6d0d-c59e-cc043f552134|model:5f0cee1e-bfe5-1171-9153-91c4b775587c" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "editor" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "editor:42b8f95e-d208-6d0d-c59e-cc043f552134" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "graph:step:1" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W3siY29udGVudCI6IkZvciBhIHN5bnRoZXRpYyB0cmFjaW5nIGZpeHR1cmUsIGV4cGxhaW4gUHl0aG9uIHR1cGxlIHZlcnN1cyBsaXN0IGltbXV0YWJpbGl0eSBpbiBhdCBtb3N0IDEyMCB3b3Jkcy4gVXNlIG9ubHkgb2ZmaWNpYWwgUHl0aG9uIGRvY3VtZW50YXRpb24gaWYgeW91IHNlYXJjaC4gSGF2ZSB0aGUgc2tlcHRpYyBhbmQgdmVyaWZpZXIgY2hlY2sgaXQsIHRoZW4gaGF2ZSB0aGUgZWRpdG9yIHdyaXRlIC9hbnN3ZXIubWQuIEtlZXAgdGhlIHJ1biBzaG9ydCBhbmQgZG8gbm90IGFzayBmb2xsb3ctdXAgcXVlc3Rpb25zLiIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoiaHVtYW4iLCJpZCI6IjQ2MjA0MzA1LWY4ZWUtNDM4NC05Y2U4LWZjZDgwYmZjZGY3NSJ9LHsiY29udGVudCI6W3siaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9LCJuYW1lIjoid2ViX3NlYXJjaCIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHc5YVc5YnJlblBxZ3liZk53UCIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjoyODQ1LCJvdXRwdXRfdG9rZW5zIjo3Mywib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6MH0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoicmVzZWFyY2hlciIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy0yMWEzLTcwMDAtYTM4MS0yODRhNmEwYzAzZjctMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ3ZWJfc2VhcmNoIiwiYXJncyI6eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9LCJpZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6Mjg0NSwib3V0cHV0X3Rva2VucyI6NzMsInRvdGFsX3Rva2VucyI6MjkxOCwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fSx7ImNvbnRlbnQiOiJbe1widGl0bGVcIjogXCI1LiBEYXRhIFN0cnVjdHVyZXMgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3R1dG9yaWFsL2RhdGFzdHJ1Y3R1cmVzLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcIllvdSBtaWdodCBoYXZlIG5vdGljZWQgdGhhdCBtZXRob2RzIGxpa2VgaW5zZXJ0YCxgcmVtb3ZlYCBvcmBzb3J0YCB0aGF0IG9ubHkgbW9kaWZ5IHRoZSBsaXN0IGhhdmUgbm8gcmV0dXJuIHZhbHVlIHByaW50ZWQgXFx1MjAxMyB0aGV5IHJldHVybiB0aGUgZGVmYXVsdGBOb25lYC4gWzFdIFRoaXMgaXMgYSBkZXNpZ24gcHJpbmNpcGxlIGZvciBhbGwgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgaW4gUHl0aG9uLlxcbi4uLlxcbiMjIDUuMy4gVHVwbGVzIGFuZCBTZXF1ZW5jZXNcXHUwMGI2XFxuXFxuV2Ugc2F3IHRoYXQgbGlzdHMgYW5kIHN0cmluZ3MgaGF2ZSBtYW55IGNvbW1vbiBwcm9wZXJ0aWVzLCBzdWNoIGFzIGluZGV4aW5nIGFuZCBzbGljaW5nIG9wZXJhdGlvbnMuIFRoZXkgYXJlIHR3byBleGFtcGxlcyBvZiBzZXF1ZW5jZSBkYXRhIHR5cGVzIChzZWUgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBsaXN0LCB0dXBsZSwgcmFuZ2UpLiBTaW5jZSBQeXRob24gaXMgYW4gZXZvbHZpbmcgbGFuZ3VhZ2UsIG90aGVyIHNlcXVlbmNlIGRhdGEgdHlwZXMgbWF5IGJlIGFkZGVkLiBUaGVyZSBpcyBhbHNvIGFub3RoZXIgc3RhbmRhcmQgc2VxdWVuY2UgZGF0YSB0eXBlOiB0aGUgdHVwbGUuXFxuXFxuQSB0dXBsZSBjb25zaXN0cyBvZiBhIG51bWJlciBvZiB2YWx1ZXMgc2VwYXJhdGVkIGJ5IGNvbW1hcywgZm9yIGluc3RhbmNlOlxcbi4uLlxcbj4+PiAjIFR1cGxlcyBhcmUgaW1tdXRhYmxlOlxcbj4+PiB0WzBdID0gODg4ODhcXG5UcmFjZWJhY2sgKG1vc3QgcmVjZW50IGNhbGwgbGFzdCk6XFxuICBGaWxlIFxcXCI8c3RkaW4+XFxcIiwgbGluZSAxLCBpbiA8bW9kdWxlPlxcblR5cGVFcnJvcjogJ3R1cGxlJyBvYmplY3QgZG9lcyBub3Qgc3VwcG9ydCBpdGVtIGFzc2lnbm1lbnRcXG4+Pj4gIyBidXQgdGhleSBjYW4gY29udGFpbiBtdXRhYmxlIG9iamVjdHM6XFxuPj4+IHYgPSAoWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuPj4+IHZcXG4oWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuLi4uXFxuQXMgeW91IHNlZSwgb24gb3V0cHV0IHR1cGxlcyBhcmUgYWx3YXlzIGVuY2xvc2VkIGluIHBhcmVudGhlc2VzLCBzbyB0aGF0IG5lc3RlZCB0dXBsZXMgYXJlIGludGVycHJldGVkIGNvcnJlY3RseTsgdGhleSBtYXkgYmUgaW5wdXQgd2l0aCBvciB3aXRob3V0IHN1cnJvdW5kaW5nIHBhcmVudGhlc2VzLCBhbHRob3VnaCBvZnRlbiBwYXJlbnRoZXNlcyBhcmUgbmVjZXNzYXJ5IGFueXdheSAoaWYgdGhlIHR1cGxlIGlzIHBhcnQgb2YgYSBsYXJnZXIgZXhwcmVzc2lvbikuIEl0IGlzIG5vdCBwb3NzaWJsZSB0byBhc3NpZ24gdG8gdGhlIGluZGl2aWR1YWwgaXRlbXMgb2YgYSB0dXBsZSwgaG93ZXZlciBpdCBpcyBwb3NzaWJsZSB0byBjcmVhdGUgdHVwbGVzIHdoaWNoIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzLCBzdWNoIGFzIGxpc3RzLlxcblxcblRob3VnaCB0dXBsZXMgbWF5IHNlZW0gc2ltaWxhciB0byBsaXN0cywgdGhleSBhcmUgb2Z0ZW4gdXNlZCBpbiBkaWZmZXJlbnQgc2l0dWF0aW9ucyBhbmQgZm9yIGRpZmZlcmVudCBwdXJwb3Nlcy4gVHVwbGVzIGFyZSBpbW11dGFibGUsIGFuZCB1c3VhbGx5IGNvbnRhaW4gYSBoZXRlcm9nZW5lb3VzIHNlcXVlbmNlIG9mIGVsZW1lbnRzIHRoYXQgYXJlIGFjY2Vzc2VkIHZpYSB1bnBhY2tpbmcgKHNlZSBsYXRlciBpbiB0aGlzIHNlY3Rpb24pIG9yIGluZGV4aW5nIChvciBldmVuIGJ5IGF0dHJpYnV0ZSBpbiB0aGUgY2FzZSBvZiBuYW1lZHR1cGxlcykuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG5Bbm90aGVyIHVzZWZ1bCBkYXRhIHR5cGUgYnVpbHQgaW50byBQeXRob24gaXMgdGhlIGRpY3Rpb25hcnkgKHNlZSBNYXBwaW5nIFR5cGVzIFxcdTIwMTQgZGljdCkuIERpY3Rpb25hcmllcyBhcmUgc29tZXRpbWVzIGZvdW5kIGluIG90aGVyIGxhbmd1YWdlcyBhcyBcXHUyMDFjYXNzb2NpYXRpdmUgbWVtb3JpZXNcXHUyMDFkIG9yIFxcdTIwMWNhc3NvY2lhdGl2ZSBhcnJheXNcXHUyMDFkLiBVbmxpa2Ugc2VxdWVuY2VzLCB3aGljaCBhcmUgaW5kZXhlZCBieSBhIHJhbmdlIG9mIG51bWJlcnMsIGRpY3Rpb25hcmllcyBhcmUgaW5kZXhlZCBieSBrZXlzLCB3aGljaCBjYW4gYmUgYW55IGltbXV0YWJsZSB0eXBlOyBzdHJpbmdzIGFuZCBudW1iZXJzIGNhbiBhbHdheXMgYmUga2V5cy4gVHVwbGVzIGNhbiBiZSB1c2VkIGFzIGtleXMgaWYgdGhleSBjb250YWluIG9ubHkgc3RyaW5ncywgbnVtYmVycywgb3IgdHVwbGVzOyBpZiBhIHR1cGxlIGNvbnRhaW5zIGFueSBtdXRhYmxlIG9iamVjdCBlaXRoZXIgZGlyZWN0bHkgb3IgaW5kaXJlY3RseSwgaXQgY2Fubm90IGJlIHVzZWQgYXMgYSBrZXkuIFlvdSBjYW5cXHUyMDE5dCB1c2UgbGlzdHMgYXMga2V5cywgc2luY2UgbGlzdHMgY2FuIGJlIG1vZGlmaWVkIGluIHBsYWNlIHVzaW5nIGluZGV4IGFzc2lnbm1lbnRzLCBzbGljZSBhc3NpZ25tZW50cywgb3IgbWV0aG9kcyBsaWtlIGFwcGVuZCgpIGFuZCBleHRlbmQoKS5cIl19LCB7XCJ0aXRsZVwiOiBcIjMuIERhdGEgbW9kZWwgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3JlZmVyZW5jZS9kYXRhbW9kZWwuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiVGhlIHZhbHVlIG9mIHNvbWUgb2JqZWN0cyBjYW4gY2hhbmdlLiBPYmplY3RzIHdob3NlIHZhbHVlIGNhbiBjaGFuZ2UgYXJlIHNhaWQgdG8gYmUgbXV0YWJsZTsgb2JqZWN0cyB3aG9zZSB2YWx1ZSBpcyB1bmNoYW5nZWFibGUgb25jZSB0aGV5IGFyZSBjcmVhdGVkIGFyZSBjYWxsZWQgaW1tdXRhYmxlLiAoVGhlIHZhbHVlIG9mIGFuIGltbXV0YWJsZSBjb250YWluZXIgb2JqZWN0IHRoYXQgY29udGFpbnMgYSByZWZlcmVuY2UgdG8gYSBtdXRhYmxlIG9iamVjdCBjYW4gY2hhbmdlIHdoZW4gdGhlIGxhdHRlclxcdTIwMTlzIHZhbHVlIGlzIGNoYW5nZWQ7IGhvd2V2ZXIgdGhlIGNvbnRhaW5lciBpcyBzdGlsbCBjb25zaWRlcmVkIGltbXV0YWJsZSwgYmVjYXVzZSB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGl0IGNvbnRhaW5zIGNhbm5vdCBiZSBjaGFuZ2VkLiBTbywgaW1tdXRhYmlsaXR5IGlzIG5vdCBzdHJpY3RseSB0aGUgc2FtZSBhcyBoYXZpbmcgYW4gdW5jaGFuZ2VhYmxlIHZhbHVlLCBpdCBpcyBtb3JlIHN1YnRsZS4pIEFuIG9iamVjdFxcdTIwMTlzIG11dGFiaWxpdHkgaXMgZGV0ZXJtaW5lZCBieSBpdHMgdHlwZTsgZm9yIGluc3RhbmNlLCBudW1iZXJzLCBzdHJpbmdzIGFuZCB0dXBsZXMgYXJlIGltbXV0YWJsZSwgd2hpbGUgZGljdGlvbmFyaWVzIGFuZCBsaXN0cyBhcmUgbXV0YWJsZS5cXG4uLi5cXG5Tb21lIG9iamVjdHMgY29udGFpbiByZWZlcmVuY2VzIHRvIG90aGVyIG9iamVjdHM7IHRoZXNlIGFyZSBjYWxsZWQgY29udGFpbmVycy4gRXhhbXBsZXMgb2YgY29udGFpbmVycyBhcmUgdHVwbGVzLCBsaXN0cyBhbmQgZGljdGlvbmFyaWVzLiBUaGUgcmVmZXJlbmNlcyBhcmUgcGFydCBvZiBhIGNvbnRhaW5lclxcdTIwMTlzIHZhbHVlLiBJbiBtb3N0IGNhc2VzLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIHZhbHVlIG9mIGEgY29udGFpbmVyLCB3ZSBpbXBseSB0aGUgdmFsdWVzLCBub3QgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGNvbnRhaW5lZCBvYmplY3RzOyBob3dldmVyLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIG11dGFiaWxpdHkgb2YgYSBjb250YWluZXIsIG9ubHkgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGltbWVkaWF0ZWx5IGNvbnRhaW5lZCBvYmplY3RzIGFyZSBpbXBsaWVkLiBTbywgaWYgYW4gaW1tdXRhYmxlIGNvbnRhaW5lciAobGlrZSBhIHR1cGxlKSBjb250YWlucyBhIHJlZmVyZW5jZSB0byBhIG11dGFibGUgb2JqZWN0LCBpdHMgdmFsdWUgY2hhbmdlcyBpZiB0aGF0IG11dGFibGUgb2JqZWN0IGlzIGNoYW5nZWQuXFxuLi4uXFxuIyMjIDMuMi41LiBTZXF1ZW5jZXNcXHUwMGI2XFxuLi4uXFxuIyMjIyAzLjIuNS4xLiBJbW11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbkFuIG9iamVjdCBvZiBhbiBpbW11dGFibGUgc2VxdWVuY2UgdHlwZSBjYW5ub3QgY2hhbmdlIG9uY2UgaXQgaXMgY3JlYXRlZC4gKElmIHRoZSBvYmplY3QgY29udGFpbnMgcmVmZXJlbmNlcyB0byBvdGhlciBvYmplY3RzLCB0aGVzZSBvdGhlciBvYmplY3RzIG1heSBiZSBtdXRhYmxlIGFuZCBtYXkgYmUgY2hhbmdlZDsgaG93ZXZlciwgdGhlIGNvbGxlY3Rpb24gb2Ygb2JqZWN0cyBkaXJlY3RseSByZWZlcmVuY2VkIGJ5IGFuIGltbXV0YWJsZSBvYmplY3QgY2Fubm90IGNoYW5nZS4pXFxuXFxuVGhlIGZvbGxvd2luZyB0eXBlcyBhcmUgaW1tdXRhYmxlIHNlcXVlbmNlczpcXG4uLi5cXG5UdXBsZXNcXG46IFRoZSBpdGVtcyBvZiBhIGB0dXBsZWAgYXJlIGFyYml0cmFyeSBQeXRob24gb2JqZWN0cy4gVHVwbGVzIG9mIHR3byBvciBtb3JlIGl0ZW1zIGFyZSBmb3JtZWQgYnkgY29tbWEtc2VwYXJhdGVkIGxpc3RzIG9mIGV4cHJlc3Npb25zLiBBIHR1cGxlIG9mIG9uZSBpdGVtIChhIFxcdTIwMThzaW5nbGV0b25cXHUyMDE5KSBjYW4gYmUgZm9ybWVkIGJ5IGFmZml4aW5nIGEgY29tbWEgdG8gYW4gZXhwcmVzc2lvbiAoYW4gZXhwcmVzc2lvbiBieSBpdHNlbGYgZG9lcyBub3QgY3JlYXRlIGEgdHVwbGUsIHNpbmNlIHBhcmVudGhlc2VzIG11c3QgYmUgdXNhYmxlIGZvciBncm91cGluZyBvZiBleHByZXNzaW9ucykuIEFuIGVtcHR5IHR1cGxlIGNhbiBiZSBmb3JtZWQgYnkgYW4gZW1wdHkgcGFpciBvZiBwYXJlbnRoZXNlcy5cXG4uLi5cXG4jIyMjIDMuMi41LjIuIE11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbk11dGFibGUgc2VxdWVuY2VzIGNhbiBiZSBjaGFuZ2VkIGFmdGVyIHRoZXkgYXJlIGNyZWF0ZWQuIFRoZSBzdWJzY3JpcHRpb24gYW5kIHNsaWNpbmcgbm90YXRpb25zIGNhbiBiZSB1c2VkIGFzIHRoZSB0YXJnZXQgb2YgYXNzaWdubWVudCBhbmQgYGRlbGAgKGRlbGV0ZSkgc3RhdGVtZW50cy5cXG4uLi5cXG5UaGVyZSBhcmUgY3VycmVudGx5IHR3byBpbnRyaW5zaWMgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlczpcXG5cXG5MaXN0c1xcbjogVGhlIGl0ZW1zIG9mIGEgbGlzdCBhcmUgYXJiaXRyYXJ5IFB5dGhvbiBvYmplY3RzLiBMaXN0cyBhcmUgZm9ybWVkIGJ5IHBsYWNpbmcgYSBjb21tYS1zZXBhcmF0ZWQgbGlzdCBvZiBleHByZXNzaW9ucyBpbiBzcXVhcmUgYnJhY2tldHMuIChOb3RlIHRoYXQgdGhlcmUgYXJlIG5vIHNwZWNpYWwgY2FzZXMgbmVlZGVkIHRvIGZvcm0gbGlzdHMgb2YgbGVuZ3RoIDAgb3IgMS4pXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjcgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvYnVpbHRpbnMvc3RkdHlwZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiY29sbGVjdGlvbiBjbGFzc2VzIGFyZSBtdXRhYmxlLiBUaGUgbWV0aG9kcyB0aGF0IGFkZCwgc3VidHJhY3QsIG9yIHJlYXJyYW5nZSB0aGVpciAuLi4gaW4gcGxhY2UsIGFuZCBkb25cXHUyMDE5dCByZXR1cm4gYSAuLi4gLCBuZXZlciByZXR1cm4gdGhlIGNvbGxlY3Rpb24gaW5zdGFuY2UgaXRzZWxmIGJ1dCBgTm9uZWAuXFxuLi4uXFxuIyMgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBgbGlzdGAsIGB0dXBsZWAsIGByYW5nZWBcXHUwMGI2XFxuXFxuVGhlcmUgYXJlIHRocmVlIGJhc2ljIHNlcXVlbmNlIHR5cGVzOiBsaXN0cywgdHVwbGVzLCBhbmQgcmFuZ2Ugb2JqZWN0cy4gQWRkaXRpb25hbCBzZXF1ZW5jZSB0eXBlcyB0YWlsb3JlZCBmb3IgcHJvY2Vzc2luZyBvZiBiaW5hcnkgZGF0YSBhbmQgdGV4dCBzdHJpbmdzIGFyZSBkZXNjcmliZWQgaW4gZGVkaWNhdGVkIHNlY3Rpb25zLlxcbi4uLlxcblRoZSBvcGVyYXRpb25zIGluIHRoZSBmb2xsb3dpbmcgdGFibGUgLi4uIG11dGFibGUgYW5kIGltbXV0YWJsZS4gVGhlIGBjb2xsZWN0aW9ucy4gLi4uIGlzIHByb3ZpZGVkIHRvIG1ha2UgLi4uIGVhc2llciB0byBjb3JyZWN0bHkgaW1wbGVtZW50IHRoZXNlIG9wZXJhdGlvbnMgb24gY3VzdG9tIHNlcXVlbmNlIHR5cGVzXFxuLi4uXFxuIyMjIEltbXV0YWJsZSBTZXF1ZW5jZSBUeXBlc1xcdTAwYjZcXG4uLi5cXG5tdXRhYmxlIHNlcXVlbmNlIHR5cGVzIGlzXFxuLi4uXFxuc3VwcG9ydCBhbGxvd3MgaW1tdXRhYmxlIHNlcXVlbmNlcywgLi4uICwgdG8gYmUgdXNlZCBhcyBgZGljdGAga2V5cyBhbmQgc3RvcmVkIGluIC4uLiBlbnNldGAgaW5zdGFuY2VzXFxuLi4uXFxuIyMjIE11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxuIyMjIExpc3RzXFx1MDBiNlxcblxcbkxpc3RzIGFyZSBtdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaG9tb2dlbmVvdXMgaXRlbXMgKHdoZXJlIHRoZSBwcmVjaXNlIGRlZ3JlZSBvZiBzaW1pbGFyaXR5IHdpbGwgdmFyeSBieSBhcHBsaWNhdGlvbikuXFxuLi4uXFxuIyMjIFR1cGxlc1xcdTAwYjZcXG5cXG5UdXBsZXMgYXJlIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhldGVyb2dlbmVvdXMgZGF0YSAoc3VjaCBhcyB0aGUgMi10dXBsZXMgcHJvZHVjZWQgYnkgdGhlIGBlbnVtZXJhdGUoKWAgYnVpbHQtaW4pLiBUdXBsZXMgYXJlIGFsc28gdXNlZCBmb3IgY2FzZXMgd2hlcmUgYW4gaW1tdXRhYmxlIHNlcXVlbmNlIG9mIGhvbW9nZW5lb3VzIGRhdGEgaXMgbmVlZGVkIChzdWNoIGFzIGFsbG93aW5nIHN0b3JhZ2UgaW4gYSBgc2V0YCBvciBgZGljdGAgaW5zdGFuY2UpLlxcbi4uLlxcbnR1cGxlKGl0ZXJhYmxlPVxcbi4uLlxcblRoZSBjb25zdHJ1Y3RvciBidWlsZHMgYSB0dXBsZSB3aG9zZSBpdGVtcyBhcmUgdGhlIHNhbWUgYW5kIGluIHRoZSBzYW1lIG9yZGVyIGFzIGl0ZXJhYmxlXFx1MjAxOXMgaXRlbXMuIGl0ZXJhYmxlIG1heSBiZSBlaXRoZXIgYSBzZXF1ZW5jZSwgYSBjb250YWluZXIgdGhhdCBzdXBwb3J0cyBpdGVyYXRpb24sIG9yIGFuIGl0ZXJhdG9yIG9iamVjdC4gSWYgaXRlcmFibGUgaXMgYWxyZWFkeSBhIHR1cGxlLCBpdCBpcyByZXR1cm5lZCB1bmNoYW5nZWQuIEZvciBleGFtcGxlLCBgdHVwbGUoJ2FiYycpYCByZXR1cm5zIGAoJ2EnLCAnYicsICdjJylgIGFuZCBgdHVwbGUoIFsxLCAyLCAzXSApYCByZXR1cm5zIGAoMSwgMiwgMylgLiBJZiBubyBhcmd1bWVudCBpcyBnaXZlbiwgdGhlIGNvbnN0cnVjdG9yIGNyZWF0ZXMgYSBuZXcgZW1wdHkgdHVwbGUsIGAoKWAuXFxuLi4uXFxuVHVwbGVzIGltcGxlbWVudCBhbGwgb2YgdGhlIGNvbW1vbiBzZXF1ZW5jZSBvcGVyYXRpb25zLlxcbi4uLlxcbkZvciBoZXRlcm9nZW5lb3VzIGNvbGxlY3Rpb25zIG9mIGRhdGEgd2hlcmUgYWNjZXNzIGJ5IG5hbWUgaXMgY2xlYXJlciB0aGFuIGFjY2VzcyBieSBpbmRleCwgYGNvbGxlY3Rpb25zLm5hbWVkdHVwbGUoKWAgbWF5IGJlIGEgbW9yZSBhcHByb3ByaWF0ZSBjaG9pY2UgdGhhbiBhIHNpbXBsZSB0dXBsZSBvYmplY3QuXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvbGlicmFyeS9zdGR0eXBlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJTb21lIGNvbGxlY3Rpb24gY2xhc3NlcyBhcmUgbXV0YWJsZS4gVGhlIG1ldGhvZHMgdGhhdCBhZGQsIHN1YnRyYWN0LCBvciByZWFycmFuZ2UgdGhlaXIgLi4uIGluIHBsYWNlLCBhbmQgZG9uXFx1MjAxOXQgcmV0dXJuIGEgLi4uICwgbmV2ZXIgcmV0dXJuIHRoZSBjb2xsZWN0aW9uIGluc3RhbmNlIGl0c2VsZiBidXQgYE5vbmVgLlxcbi4uLlxcbiMjIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgYGxpc3RgLCBgdHVwbGVgLCBgcmFuZ2VgXFx1MDBiNlxcblxcblRoZXJlIGFyZSB0aHJlZSBiYXNpYyBzZXF1ZW5jZSB0eXBlczogbGlzdHMsIHR1cGxlcywgYW5kIC4uLiBvYmplY3RzLiBBZGRpdGlvbmFsIHNlcXVlbmNlIHR5cGVzIHRhaWxvcmVkIGZvciBwcm9jZXNzaW5nIG9mIGJpbmFyeSBkYXRhIGFuZCB0ZXh0IHN0cmluZ3MgYXJlIGRlc2NyaWJlZCBpbiBkZWRpY2F0ZWQgc2VjdGlvbnMuXFxuLi4uXFxuVGhlIG9wZXJhdGlvbnMgaW4gdGhlIGZvbGxvd2luZyB0YWJsZSAuLi4gaW1tdXRhYmxlLiBUaGUgYCAuLi4gaXMgcHJvdmlkZWQgdG8gbWFrZSAuLi4gZWFzaWVyIHRvIGNvcnJlY3RseSBpbXBsZW1lbnQgdGhlc2Ugb3BlcmF0aW9ucyBvbiBjdXN0b20gc2VxdWVuY2UgdHlwZXNcXG4uLi5cXG4jIyMgSW1tdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbm9wZXJhdGlvbiB0aGF0IGltbXV0YWJsZSBzZXF1ZW5jZSAuLi4gYnkgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlcyBpcyAuLi4gYGhhc2goKWBcXG4uLi5cXG5UaGlzIHN1cHBvcnQgYWxsb3dzIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHN1Y2ggYXMgYHR1cGxlYCBpbnN0YW5jZXMsIHRvIGJlIHVzZWQgYXMgYGRpY3RgIGtleXMgYW5kIHN0b3JlZCBpbiBgc2V0YCBhbmQgYGZyb3plbnNldGAgaW5zdGFuY2VzLlxcbi4uLlxcbiMjIyBNdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbiMjIyBMaXN0c1xcdTAwYjZcXG5cXG5MaXN0cyBhcmUgbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhvbW9nZW5lb3VzIGl0ZW1zICh3aGVyZSB0aGUgcHJlY2lzZSBkZWdyZWUgb2Ygc2ltaWxhcml0eSB3aWxsIHZhcnkgYnkgYXBwbGljYXRpb24pLlxcbi4uLlxcbiMjIyBUdXBsZXNcXHUwMGI2XFxuXFxuVHVwbGVzIGFyZSBpbW11dGFibGUgc2VxdWVuY2VzLCB0eXBpY2FsbHkgdXNlZCB0byBzdG9yZSBjb2xsZWN0aW9ucyBvZiBoZXRlcm9nZW5lb3VzIGRhdGEgKHN1Y2ggYXMgdGhlIDItdHVwbGVzIHByb2R1Y2VkIGJ5IHRoZSBgZW51bWVyYXRlKClgIGJ1aWx0LWluKS4gVHVwbGVzIGFyZSBhbHNvIHVzZWQgZm9yIGNhc2VzIHdoZXJlIGFuIGltbXV0YWJsZSBzZXF1ZW5jZSBvZiBob21vZ2VuZW91cyBkYXRhIGlzIG5lZWRlZCAoc3VjaCBhcyBhbGxvd2luZyBzdG9yYWdlIGluIGEgYHNldGAgb3IgYGRpY3RgIGluc3RhbmNlKS5cXG4uLi5cXG5UaGUgY29uc3RydWN0b3IgYnVpbGRzIGEgdHVwbGUgd2hvc2UgaXRlbXMgYXJlIHRoZSBzYW1lIGFuZCBpbiB0aGUgc2FtZSBvcmRlciBhcyBpdGVyYWJsZVxcdTIwMTlzIGl0ZW1zLiBpdGVyYWJsZSBtYXkgYmUgZWl0aGVyIGEgc2VxdWVuY2UsIGEgY29udGFpbmVyIHRoYXQgc3VwcG9ydHMgaXRlcmF0aW9uLCBvciBhbiBpdGVyYXRvciBvYmplY3QuIElmIGl0ZXJhYmxlIGlzIGFscmVhZHkgYSB0dXBsZSwgaXQgaXMgcmV0dXJuZWQgdW5jaGFuZ2VkLiBGb3IgZXhhbXBsZSwgYHR1cGxlKCdhYmMnKWAgcmV0dXJucyBgKCdhJywgJ2InLCAnYycpYCBhbmQgYHR1cGxlKCBbMSwgMiwgM10gKWAgcmV0dXJucyBgKDEsIDIsIDMpYC4gSWYgbm8gYXJndW1lbnQgaXMgZ2l2ZW4sIHRoZSBjb25zdHJ1Y3RvciBjcmVhdGVzIGEgbmV3IGVtcHR5IHR1cGxlLCBgKClgLlxcbi4uLlxcblR1cGxlcyBpbXBsZW1lbnQgYWxsIG9mIHRoZSBjb21tb24gc2VxdWVuY2Ugb3BlcmF0aW9ucy5cXG4uLi5cXG5Gb3IgaGV0ZXJvZ2VuZW91cyBjb2xsZWN0aW9ucyBvZiBkYXRhIHdoZXJlIGFjY2VzcyBieSBuYW1lIGlzIGNsZWFyZXIgdGhhbiBhY2Nlc3MgYnkgaW5kZXgsIGBjb2xsZWN0aW9ucy5uYW1lZHR1cGxlKClgIG1heSBiZSBhIG1vcmUgYXBwcm9wcmlhdGUgY2hvaWNlIHRoYW4gYSBzaW1wbGUgdHVwbGUgb2JqZWN0LlwiXX0sIHtcInRpdGxlXCI6IFwiNS4gRGF0YSBTdHJ1Y3R1cmVzIFxcdTIwMTQgUHl0aG9uIDMuMTAuMjAgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMuMTAvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiWW91IG1pZ2h0IGhhdmUgbm90aWNlZCB0aGF0IG1ldGhvZHMgbGlrZSBgaW5zZXJ0YCwgYHJlbW92ZWAgb3IgYHNvcnRgIHRoYXQgb25seSBtb2RpZnkgdGhlIGxpc3QgaGF2ZSBubyByZXR1cm4gdmFsdWUgcHJpbnRlZCBcXHUyMDEzIHRoZXkgcmV0dXJuIHRoZSBkZWZhdWx0IGBOb25lYC4gMSBUaGlzIGlzIGEgZGVzaWduIHByaW5jaXBsZSBmb3IgYWxsIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIGluIFB5dGhvbi5cXG4uLi5cXG4jIyA1LjMuIFR1cGxlcyBhbmQgU2VxdWVuY2VzXFx1MDBiNlxcblxcbldlIHNhdyB0aGF0IGxpc3RzIGFuZCBzdHJpbmdzIGhhdmUgbWFueSBjb21tb24gcHJvcGVydGllcywgc3VjaCBhcyBpbmRleGluZyBhbmQgc2xpY2luZyBvcGVyYXRpb25zLiBUaGV5IGFyZSB0d28gZXhhbXBsZXMgb2Ygc2VxdWVuY2UgZGF0YSB0eXBlcyAoc2VlIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgbGlzdCwgdHVwbGUsIHJhbmdlKS4gU2luY2UgUHl0aG9uIGlzIGFuIGV2b2x2aW5nIGxhbmd1YWdlLCBvdGhlciBzZXF1ZW5jZSBkYXRhIHR5cGVzIG1heSBiZSBhZGRlZC4gVGhlcmUgaXMgYWxzbyBhbm90aGVyIHN0YW5kYXJkIHNlcXVlbmNlIGRhdGEgdHlwZTogdGhlIHR1cGxlLlxcblxcbkEgdHVwbGUgY29uc2lzdHMgb2YgYSBudW1iZXIgb2YgdmFsdWVzIHNlcGFyYXRlZCBieSBjb21tYXMsIGZvciBpbnN0YW5jZTpcXG4uLi5cXG4+Pj4gIyBUdXBsZXMgYXJlIGltbXV0YWJsZTpcXG4uLi4gdFswXSA9IDg4ODg4XFxuVHJhY2ViYWNrIChtb3N0IHJlY2VudCBjYWxsIGxhc3QpOlxcbiAgRmlsZSBcXFwiPHN0ZGluPlxcXCIsIGxpbmUgMSwgaW4gPG1vZHVsZT5cXG5UeXBlRXJyb3I6ICd0dXBsZScgb2JqZWN0IGRvZXMgbm90IHN1cHBvcnQgaXRlbSBhc3NpZ25tZW50XFxuPj4+ICMgYnV0IHRoZXkgY2FuIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzOlxcbi4uLiB2ID0gKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbj4+PiB2XFxuKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbi4uLlxcbkFzIHlvdSBzZWUsIG9uIG91dHB1dCB0dXBsZXMgYXJlIGFsd2F5cyBlbmNsb3NlZCBpbiBwYXJlbnRoZXNlcywgc28gdGhhdCBuZXN0ZWQgdHVwbGVzIGFyZSBpbnRlcnByZXRlZCBjb3JyZWN0bHk7IHRoZXkgbWF5IGJlIGlucHV0IHdpdGggb3Igd2l0aG91dCBzdXJyb3VuZGluZyBwYXJlbnRoZXNlcywgYWx0aG91Z2ggb2Z0ZW4gcGFyZW50aGVzZXMgYXJlIG5lY2Vzc2FyeSBhbnl3YXkgKGlmIHRoZSB0dXBsZSBpcyBwYXJ0IG9mIGEgbGFyZ2VyIGV4cHJlc3Npb24pLiBJdCBpcyBub3QgcG9zc2libGUgdG8gYXNzaWduIHRvIHRoZSBpbmRpdmlkdWFsIGl0ZW1zIG9mIGEgdHVwbGUsIGhvd2V2ZXIgaXQgaXMgcG9zc2libGUgdG8gY3JlYXRlIHR1cGxlcyB3aGljaCBjb250YWluIG11dGFibGUgb2JqZWN0cywgc3VjaCBhcyBsaXN0cy5cXG5cXG5UaG91Z2ggdHVwbGVzIG1heSBzZWVtIHNpbWlsYXIgdG8gbGlzdHMsIHRoZXkgYXJlIG9mdGVuIHVzZWQgaW4gZGlmZmVyZW50IHNpdHVhdGlvbnMgYW5kIGZvciBkaWZmZXJlbnQgcHVycG9zZXMuIFR1cGxlcyBhcmUgaW1tdXRhYmxlLCBhbmQgdXN1YWxseSBjb250YWluIGEgaGV0ZXJvZ2VuZW91cyBzZXF1ZW5jZSBvZiBlbGVtZW50cyB0aGF0IGFyZSBhY2Nlc3NlZCB2aWEgdW5wYWNraW5nIChzZWUgbGF0ZXIgaW4gdGhpcyBzZWN0aW9uKSBvciBpbmRleGluZyAob3IgZXZlbiBieSBhdHRyaWJ1dGUgaW4gdGhlIGNhc2Ugb2YgYG5hbWVkdHVwbGVzYCkuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG4jIyA1LjQuXFxuLi4uXFxuLiBTZXQgb2JqZWN0c1xcbi4uLlxcbiMjIDUuNS4gRGljdGlvbmFyaWVzXFx1MDBiNlxcblxcbkFub3RoZXIgdXNlZnVsIGRhdGEgdHlwZSBidWlsdCBpbnRvIFB5dGhvbiBpcyB0aGUgZGljdGlvbmFyeSAoc2VlIE1hcHBpbmcgVHlwZXMgXFx1MjAxNCBkaWN0KS4gRGljdGlvbmFyaWVzIGFyZSBzb21ldGltZXMgZm91bmQgaW4gb3RoZXIgbGFuZ3VhZ2VzIGFzIFxcdTIwMWNhc3NvY2lhdGl2ZSBtZW1vcmllc1xcdTIwMWQgb3IgXFx1MjAxY2Fzc29jaWF0aXZlIGFycmF5c1xcdTIwMWQuIFVubGlrZSBzZXF1ZW5jZXMsIHdoaWNoIGFyZSBpbmRleGVkIGJ5IGEgcmFuZ2Ugb2YgbnVtYmVycywgZGljdGlvbmFyaWVzIGFyZSBpbmRleGVkIGJ5IGtleXMsIHdoaWNoIGNhbiBiZSBhbnkgaW1tdXRhYmxlIHR5cGU7IHN0cmluZ3MgYW5kIG51bWJlcnMgY2FuIGFsd2F5cyBiZSBrZXlzLiBUdXBsZXMgY2FuIGJlIHVzZWQgYXMga2V5cyBpZiB0aGV5IGNvbnRhaW4gb25seSBzdHJpbmdzLCBudW1iZXJzLCBvciB0dXBsZXM7IGlmIGEgdHVwbGUgY29udGFpbnMgYW55IG11dGFibGUgb2JqZWN0IGVpdGhlciBkaXJlY3RseSBvciBpbmRpcmVjdGx5LCBpdCBjYW5ub3QgYmUgdXNlZCBhcyBhIGtleS4gWW91IGNhblxcdTIwMTl0IHVzZSBsaXN0cyBhcyBrZXlzLCBzaW5jZSBsaXN0cyBjYW4gYmUgbW9kaWZpZWQgaW4gcGxhY2UgdXNpbmcgaW5kZXggYXNzaWdubWVudHMsIHNsaWNlIGFzc2lnbm1lbnRzLCBvciBtZXRob2RzIGxpa2UgYGFwcGVuZCgpYCBhbmQgYGV4dGVuZCgpYC5cIl19XSIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ3ZWJfc2VhcmNoIiwiaWQiOiI2ZTUxMjVhYy0zMTY0LTRhNzYtOWM1Mi1hYzM0NjBjZTM2ZTQiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJzdGF0dXMiOiJzdWNjZXNzIn0seyJjb250ZW50IjpbeyJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3NrZXB0aWMiLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QTZGNGNDTjV5aEQ0Q3JzOU0iLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjB9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InJlc2VhcmNoZXIiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtM2NkOC03OWYxLTg4YTEtY2U2Y2VlMjk0ZTc5LTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoidHJhbnNmZXJfdG9fc2tlcHRpYyIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsInRvdGFsX3Rva2VucyI6NzQxMSwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fSx7ImNvbnRlbnQiOiJTdWNjZXNzZnVsbHkgdHJhbnNmZXJyZWQgdG8gc2tlcHRpYyIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ0cmFuc2Zlcl90b19za2VwdGljIiwiaWQiOiIzODkwNThkMS0wODNmLTQ2MzItYmJjNS0wOWJmZDNhMGZmZDIiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMVhqajNhUmRXeWhCSmZQbVRKQnhRYTkiLCJzdGF0dXMiOiJzdWNjZXNzIn0seyJjb250ZW50IjpbeyJzaWduYXR1cmUiOiJDQVFTMVFnS0VBZ1NHQUk0QVVJSWRHaHBibXRwYm1jU0RONlRmVTZORE05NzFsRWJ6eG9NdlJVQTFKOW94M2ZKUVlUOUlqQXJDUVFHSEJQNHBSSFBhdlh6UmxiTWJhWUhDNTdZaitHYnhncjBoZWx2ZDJPSUwvbzZSYnQ0UEZBM0ZhSUZVbGdxOGdjN0tRSTlGdEdudlNBa0xCUnpJc1VwLzJkNVJkMnpnRXNma0ZWZVhzbjhXWVlaa2luQVFWVnk2QTdCM0xhRUpXMHhiL3g4OERpNjdmZWR3eDBRK0ZsWnR3Y21UM0RjYWExMUp2ZXlIUnIzZG0rekdTWlA2Q1hlWE5KOG5yVFo3VzFZUlR5c0Q2dzdvN0xwOGgxUndxNGY0bzhqK1FVZlkwdmplK3pKWnN4SFBtUU50M3lrZUtoOTloZ242ZGJpejYvRUhxbWJIalNBdTFNNFNxRURmcUlLVGhIWWVuYW1BWm1jUTRYQU9vNm1kUTYrNzdWZ3BWMWxDMS9rNTJJaVVvSmprR2xRcm5MQ3Z1dGlJN0tOK3NnSzhjMisyS0NKclBSQ3ZWVitCa1N0dlA1aldXcXp5dHBLUStuSHpoTDQ0OE9ySUE5Z2RKQzM5QzhlRHJYZVJuSTd0NEZxbGtYR29NbFpBVjlVTWNyUUhSeE9NaTh0T3lUemk5b3lCRkRUU3NRK0xKcWVyMHR6MUZ0OWdlazZ2emc1MFNwVkFGUWRMRVJ2OFIxWXFNYWVJK3hzdlRBRVc5TVJndlJNa3pMdXpSVDJndXczakQrRmppcDM3eGdLNVFJMFIrQ3NPQW1RUG9UWml6emNJeUFVQXVDSkZDZ3EvZ0QvQ2liTHhveW1pVlVJMWFuOVNudDNLcE5Sb1dkdldJbmUzM1YwcHY4UEhnOXlXMkp1TE9xUUdpbExDc0svUTNvUXIzdXBFTVh4WEdqQ0w3V1VpenZSWkdGRUVWK1ZwbFl0N3Y0dEtaTWo1cGszdmRlTTBNSjVYWmc3UHZVc1J6eWN4Y25yVDNLbm9jM3FNK3R0cVpmVHNiOGQ2VS9wUWlmcTdHdWFoRk9IcnZyb3FnSkxqKy84MVl5MXBaRmtqZkpnWTBYa2x6QW5BNkNGcS9ZVDBneGJWK1RINUlTK1dhWU9VekszNlM4czVxcU13aG5ndjRJekUzY3RYWHFuaElaWE56bnlWQnFiWWEvR3gyYkNqY0VqMm41UnlHWVJTLytNY3VsZ0xCem5ROU1rRzJEN1FQM01xL253SFhLTUlhTFM4Vks4aEczZDByQWpsdm5hdVhTaE5ySnh0a2o0cWt3bnhENExyRWprMXVlK1BMY2JMcVlXd0FyOG4wcys0MWpyc0lQVnl5Szl2RmFFWnJBOUthQnhrSnlpcXg1MEdaTm43Y2t4K21rSTE4Z0g1Mml5VlB0VThGS01YanVnQ2w2bVBMaUhZaURRZCtHNjNMdWRtT0t5czlpdFZuQkRCTk1QMmIvSTcrYklWUmxoZUdhWldyNk1BVEtsODZCM3krMGt1T3JMUk15V2NkaWJjMmlRa1ptKzhmN2RUVXdiS2JCSnNLT0ZZNzMvOG5sTC9KaVlwano2blVJaytJcVNqYUZ5SzZYRmxDY3VrV3BXSHBESm5TdmJFemV5N3FIeUNxSitJK29uOGlQOEdPdGVtcFFhUzAvNGtaR1N3bHJFNnhPajZFd0dwZmpiRFpLSGtQLzJSQWROWVBRcTN6N2JkVkpNSk5uRk16NmUzN216TXBDQmlod1BkU0ZUUkUzU0NZNjZ3dlRnSnpHQ1V5aEhiU2k1aTFhenpPWGQ4dUhDU3BULzZMMW82VlUwcGpvUjhLYWE0d0NXSEdmN1AvV0VkK2F2eXAwMkpudTFjbXBXMCtHcSt2SzloMHRkYjJJQ2xNL2VOZmRvdVFQK2VwOE1KVWpyYURSSUNtQUpRWTVvYlM3MXZuaU9vK3JlYUZ5MHpSZ0IiLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsic2lnbmF0dXJlIjoiQ0FRUy9SRUtFUWdTR0FJNEFVSUpibUZ5Y21GMGFXOXVFZ3dGaDNBVkpaUDNzOHkxSHEwYURHNzZ1aEhPTmFTY1hBR1NoaUl3R2dqTFhIVXgxeVpya0xQbGpqelRPUXljcExKaUtWd1hjeG0vRXpLSm0xczhNNVd4dVJFV0VsZFhjb2FOMC9tc0twa1JNVCtlTGI1aWg5ckFIVUxUb21VTnNVd0pldTBqckplNEdMYXp5N3NQNUt0akVPRUJLUXNYVWZvUTdmRGtaT1FVSFppOXp6UU40aEZObVlKVWYzT1ZxZjlUUU5kQzErbS8vSEFaUGZXbUF5Nkh0RHZtN2tnQkprQ0VvaUZubTd0bi9QWDFoWEdQUmdXYzZCVFhVTXIyV0E4YUt5cWdnUlViTUgyK0pZdjVpV1pkZENjMXVwMU5icjNzRjdXUGNvdDlaQmh5SFFYcjY5MnplSms2U0pneWdJVnJBU2JNbHkyYkJ4VFdKQjYyM2dWZ3pwQ21WU0xaRnBrS1hnUGNwOGhHeURLT3ZGNlNpUVNIcUdSeFFvZnI3VnMxYVBoZlppeFJPSkdEK01scmtnZ1ZBbkdNeVlJRFVacGF2dENlaVUxSm1CU3owMDg0YlhQdFQvdG5wc0tPblNQWVpRbzNCMEJYS2JxQS9Xc0tuZzgvVjNaOFV1YWpvNklWWjJoZ0ExV0RkdXFRT1FrRXhHVTZwRnVMaGViSWp1V3hTTkkvRGZmdlIvL0xOcUNmdmRuZjBodm9QdER4ZngvU2F3SXg0L3NwcTl4YndGekh0TGdvSGcxRjJ3Rnc3Y2xQRGxnMVB5RC92MnlKRzNWOVQ4R0w2akdQRTZyaGZpbC91NEhOdmY3QlcwZFF6Q1RUWGNxWnk0bkJ5eTArcCtTN1p4Zk1sNEtweGEyZ09oaFNOamZwZnVYMFFwTDRrTUU4SjFubGlGTjFkOEtxRVB1REkxOEJRSFVoRC9VZlkyOW9BMHg1MTBGak0vQnNMWjRuNXNwcGtvSWk3Q0ZvUXEyYUFLVk5mQzFLaWxoSXBSRCsxZHlEdFpRa2xOOGdacE9FU0h3Nk1MMVR0VTB5ZkFjYm1xb3hKNmhoanp3SDZDTXNUUUVFeThNYlJCTStRS0trSVJkUkNQSEdGVzJMa3VSNTArSjBwRi90cjZwcVZ2SmVvemZpTW5yRWlsQjI3VFpQNG5LVEpHOHJkLy92Q1dlbjB5N0NBLzhkQko5Z2RwNXR4N05WQTQ1azFnZGN4RVIxZ1lvdU5YM24xdkczL0IxRjQrOTN3WFc5TXFvNlVzNTlUUW1yYjlOcWE3cFNRTlNySDA2dzFHeHlCQWxrRmZxQWxhZmsxNmFNcHE0MTZUSllwR3k0M1dDVWlDY0xXbGhpV2d6MnNnT0xBWFA5YTV3ZVdBS1JDOEo2amtjVWtEZVhuVytIRlNpUE02KzlCbzZrVmhMVWhONXJjNG1IWk9acEY3cHRQcHNRTjRYQ3FMUHNxcFJILzhvNVhweUM2Wm5lVGVGMkV4dGpod1pWalhEd01iSXpWWUlidzF5MlBQR3c4ZEM3cGM0c0lWU25JNG9RQUgwdjY1MGdoZmhtTWV2V1c0aDRQeWxidy9ERzRmZGVUMys4ejBmOE1PU1R0ZmlweDA1L1F0dEhsQXZPRFJ4YTV5REN2T3o2bjNmMUJPME4vNE1iR3lPTTI3cVc0eDhtQlZvcnlVWldmc21oZmkwL0dmTnZCOCt5bFRHYllOWDI3T2NFRjVXWnROcWJXRXk3VjVHYm1aVVN4c3BjZzdGQlozajMvM0w3bXV2WlhvWmkxZjVlcnE0eTNsU0grb0Y3cVBGbnZpZXlMT0N3dWJjUXkrb3lvd2o0Y0QwaWRSZWNucCtzKzUyRDcrbXpGMWdwVytmK2NKYXNveGlnYnhrT2xvUzR5ZWlLdmdNNG5XU3BHRWoxMlJQeDVvODBFbXppWE45dDR6SnpXMWh0QTVoWEpKZUZ5bXBNWXR5Q1VDZXZSV1ZtclJZa3RRN1VuQ2E2dEwvQVZQaTFuaTVYQ1hIZkxPdXFmSzVLTFl4andWdGRoaGZtQ0x2cGFLVk5DRU9DZjRuTkdIcUQ5OHdVYjBlaldOd0RZQm5KR2tCUnc5UlZzcGZ3TmRvV29WY24ycXY4eXdHVXA3eWMvc2syTHBJNlZ6aVJ5dldxYkdORzFaSnFWVnV0UmM0NitzT0U0VlNlUzIvQ1ROS05hNk5NQ2YvaHh4YVVWc1lmS2RQK1MxTGM0aWFuS3Awc1V1Z1BwbXRveWRDMENQMlJ3am5ITGNuenhDbnZxeGxMUVdYTU5QVHN1MnlCY0hsV0pMdGNtZGcvTWNMYWd5N2w1a0c2c043WnhKTEQva0F4OEU2dXFvaFArSy9qTVNyWXVRSm5ocjR0ZjRiaVdGWTNKemYvS3BBWXlWU3ErcGJmRktYTWJMOW5IRW9aUVQxNVd5YWJkNkRNQXEzUVpPdGhNNXBVemROcVVYa2NySTFQd2pBU0FnNGRWVS8ybys5OG1UWG55aFA3VmxFa1RuZGRwd3lKRitZQ0NjMDFwcWE2Wlg5OXBuUitZT3BJcmI1RHRQY0txMHQ1Y0ZDRHlReE4xbFV1cVpjWXVqdlgvR0M3KzZJcFJUei9Oa0VZLzVjbnRPZlpKeWQ2U2tVeC9KUjVBSklSUENOZWliMHJjV25uRm4wWEs4czIyeDQwd0xqRXR1ZlhETzkreXJhZjRrSHhsQkkzU0IwbWVNamVwWDUwQ0poQXg3TmVra1JGbndvZWNMZnFJeXJqWHJnQVpzVE1QaVg5NHNuamRyemlzQVh0QkthbnVsaWxTeE9mVzJIU21mbmswTDJHWEhKSjNTK3ZDYmdSRW1sOWRla2ZtSGhtd0VZdHYza2ZodGg3RVJJd1ZUOGo2OU90SUJDbWVUSUp1QTdvNzNQWGMxTEoyd2h1Wks3RlplNk5UT3M2MU1QN0pWbEljNnRiaHRPQmszTlhVOHBCUE9DWTZRNTVkNjNGdHBVVS9LQ2JRMEsyV1E5bzVqTkd3Tk5KNkN3NUZEQ3QreDVZSmVQMWQyUXpEU2lxK2NTWnF3cUNaVmNOb0FmdEdHemdyUlo0dmtwYlAweFFaSEN0V3NVd0hWVGpyWTROZlJEakhkVlBSbjhKOVh0b29jL2hJV2txekZhY3pZVGxhcHUyUkNKcWU4YUVvM2p6V1pDRUo3QWlCekJZcmFZRGdPZmVpUXllSlVzYmJYSGRHS2lpdzIyQmRwNE0rWXo2NzFNWjBpZTlFV1JrczBRMkRIR1VUbmU5UzR0QUFKUEdiVWY0eHRDTW9KNDBRNFFXMDVRTFNaeTN3TkZBVUZZbVhFNEVDREZCOCtJRWVORkZGL3RnQW5maDhHdkZoLzF4WStMRStvek1taW5qU1J3aHEwOXpRQUVrTkZ5dTBJS2d2RGt5T0kraWNjRjdkb1dMVFVhc1B4TEgxeEdQQlNLa2VXQ25Tb1gvNEMyYzlkdTY2QmJSWGFtSHFhZStWQ2lTNkJOOTUzaVlLT3U4ck1rbmVXK1RGalYwQ3pBZjhQTkc5TEJheVVnWTVVdzl4MFNhcDZ6STRQcGFaS0g0ZDlrdGtGVSs5MXlCYk41U1J0ZGQ4L0VRYXRKWG1CTmZ3N2tScmxsK0xqd1p3ZVdMRWIwalQxUnBHdURxSExkTFRNTWdvd1NjenJwMnpDc0pkUUgvc2VhWXJwd2tDRlI4ZFJqTTdKUlVrVmExQTBTdmRaQVg3NXZ1QnZ2TEJ6TjJjK1JidDVvcXk5M2xxSUJJaktiN3cyVWtSVGI0cEJXY05LTlBVZ3QzYXovUHhReEVhc2FsdGRtdXl2c1hZYnVyM1ZQTm44SnpOLzZKNU4rUUNuejU3MVRjY2FwQ21rUHZGRStnYXlYVStVRjFWS1l5RmNVdmxldjdsc1FsbWpxeksyUUE4WDdRM1R6RmFwcWh2SktsdDdkekJIcmFxWkJnUGdiOU40b0tOYXlpOEZaVDcxK2thWWhJaEZyRVNLWVJIZkhrajVuM2tmaGdMeSt4K2pMRGJLeE1nZ2tnSG5KejlSdUVIWlhCV3RrWUFRPT0iLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsiaWQiOiJ0b29sdV8wMUg2ZlY4SnZSSk52WHk0N2VSbmQzakgiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6e30sIm5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHdBTVJkUzIxRmVKZnpRaE0zcyIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo3Mzk2LCJvdXRwdXRfdG9rZW5zIjo3NzAsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjczOX0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoic2tlcHRpYyIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy00YWU5LTc2YzAtODlhNi00ZjBiMTE0ZTMwMzAtMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzM5Niwib3V0cHV0X3Rva2VucyI6NzcwLCJ0b3RhbF90b2tlbnMiOjgxNjYsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjczOX19fSx7ImNvbnRlbnQiOiJTdWNjZXNzZnVsbHkgdHJhbnNmZXJyZWQgdG8gdmVyaWZpZXIiLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7fSwidHlwZSI6InRvb2wiLCJuYW1lIjoidHJhbnNmZXJfdG9fdmVyaWZpZXIiLCJpZCI6ImMwNjQ1NjNhLTk3NGYtNGYwZi1hYjRhLTA4MGU2M2MwMWNmNSIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsInN0YXR1cyI6InN1Y2Nlc3MifSx7ImNvbnRlbnQiOlt7InNpZ25hdHVyZSI6IkNBUVM5Z1VLRUFnU0dBSTRBVUlJZEdocGJtdHBibWNTREdGeUoxTVhvWStBaTZOa2J4b01SZDJUclVmVVU3V3JQcEEySWpEZ3k4ZFVwbGNaS2M3RG5Kd05MVEJmSDVVTlIrbkduYWtTUFFpYThVVkl6cmprT25KMmZMcldVc0Eyb0VPOHlSc3Frd1hra0RDc3loQjk3ZHdqNTdSTXQ5MVh6Z1doTDlsdU5FR085MnlXMHJORkdvTVNVVk94UElyRmM1Ump5cmhLRWMzKzFSbnB4Q2t5bHErTE5sUTVORnhMbjZ2MmNGNUR6cXB2QkhOUjdYQWlnK3VySklVM2dYeWRDa2FHWlpEZXRKdFFPVU9YZ1ppOXo5MTAwTnBkQUVrWERvMklDSENHVEdwVmMwSi9wYlh4V1JCeGI2QjEvR2wwWWpuNjhWNkhsZXNIN0lvQzdHUi8zSHdSdTlOU0RCRzR0dGZRakVXY2crUGVBZWE0Um43anp4K1RtOUg3TDFqb1RLVmdEYlFnNWlLTUxmanZQcWxua3NDTHRYT3Q4alNJUzVPWUxON2EvN1RYNi93NWQzNHZudzh2UERtVTFVWWkwUTcycVVXaFVhMnZ3OE9YTnBjS0w3QnAwbE5mQlM4cnZIWSt2Qlk4L2hhQW9UTDN3dkxGVTFmVCs0ei92QTNCY3NpZ1pEWWxmSmVEWWN4WG1SdXF4enhFR3BYQ1JlOXMxSFd0SUQrNDFwSHU2bHBaSGtYYWdocGM2VFpITHhOd2NWaFRiNmZNUTdRbysyTDZQdmtsaDFtMzJwZW9keGVnMllpUGhkcGwzdy94a09FdTUvSkVGN1pCeHBkVUwrdTN3a081cFVsc3FmZ1g3dWh6djdIQzJXcFNXVmltcndpUXdRT1V4MCtKUHJMdHcrbFlmcTdJM2pydW8wR1cwZ3VXdm51ZjdKTTNZTGo4NHErSmdIb21uU1Nabm5abXJMMFVhUnZyVnlMbzg3N0YzOWNjTFM3WG9abm51aThKNFA1OFpUbWlMa0VKZUdjeDFJdWFnMmUzMzMzOU83WWV3LzU5dExiUXRJR2g5VldNd0hCbkZ4MzlTOC80TEVGcks1SGxkUFFoT3Jsb3lUdGtSczY2aVFSS3R4TUtTMFZJNlFUS1pEQWJzN2E3SDF5QnYrbUppOVhnNHVLbXlVNnFubjd6RUtqalkyYklsQ3dleng0TExHUDVIRDNacjZmWnNtZEcxVUh6RHNCdllHMm9DSTBvcUVLTSt2YWxzVDZoVThNUmpiUUJEL0pKOEpKTi9pQkdsaEJBVUhvMFVtaGh0UjJldVRWcFpXbE1uaG1wWEFkS0RYU2ZQZGFQb2hnQiIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJpZCI6InRvb2x1XzAxWEVBdWhRSENCZHlDd3R5bVp3RmZEaSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3JlZF90ZWFtIiwidHlwZSI6InRvb2xfdXNlIiwidG9vbHNldF9uYW1lIjpudWxsfV0sImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2Zkd0FzYWJEcXVlZ0hpNllVcENuIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjc1MDksIm91dHB1dF90b2tlbnMiOjg4LCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjo1NX0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoidmVyaWZpZXIiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtNjY4Mi03ZDIxLThjMTUtZmZiY2U5NDIyYzlmLTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoidHJhbnNmZXJfdG9fcmVkX3RlYW0iLCJhcmdzIjp7fSwiaWQiOiJ0b29sdV8wMVhFQXVoUUhDQmR5Q3d0eW1ad0ZmRGkiLCJ0eXBlIjoidG9vbF9jYWxsIn1dLCJpbnZhbGlkX3Rvb2xfY2FsbHMiOltdLCJ1c2FnZV9tZXRhZGF0YSI6eyJpbnB1dF90b2tlbnMiOjc1MDksIm91dHB1dF90b2tlbnMiOjg4LCJ0b3RhbF90b2tlbnMiOjc1OTcsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjU1fX19LHsiY29udGVudCI6IlN1Y2Nlc3NmdWxseSB0cmFuc2ZlcnJlZCB0byByZWRfdGVhbSIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ0cmFuc2Zlcl90b19yZWRfdGVhbSIsImlkIjoiOTgyYzAxMjYtZTZhYi00NThhLTkyMmUtN2IyNjZlODNjMmIyIiwidG9vbF9jYWxsX2lkIjoidG9vbHVfMDFYRUF1aFFIQ0JkeUN3dHltWndGZkRpIiwic3RhdHVzIjoic3VjY2VzcyJ9LHsiY29udGVudCI6W3sic2lnbmF0dXJlIjoiQ0FRU3dnY0tFQWdTR0FJNEFVSUlkR2hwYm10cGJtY1NET1JYZ2U5dUwxODMyUk1Rd0JvTWpRd0F3QUhiOWlQMW9qUEdJakREU2NhbDQ4NmJlQ0pidEc4L0QyWTlnOVM1eElQZmpvSGcvZHh3WDVIeXBTUUVCYmpGaVRTYlBFQ1hTRnpWNllJcTN3YkxjbXlQYnZ1c0duRGV4Rmd4KzNzSVh6dnF5RlYvNjl4aGloTW5TcGZUQ0h5MCswelN1MVdwWVUvV1VwSWZvYVNYVGNUYTFxa004eDhFay95LzFqa0tNWlVmREZhOTN1aFVKeE9DNCtRZ1BXQmwvK1h5MnQ4MkwyeC9lYnE3UUxRVHF0UjBqWGVhVVJRbTNDbzZKdk9nV21Edk1wRWRUZVRXRTNmRGgxdHp6VktzZmx1bTRkODliNkNhS3RuOG5sUWQ3UmhOOTl2TlFNUUhHNVY0T3VWTmRySFBaWnBGT3VoZGVqc01GV1h5Qis1NXNsR0FkOHdFWGdGS2JJL2tuT1hWR3pML05ERjhHdHkxdE5abHUyZmRTQ0FzWVdNc0ZRUDFjYndMZVFHc3p5WHpKTVZaSjJJOXhCUGt5ejFIT1hDUHloNUYrajJnUlRQaWtWQjNBUkM2dGZCL2xEU1VlS0JWWlBRUzhwWS9CNGVDcytJamphQk5XcnVMemZxQ3NjNnhMMEN1UVBTWXlRWGFZRmpZclNSTmNDNzBXUUJXVDVWVmhwMG1BOXlaREdZYlJadDlJNU9LOVFrUW11SjV6REZhTUVPWHpTcnNDNGx0ZVA2TWRpbWlNaWp2c0gzaXErYzZXVVlvbFJqM25lSTg4RWFISXpMQUdScTY0Qk5DTHpkQVlMMnBFVTJzYjV1Rzc4UEtWb1VVQ2JWSkw1anBET2hMT1RPMWJRc1R1b1VwblJSckxRRC9rSXpWK2xqcFZCd3dmSmN5cTc1VEpBL0N0UHIveHIyU1o5QjFiMnNiakc2akQwbGh0b0NkZGVEZ2NxZ2wvTFNTa2N1YVlZOHhQV0N5K0x0TmhjcVRoZm1mdm5ITEpJcEw2Z2pialcvT1FwUmZWVXdmdzFqT2I5YldWd2NnSGhOWWZLWUdQdE04b3RoNXh3TFlSUlZnenFNS092czdmYWxYUmhWeloyU1A2SXcwQk1tQUFaNkhqZkNsOUwrdFBxSk5sblFRT0hwb1dqdW00QitYc1lsZEZFS0dRV1Fxc2Q1OWlOUHZhbzlwL3NDT3Q3ckdxQ296Y21naUVtNHc2NmV6L1FJRHliNDZuMCt0Y05KUFVjb0JLdlEvakR3RTZWd21rUS9uRUNqRnAzVWREVCtrV1lRU05kOGo4VkJaT3BHOExscVVZckVaQ3YwcTdCczF2Y05yZFlpYUZvUzMyb3NheW5XaytIdDNPSHVGV2JFRjhKbHNGVFFBTzA1VnZZa2VjZjBWL29USHNHNWh5T09zN1VFYURGcTd1YUp1eldWaGg1cEFvaklhQmNSOGZWbG4yUExSVFg3aXMybnNBK1FyZGlkZVBjWnRGdGd4QWlLWWUxaTJzU0ZpNHZtOEVPa0NiUTYzd2RRZVk0VXJyNVB1b01Fd3hVVUxqZ0thZ2lBbXNKRHE0UnJWbTdzTzQrNll1QTlqNmwydXlsa09FYzY3dmYvNWZrWHVDc2lBcE5MNmtlYjhjK3ZMMG40RlhHZHpqK2FwU0RmRkdCZ0IiLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsiaWQiOiJ0b29sdV8wMVZCcldkV0VXWWdpdDRrTlllaEJNVTUiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6e30sIm5hbWUiOiJ0cmFuc2Zlcl90b19lZGl0b3IiLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QXpmanFVb3BvRW1aTGFxbTEiLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzQxNiwib3V0cHV0X3Rva2VucyI6MTUxLCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjoxMTl9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InJlZF90ZWFtIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTZjZTYtNzI1MS1iNzk2LTRmNjQzMzNmNjdjNi0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6InRyYW5zZmVyX3RvX2VkaXRvciIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxVkJyV2RXRVdZZ2l0NGtOWWVoQk1VNSIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzQxNiwib3V0cHV0X3Rva2VucyI6MTUxLCJ0b3RhbF90b2tlbnMiOjc1NjcsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjExOX19fSx7ImNvbnRlbnQiOiJTdWNjZXNzZnVsbHkgdHJhbnNmZXJyZWQgdG8gZWRpdG9yIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJ0b29sIiwibmFtZSI6InRyYW5zZmVyX3RvX2VkaXRvciIsImlkIjoiNWQzOWUzNmItYzViNS00NDI2LWE5YWEtYmUyZjdkMzgxNmQ2IiwidG9vbF9jYWxsX2lkIjoidG9vbHVfMDFWQnJXZFdFV1lnaXQ0a05ZZWhCTVU1Iiwic3RhdHVzIjoic3VjY2VzcyJ9XX0=" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJvdXRwdXQiOlt7ImdyYXBoIjpudWxsLCJ1cGRhdGUiOnsibWVzc2FnZXMiOlt7ImNvbnRlbnQiOlt7InNpZ25hdHVyZSI6IkNBUVM1Z1lLRUFnU0dBSTRBVUlJZEdocGJtdHBibWNTREk0UUliN0wydzVhVzNjTS9Sb01MaTJ3RXhEa3FodXhKRGlZSWpBcUd4TTRXVXdpcTR1WkhyaUNidUM0a1Z3YTdpZ2RVTG9vY1piaTVmZXZLYmZ3cUhTYnJMTlQ1MU5HWDRYc0x0c3Fnd1kvdGFiSGpsbHpxdTduUld2OCtGejRSNm45amE3Q3BqMjRadXhGdUtra1FPQm1BNFRmMWFwazNSUHFjRVErait3ZWR1MGxPK3JkRUN5ZDNsWGhmZ0VSRkZxY002YXVjeGVsNFJidzVvQUhYdTkrWGVUdHpsK044dEFlWjlpR1oyWFR0VTVPYjhhUFQxS3VYK3VwOGtRNFZVWGtCVGZkMmY1SlA5UnhaRXE5d1NZYVpzeUhkdjBuKzFVNDZBT3N2S1ptcDlRWEJ3bzJmNE85MGpQOGVnaXJlbTlFYlpibWFZT0o4V0s3Q3RLUlZiWTJ5NCs5eFJVSjA0bnNkSXo1RjVGRktFeHhOT3JCNG1jL05FODNhYjNaNjRpV3pPd3FvVUdBR1FTa2FIMGQ1Y0V1bVpvNGZaWkFoUHl1SmVRRHh1bE1DZXduN0VHQ25wSncvbnppMHdveUVhUUw0WTdoOTlQN2FYeEZyMjhwNUtDeUpFSlREZWZlSXBWbEF3TUNBanoreEM2OUFBTG13eVRRK2NGRU0rS0FnZUlTcGhMVnk4QVByMkZTaGF4Qk5yWFFnejFzOXMyZGhMWlFPQml3emh4bUVnZEs3KzhKYXFoQjBlRUVkLzFSVkF1SVlkZXpBNUhCcndFOUR5UWlDbDNVT3NiU25ORVZlK2dySHA1cDN0bU1CcTY4Z0xWODJvQXpGWVpsK0Nld3g3MXY1ZFBGT3ZLUEVIVXpwUVlPVkxybkIxRVpSWXJaejcybjJ3ZlNWeEJPenBUY25CeFY2SGVkQ1B5SXpLWFpNOSt5RWVUSmk4UXR2VmdaMXcrdjBobUNGR3BubVpON21YbGdxTDFzNk1YVnlkcFBPS1BsTTNOYTdDZEhuUEZZaXdyMnpMdmtrY0lNN0Nzay9DMytaTUFXVEsrQzlCMXZiR3hQbHRFM092OGtPOThQZ1NYbTFBSnVFYlh5K3IzYTNEM1FsWkozZEVLZTJMTmRBQnlyUE1oU2NLVThHbGZILzlOdGdyaHRha2dsbEQvTWZBbFRzYk53TkRyVXRXUnhtRHVKLzBaK2hray9ZTndFRFlBWmE4VG13Vm9XT2RCSHBSUEh2WjlFOXVzZXVsdG1hK0prMWdSTER0dkVaa3FYVUVTa01jcVV0VndZTTVhakpBNU41MmNOaVoxNUJUZjhYakdoS1dkT1Y0MCs4SDBQT1N4TTJKaWZ3Q08wQzV4Rlc1Qm8zSWlJTnYwVU90WTVmOGt2UGRNM1Bqa2hhT3JLMXlUQThlRUU0NHZ1bkw5UGtvR2VjYlltK1hFNUt6TDNRc0VuMlkxSUp3Tzl3MHhIWFFaRWVpUDUrMHUzWW5EYzhxOWRCTGhHVzNGeTMwZ1lBUT09IiwidGhpbmtpbmciOiIiLCJ0eXBlIjoidGhpbmtpbmcifSx7ImlkIjoidG9vbHVfMDFCbThBVTlFc1FVd3FvVkxZcHhLQTc3IiwiY2FsbGVyIjp7InR5cGUiOiJkaXJlY3QifSwiaW5wdXQiOnsiZmlsZV9wYXRoIjoiL2Fuc3dlci5tZCIsImNvbnRlbnQiOiIqKlR1cGxlIHZzLiBsaXN0IGltbXV0YWJpbGl0eSAoUHl0aG9uKSoqXG5cbkEgdHVwbGUgaXMgaW1tdXRhYmxlOiBvbmNlIGNyZWF0ZWQsIHlvdSBjYW4ndCBhc3NpZ24gdG8gaXRzIGl0ZW1zLCBhbmQgdGhlIGNvbGxlY3Rpb24gb2Ygb2JqZWN0cyBpdCByZWZlcmVuY2VzIGNhbid0IGNoYW5nZS4gYHRbMF0gPSAxYCByYWlzZXMgYFR5cGVFcnJvcmAuIEEgbGlzdCBpcyBtdXRhYmxlOiB5b3UgY2FuIGNoYW5nZSBpdCBpbiBwbGFjZSB3aXRoIGluZGV4IG9yIHNsaWNlIGFzc2lnbm1lbnQsIGBhcHBlbmQoKWAsIGBleHRlbmQoKWAsIGBzb3J0KClgIGFuZCBzaW1pbGFyIG1ldGhvZHMuXG5cblR1cGxlIGltbXV0YWJpbGl0eSBpcyBzaGFsbG93LiBJZiBhIHR1cGxlIGhvbGRzIGEgbXV0YWJsZSBvYmplY3QsIHN1Y2ggYXMgYSBsaXN0LCB0aGF0IGlubmVyIG9iamVjdCBjYW4gc3RpbGwgY2hhbmdlLCBhbmQgc28gdGhlIHR1cGxlJ3MgdmFsdWUgY2hhbmdlcy4gQmVjYXVzZSBvZiB0aGlzLCBhIHR1cGxlIGNhbiBiZSBhIGBkaWN0YCBrZXkgb3IgYHNldGAgbWVtYmVyIG9ubHkgaWYgZXZlcnl0aGluZyBpbnNpZGUgaXQgaXMgaGFzaGFibGUuIEEgbGlzdCBjYW4gbmV2ZXIgYmUgYSBrZXkuXG5cblNvdXJjZXM6XG4tIGh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFxuLSBodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3JlZmVyZW5jZS9kYXRhbW9kZWwuaHRtbFxuLSBodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL2xpYnJhcnkvc3RkdHlwZXMuaHRtbFxuIn0sIm5hbWUiOiJ3cml0ZV9maWxlIiwidHlwZSI6InRvb2xfdXNlIiwidG9vbHNldF9uYW1lIjpudWxsfV0sImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2Zkd0JCYWE5Z2pNQ2NlMTZUVWVuIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjgxMjcsIm91dHB1dF90b2tlbnMiOjQzMywib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6Nzl9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6ImVkaXRvciIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy03NmU5LTdjYTMtYTVmZi02M2RlNjZkZDhkZDEtMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ3cml0ZV9maWxlIiwiYXJncyI6eyJmaWxlX3BhdGgiOiIvYW5zd2VyLm1kIiwiY29udGVudCI6IioqVHVwbGUgdnMuIGxpc3QgaW1tdXRhYmlsaXR5IChQeXRob24pKipcblxuQSB0dXBsZSBpcyBpbW11dGFibGU6IG9uY2UgY3JlYXRlZCwgeW91IGNhbid0IGFzc2lnbiB0byBpdHMgaXRlbXMsIGFuZCB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGl0IHJlZmVyZW5jZXMgY2FuJ3QgY2hhbmdlLiBgdFswXSA9IDFgIHJhaXNlcyBgVHlwZUVycm9yYC4gQSBsaXN0IGlzIG11dGFibGU6IHlvdSBjYW4gY2hhbmdlIGl0IGluIHBsYWNlIHdpdGggaW5kZXggb3Igc2xpY2UgYXNzaWdubWVudCwgYGFwcGVuZCgpYCwgYGV4dGVuZCgpYCwgYHNvcnQoKWAgYW5kIHNpbWlsYXIgbWV0aG9kcy5cblxuVHVwbGUgaW1tdXRhYmlsaXR5IGlzIHNoYWxsb3cuIElmIGEgdHVwbGUgaG9sZHMgYSBtdXRhYmxlIG9iamVjdCwgc3VjaCBhcyBhIGxpc3QsIHRoYXQgaW5uZXIgb2JqZWN0IGNhbiBzdGlsbCBjaGFuZ2UsIGFuZCBzbyB0aGUgdHVwbGUncyB2YWx1ZSBjaGFuZ2VzLiBCZWNhdXNlIG9mIHRoaXMsIGEgdHVwbGUgY2FuIGJlIGEgYGRpY3RgIGtleSBvciBgc2V0YCBtZW1iZXIgb25seSBpZiBldmVyeXRoaW5nIGluc2lkZSBpdCBpcyBoYXNoYWJsZS4gQSBsaXN0IGNhbiBuZXZlciBiZSBhIGtleS5cblxuU291cmNlczpcbi0gaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy90dXRvcmlhbC9kYXRhc3RydWN0dXJlcy5odG1sXG4tIGh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvcmVmZXJlbmNlL2RhdGFtb2RlbC5odG1sXG4tIGh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvbGlicmFyeS9zdGR0eXBlcy5odG1sXG4ifSwiaWQiOiJ0b29sdV8wMUJtOEFVOUVzUVV3cW9WTFlweEtBNzciLCJ0eXBlIjoidG9vbF9jYWxsIn1dLCJpbnZhbGlkX3Rvb2xfY2FsbHMiOltdLCJ1c2FnZV9tZXRhZGF0YSI6eyJpbnB1dF90b2tlbnMiOjgxMjcsIm91dHB1dF90b2tlbnMiOjQzMywidG90YWxfdG9rZW5zIjo4NTYwLCJpbnB1dF90b2tlbl9kZXRhaWxzIjp7ImNhY2hlX3JlYWQiOjAsImNhY2hlX2NyZWF0aW9uIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VuX2RldGFpbHMiOnsicmVhc29uaW5nIjo3OX19fV19LCJyZXN1bWUiOm51bGwsImdvdG8iOltdfV19" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "fc98979761fe2c4e", + "parentSpanId": "d1e910014914b0a2", + "name": "FilesystemMiddleware.wrap_model_call", + "kind": 1, + "startTimeUnixNano": "1790968035044310016", + "endTimeUnixNano": "1790968039090991104", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "e30=" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "FilesystemMiddleware.wrap_model_call" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "1" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "editor:42b8f95e-d208-6d0d-c59e-cc043f552134|model:5f0cee1e-bfe5-1171-9153-91c4b775587c" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "editor" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "editor:42b8f95e-d208-6d0d-c59e-cc043f552134" + } + }, + { + "key": "langsmith.metadata.ls_method", + "value": { + "stringValue": "traceable" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJvdXRwdXQiOnsicmVzdWx0IjpbeyJjb250ZW50IjpbeyJzaWduYXR1cmUiOiJDQVFTNWdZS0VBZ1NHQUk0QVVJSWRHaHBibXRwYm1jU0RJNFFJYjdMMnc1YVczY00vUm9NTGkyd0V4RGtxaHV4SkRpWUlqQXFHeE00V1V3aXE0dVpIcmlDYnVDNGtWd2E3aWdkVUxvb2NaYmk1ZmV2S2Jmd3FIU2JyTE5UNTFOR1g0WHNMdHNxZ3dZL3RhYkhqbGx6cXU3blJXdjgrRno0UjZuOWphN0NwajI0WnV4RnVLa2tRT0JtQTRUZjFhcGszUlBxY0VRK2ord2VkdTBsTytyZEVDeWQzbFhoZmdFUkZGcWNNNmF1Y3hlbDRSYnc1b0FIWHU5K1hlVHR6bCtOOHRBZVo5aUdaMlhUdFU1T2I4YVBUMUt1WCt1cDhrUTRWVVhrQlRmZDJmNUpQOVJ4WkVxOXdTWWFac3lIZHYwbisxVTQ2QU9zdktabXA5UVhCd28yZjRPOTBqUDhlZ2lyZW05RWJaYm1hWU9KOFdLN0N0S1JWYlkyeTQrOXhSVUowNG5zZEl6NUY1RkZLRXh4Tk9yQjRtYy9ORTgzYWIzWjY0aVd6T3dxb1VHQUdRU2thSDBkNWNFdW1abzRmWlpBaFB5dUplUUR4dWxNQ2V3bjdFR0NucEp3L256aTB3b3lFYVFMNFk3aDk5UDdhWHhGcjI4cDVLQ3lKRUpURGVmZUlwVmxBd01DQWp6K3hDNjlBQUxtd3lUUStjRkVNK0tBZ2VJU3BoTFZ5OEFQcjJGU2hheEJOclhRZ3oxczlzMmRoTFpRT0Jpd3poeG1FZ2RLNys4SmFxaEIwZUVFZC8xUlZBdUlZZGV6QTVIQnJ3RTlEeVFpQ2wzVU9zYlNuTkVWZStnckhwNXAzdG1NQnE2OGdMVjgyb0F6RllabCtDZXd4NzF2NWRQRk92S1BFSFV6cFFZT1ZMcm5CMUVaUllyWno3Mm4yd2ZTVnhCT3pwVGNuQnhWNkhlZENQeUl6S1haTTkreUVlVEppOFF0dlZnWjF3K3YwaG1DRkdwbm1aTjdtWGxncUwxczZNWFZ5ZHBQT0tQbE0zTmE3Q2RIblBGWWl3cjJ6THZra2NJTTdDc2svQzMrWk1BV1RLK0M5QjF2Ykd4UGx0RTNPdjhrTzk4UGdTWG0xQUp1RWJYeStyM2EzRDNRbFpKM2RFS2UyTE5kQUJ5clBNaFNjS1U4R2xmSC85TnRncmh0YWtnbGxEL01mQWxUc2JOd05EclV0V1J4bUR1Si8wWitoa2svWU53RURZQVphOFRtd1ZvV09kQkhwUlBIdlo5RTl1c2V1bHRtYStKazFnUkxEdHZFWmtxWFVFU2tNY3FVdFZ3WU01YWpKQTVONTJjTmlaMTVCVGY4WGpHaEtXZE9WNDArOEgwUE9TeE0ySmlmd0NPMEM1eEZXNUJvM0lpSU52MFVPdFk1ZjhrdlBkTTNQamtoYU9ySzF5VEE4ZUVFNDR2dW5MOVBrb0dlY2JZbStYRTVLekwzUXNFbjJZMUlKd085dzB4SFhRWkVlaVA1KzB1M1luRGM4cTlkQkxoR1czRnkzMGdZQVE9PSIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJpZCI6InRvb2x1XzAxQm04QVU5RXNRVXdxb1ZMWXB4S0E3NyIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7ImZpbGVfcGF0aCI6Ii9hbnN3ZXIubWQiLCJjb250ZW50IjoiKipUdXBsZSB2cy4gbGlzdCBpbW11dGFiaWxpdHkgKFB5dGhvbikqKlxuXG5BIHR1cGxlIGlzIGltbXV0YWJsZTogb25jZSBjcmVhdGVkLCB5b3UgY2FuJ3QgYXNzaWduIHRvIGl0cyBpdGVtcywgYW5kIHRoZSBjb2xsZWN0aW9uIG9mIG9iamVjdHMgaXQgcmVmZXJlbmNlcyBjYW4ndCBjaGFuZ2UuIGB0WzBdID0gMWAgcmFpc2VzIGBUeXBlRXJyb3JgLiBBIGxpc3QgaXMgbXV0YWJsZTogeW91IGNhbiBjaGFuZ2UgaXQgaW4gcGxhY2Ugd2l0aCBpbmRleCBvciBzbGljZSBhc3NpZ25tZW50LCBgYXBwZW5kKClgLCBgZXh0ZW5kKClgLCBgc29ydCgpYCBhbmQgc2ltaWxhciBtZXRob2RzLlxuXG5UdXBsZSBpbW11dGFiaWxpdHkgaXMgc2hhbGxvdy4gSWYgYSB0dXBsZSBob2xkcyBhIG11dGFibGUgb2JqZWN0LCBzdWNoIGFzIGEgbGlzdCwgdGhhdCBpbm5lciBvYmplY3QgY2FuIHN0aWxsIGNoYW5nZSwgYW5kIHNvIHRoZSB0dXBsZSdzIHZhbHVlIGNoYW5nZXMuIEJlY2F1c2Ugb2YgdGhpcywgYSB0dXBsZSBjYW4gYmUgYSBgZGljdGAga2V5IG9yIGBzZXRgIG1lbWJlciBvbmx5IGlmIGV2ZXJ5dGhpbmcgaW5zaWRlIGl0IGlzIGhhc2hhYmxlLiBBIGxpc3QgY2FuIG5ldmVyIGJlIGEga2V5LlxuXG5Tb3VyY2VzOlxuLSBodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3R1dG9yaWFsL2RhdGFzdHJ1Y3R1cmVzLmh0bWxcbi0gaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9yZWZlcmVuY2UvZGF0YW1vZGVsLmh0bWxcbi0gaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9saWJyYXJ5L3N0ZHR5cGVzLmh0bWxcbiJ9LCJuYW1lIjoid3JpdGVfZmlsZSIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHdCQmFhOWdqTUNjZTE2VFVlbiIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo4MTI3LCJvdXRwdXRfdG9rZW5zIjo0MzMsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjc5fSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCIsIm1vZGVsX3Byb3ZpZGVyIjoiYW50aHJvcGljIn0sInR5cGUiOiJhaSIsIm5hbWUiOiJlZGl0b3IiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtNzZlOS03Y2EzLWE1ZmYtNjNkZTY2ZGQ4ZGQxLTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoid3JpdGVfZmlsZSIsImFyZ3MiOnsiZmlsZV9wYXRoIjoiL2Fuc3dlci5tZCIsImNvbnRlbnQiOiIqKlR1cGxlIHZzLiBsaXN0IGltbXV0YWJpbGl0eSAoUHl0aG9uKSoqXG5cbkEgdHVwbGUgaXMgaW1tdXRhYmxlOiBvbmNlIGNyZWF0ZWQsIHlvdSBjYW4ndCBhc3NpZ24gdG8gaXRzIGl0ZW1zLCBhbmQgdGhlIGNvbGxlY3Rpb24gb2Ygb2JqZWN0cyBpdCByZWZlcmVuY2VzIGNhbid0IGNoYW5nZS4gYHRbMF0gPSAxYCByYWlzZXMgYFR5cGVFcnJvcmAuIEEgbGlzdCBpcyBtdXRhYmxlOiB5b3UgY2FuIGNoYW5nZSBpdCBpbiBwbGFjZSB3aXRoIGluZGV4IG9yIHNsaWNlIGFzc2lnbm1lbnQsIGBhcHBlbmQoKWAsIGBleHRlbmQoKWAsIGBzb3J0KClgIGFuZCBzaW1pbGFyIG1ldGhvZHMuXG5cblR1cGxlIGltbXV0YWJpbGl0eSBpcyBzaGFsbG93LiBJZiBhIHR1cGxlIGhvbGRzIGEgbXV0YWJsZSBvYmplY3QsIHN1Y2ggYXMgYSBsaXN0LCB0aGF0IGlubmVyIG9iamVjdCBjYW4gc3RpbGwgY2hhbmdlLCBhbmQgc28gdGhlIHR1cGxlJ3MgdmFsdWUgY2hhbmdlcy4gQmVjYXVzZSBvZiB0aGlzLCBhIHR1cGxlIGNhbiBiZSBhIGBkaWN0YCBrZXkgb3IgYHNldGAgbWVtYmVyIG9ubHkgaWYgZXZlcnl0aGluZyBpbnNpZGUgaXQgaXMgaGFzaGFibGUuIEEgbGlzdCBjYW4gbmV2ZXIgYmUgYSBrZXkuXG5cblNvdXJjZXM6XG4tIGh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFxuLSBodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3JlZmVyZW5jZS9kYXRhbW9kZWwuaHRtbFxuLSBodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL2xpYnJhcnkvc3RkdHlwZXMuaHRtbFxuIn0sImlkIjoidG9vbHVfMDFCbThBVTlFc1FVd3FvVkxZcHhLQTc3IiwidHlwZSI6InRvb2xfY2FsbCJ9XSwiaW52YWxpZF90b29sX2NhbGxzIjpbXSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjo4MTI3LCJvdXRwdXRfdG9rZW5zIjo0MzMsInRvdGFsX3Rva2VucyI6ODU2MCwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6Nzl9fX1dLCJzdHJ1Y3R1cmVkX3Jlc3BvbnNlIjpudWxsfX0=" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "4bd8ddfd6bc728ed", + "parentSpanId": "fc98979761fe2c4e", + "name": "UnsupportedContentMiddleware.wrap_model_call", + "kind": 1, + "startTimeUnixNano": "1790968035044775936", + "endTimeUnixNano": "1790968039090832896", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "e30=" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "UnsupportedContentMiddleware.wrap_model_call" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "1" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "editor:42b8f95e-d208-6d0d-c59e-cc043f552134|model:5f0cee1e-bfe5-1171-9153-91c4b775587c" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "editor" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "editor:42b8f95e-d208-6d0d-c59e-cc043f552134" + } + }, + { + "key": "langsmith.metadata.ls_method", + "value": { + "stringValue": "traceable" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJvdXRwdXQiOnsicmVzdWx0IjpbeyJjb250ZW50IjpbeyJzaWduYXR1cmUiOiJDQVFTNWdZS0VBZ1NHQUk0QVVJSWRHaHBibXRwYm1jU0RJNFFJYjdMMnc1YVczY00vUm9NTGkyd0V4RGtxaHV4SkRpWUlqQXFHeE00V1V3aXE0dVpIcmlDYnVDNGtWd2E3aWdkVUxvb2NaYmk1ZmV2S2Jmd3FIU2JyTE5UNTFOR1g0WHNMdHNxZ3dZL3RhYkhqbGx6cXU3blJXdjgrRno0UjZuOWphN0NwajI0WnV4RnVLa2tRT0JtQTRUZjFhcGszUlBxY0VRK2ord2VkdTBsTytyZEVDeWQzbFhoZmdFUkZGcWNNNmF1Y3hlbDRSYnc1b0FIWHU5K1hlVHR6bCtOOHRBZVo5aUdaMlhUdFU1T2I4YVBUMUt1WCt1cDhrUTRWVVhrQlRmZDJmNUpQOVJ4WkVxOXdTWWFac3lIZHYwbisxVTQ2QU9zdktabXA5UVhCd28yZjRPOTBqUDhlZ2lyZW05RWJaYm1hWU9KOFdLN0N0S1JWYlkyeTQrOXhSVUowNG5zZEl6NUY1RkZLRXh4Tk9yQjRtYy9ORTgzYWIzWjY0aVd6T3dxb1VHQUdRU2thSDBkNWNFdW1abzRmWlpBaFB5dUplUUR4dWxNQ2V3bjdFR0NucEp3L256aTB3b3lFYVFMNFk3aDk5UDdhWHhGcjI4cDVLQ3lKRUpURGVmZUlwVmxBd01DQWp6K3hDNjlBQUxtd3lUUStjRkVNK0tBZ2VJU3BoTFZ5OEFQcjJGU2hheEJOclhRZ3oxczlzMmRoTFpRT0Jpd3poeG1FZ2RLNys4SmFxaEIwZUVFZC8xUlZBdUlZZGV6QTVIQnJ3RTlEeVFpQ2wzVU9zYlNuTkVWZStnckhwNXAzdG1NQnE2OGdMVjgyb0F6RllabCtDZXd4NzF2NWRQRk92S1BFSFV6cFFZT1ZMcm5CMUVaUllyWno3Mm4yd2ZTVnhCT3pwVGNuQnhWNkhlZENQeUl6S1haTTkreUVlVEppOFF0dlZnWjF3K3YwaG1DRkdwbm1aTjdtWGxncUwxczZNWFZ5ZHBQT0tQbE0zTmE3Q2RIblBGWWl3cjJ6THZra2NJTTdDc2svQzMrWk1BV1RLK0M5QjF2Ykd4UGx0RTNPdjhrTzk4UGdTWG0xQUp1RWJYeStyM2EzRDNRbFpKM2RFS2UyTE5kQUJ5clBNaFNjS1U4R2xmSC85TnRncmh0YWtnbGxEL01mQWxUc2JOd05EclV0V1J4bUR1Si8wWitoa2svWU53RURZQVphOFRtd1ZvV09kQkhwUlBIdlo5RTl1c2V1bHRtYStKazFnUkxEdHZFWmtxWFVFU2tNY3FVdFZ3WU01YWpKQTVONTJjTmlaMTVCVGY4WGpHaEtXZE9WNDArOEgwUE9TeE0ySmlmd0NPMEM1eEZXNUJvM0lpSU52MFVPdFk1ZjhrdlBkTTNQamtoYU9ySzF5VEE4ZUVFNDR2dW5MOVBrb0dlY2JZbStYRTVLekwzUXNFbjJZMUlKd085dzB4SFhRWkVlaVA1KzB1M1luRGM4cTlkQkxoR1czRnkzMGdZQVE9PSIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJpZCI6InRvb2x1XzAxQm04QVU5RXNRVXdxb1ZMWXB4S0E3NyIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7ImZpbGVfcGF0aCI6Ii9hbnN3ZXIubWQiLCJjb250ZW50IjoiKipUdXBsZSB2cy4gbGlzdCBpbW11dGFiaWxpdHkgKFB5dGhvbikqKlxuXG5BIHR1cGxlIGlzIGltbXV0YWJsZTogb25jZSBjcmVhdGVkLCB5b3UgY2FuJ3QgYXNzaWduIHRvIGl0cyBpdGVtcywgYW5kIHRoZSBjb2xsZWN0aW9uIG9mIG9iamVjdHMgaXQgcmVmZXJlbmNlcyBjYW4ndCBjaGFuZ2UuIGB0WzBdID0gMWAgcmFpc2VzIGBUeXBlRXJyb3JgLiBBIGxpc3QgaXMgbXV0YWJsZTogeW91IGNhbiBjaGFuZ2UgaXQgaW4gcGxhY2Ugd2l0aCBpbmRleCBvciBzbGljZSBhc3NpZ25tZW50LCBgYXBwZW5kKClgLCBgZXh0ZW5kKClgLCBgc29ydCgpYCBhbmQgc2ltaWxhciBtZXRob2RzLlxuXG5UdXBsZSBpbW11dGFiaWxpdHkgaXMgc2hhbGxvdy4gSWYgYSB0dXBsZSBob2xkcyBhIG11dGFibGUgb2JqZWN0LCBzdWNoIGFzIGEgbGlzdCwgdGhhdCBpbm5lciBvYmplY3QgY2FuIHN0aWxsIGNoYW5nZSwgYW5kIHNvIHRoZSB0dXBsZSdzIHZhbHVlIGNoYW5nZXMuIEJlY2F1c2Ugb2YgdGhpcywgYSB0dXBsZSBjYW4gYmUgYSBgZGljdGAga2V5IG9yIGBzZXRgIG1lbWJlciBvbmx5IGlmIGV2ZXJ5dGhpbmcgaW5zaWRlIGl0IGlzIGhhc2hhYmxlLiBBIGxpc3QgY2FuIG5ldmVyIGJlIGEga2V5LlxuXG5Tb3VyY2VzOlxuLSBodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3R1dG9yaWFsL2RhdGFzdHJ1Y3R1cmVzLmh0bWxcbi0gaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9yZWZlcmVuY2UvZGF0YW1vZGVsLmh0bWxcbi0gaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9saWJyYXJ5L3N0ZHR5cGVzLmh0bWxcbiJ9LCJuYW1lIjoid3JpdGVfZmlsZSIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHdCQmFhOWdqTUNjZTE2VFVlbiIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo4MTI3LCJvdXRwdXRfdG9rZW5zIjo0MzMsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjc5fSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCIsIm1vZGVsX3Byb3ZpZGVyIjoiYW50aHJvcGljIn0sInR5cGUiOiJhaSIsIm5hbWUiOiJlZGl0b3IiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtNzZlOS03Y2EzLWE1ZmYtNjNkZTY2ZGQ4ZGQxLTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoid3JpdGVfZmlsZSIsImFyZ3MiOnsiZmlsZV9wYXRoIjoiL2Fuc3dlci5tZCIsImNvbnRlbnQiOiIqKlR1cGxlIHZzLiBsaXN0IGltbXV0YWJpbGl0eSAoUHl0aG9uKSoqXG5cbkEgdHVwbGUgaXMgaW1tdXRhYmxlOiBvbmNlIGNyZWF0ZWQsIHlvdSBjYW4ndCBhc3NpZ24gdG8gaXRzIGl0ZW1zLCBhbmQgdGhlIGNvbGxlY3Rpb24gb2Ygb2JqZWN0cyBpdCByZWZlcmVuY2VzIGNhbid0IGNoYW5nZS4gYHRbMF0gPSAxYCByYWlzZXMgYFR5cGVFcnJvcmAuIEEgbGlzdCBpcyBtdXRhYmxlOiB5b3UgY2FuIGNoYW5nZSBpdCBpbiBwbGFjZSB3aXRoIGluZGV4IG9yIHNsaWNlIGFzc2lnbm1lbnQsIGBhcHBlbmQoKWAsIGBleHRlbmQoKWAsIGBzb3J0KClgIGFuZCBzaW1pbGFyIG1ldGhvZHMuXG5cblR1cGxlIGltbXV0YWJpbGl0eSBpcyBzaGFsbG93LiBJZiBhIHR1cGxlIGhvbGRzIGEgbXV0YWJsZSBvYmplY3QsIHN1Y2ggYXMgYSBsaXN0LCB0aGF0IGlubmVyIG9iamVjdCBjYW4gc3RpbGwgY2hhbmdlLCBhbmQgc28gdGhlIHR1cGxlJ3MgdmFsdWUgY2hhbmdlcy4gQmVjYXVzZSBvZiB0aGlzLCBhIHR1cGxlIGNhbiBiZSBhIGBkaWN0YCBrZXkgb3IgYHNldGAgbWVtYmVyIG9ubHkgaWYgZXZlcnl0aGluZyBpbnNpZGUgaXQgaXMgaGFzaGFibGUuIEEgbGlzdCBjYW4gbmV2ZXIgYmUgYSBrZXkuXG5cblNvdXJjZXM6XG4tIGh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFxuLSBodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3JlZmVyZW5jZS9kYXRhbW9kZWwuaHRtbFxuLSBodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL2xpYnJhcnkvc3RkdHlwZXMuaHRtbFxuIn0sImlkIjoidG9vbHVfMDFCbThBVTlFc1FVd3FvVkxZcHhLQTc3IiwidHlwZSI6InRvb2xfY2FsbCJ9XSwiaW52YWxpZF90b29sX2NhbGxzIjpbXSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjo4MTI3LCJvdXRwdXRfdG9rZW5zIjo0MzMsInRvdGFsX3Rva2VucyI6ODU2MCwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6Nzl9fX1dLCJzdHJ1Y3R1cmVkX3Jlc3BvbnNlIjpudWxsfX0=" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "e90628af060558bc", + "parentSpanId": "4bd8ddfd6bc728ed", + "name": "ChatAnthropic", + "kind": 1, + "startTimeUnixNano": "1790968035049609984", + "endTimeUnixNano": "1790968039090351104", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chat" + } + }, + { + "key": "gen_ai.serialized.name", + "value": { + "stringValue": "ChatAnthropic" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "llm" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "ChatAnthropic" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "anthropic" + } + }, + { + "key": "gen_ai.request.model", + "value": { + "stringValue": "claude-sonnet" + } + }, + { + "key": "gen_ai.tool.definitions", + "value": { + "stringValue": "[{\"name\":\"ls\",\"input_schema\":{\"properties\":{\"path\":{\"description\":\"Absolute path to the directory to list. Must be absolute, not relative.\",\"type\":\"string\"}},\"required\":[\"path\"],\"type\":\"object\"},\"description\":\"Lists all files in a directory.\\n\\nThis is useful for exploring the filesystem and finding the right file to read or edit.\\nYou should almost ALWAYS use this tool before using the read_file or edit_file tools.\"},{\"name\":\"read_file\",\"input_schema\":{\"properties\":{\"file_path\":{\"description\":\"Absolute path to the file to read. Must be absolute, not relative.\",\"type\":\"string\"},\"offset\":{\"default\":0,\"description\":\"Line number to start reading from (0-indexed). Use for pagination of large files.\",\"type\":\"integer\"},\"limit\":{\"default\":100,\"description\":\"Maximum number of lines to read. Use for pagination of large files.\",\"type\":\"integer\"}},\"required\":[\"file_path\"],\"type\":\"object\"},\"description\":\"Reads a file from the filesystem. Assume any path the user provides is valid; reading a missing file returns an error.\\n\\nUsage:\\n- By default, it reads up to 100 lines starting from the beginning of the file. Use `offset`/`limit` to page through large files instead of reading them whole.\\n- A status header, `@@ field | field | ... @@`, sits above the file content, and every line after it is verbatim file content. When content is truncated, there may be an explanation before the header. Never include the header when editing.\\n- Speculatively batch multiple `read_file` calls in one response when several files may be useful.\\n- An empty file returns a system-reminder warning in place of contents.\\n- Large tool results may be offloaded to a file; the tool message gives the path. Read that path here, paging with `offset`/`limit`.\\n- Images (`.png`, `.jpg`, etc.), audio, video, and PDFs return multimodal content blocks (https://docs.langchain.com/oss/python/langchain/messages#multimodal).\\n- For images and PDFs, pagination via `offset`/`limit` is text-only - supply `file_path` only\\n- Always read a file before editing it.\"},{\"name\":\"write_file\",\"input_schema\":{\"properties\":{\"file_path\":{\"description\":\"Absolute path where the file should be written. Must be absolute, not relative.\",\"type\":\"string\"},\"content\":{\"description\":\"The text content to write to the file. This parameter is required.\",\"type\":\"string\"}},\"required\":[\"file_path\",\"content\"],\"type\":\"object\"},\"description\":\"Writes content to a file. Creates the file if it does not exist; replaces it entirely if it does.\\n\\nUsage:\\n- Use this tool when you intend to create a new file or replace the whole file. You do not need to read the file first.\\n- Prefer to edit existing files (with the edit_file tool) over creating new ones when possible.\"},{\"name\":\"edit_file\",\"input_schema\":{\"properties\":{\"file_path\":{\"description\":\"Absolute path to the file to edit. Must be absolute, not relative.\",\"type\":\"string\"},\"old_string\":{\"description\":\"The exact text to find and replace. Must be unique in the file unless replace_all is True.\",\"type\":\"string\"},\"new_string\":{\"description\":\"The text to replace old_string with. Must be different from old_string.\",\"type\":\"string\"},\"replace_all\":{\"default\":false,\"description\":\"If True, replace all occurrences of old_string. If False (default), old_string must be unique.\",\"type\":\"boolean\"}},\"required\":[\"file_path\",\"old_string\",\"new_string\"],\"type\":\"object\"},\"description\":\"Performs exact string replacements in files.\\n\\nUsage:\\n- You must read the file before editing; this tool errors otherwise.\\n- Preserve the exact source indentation from the read output, and never include the read status header in old_string or new_string.\\n- Prefer editing an existing file over creating a new one.\\n- Only use emojis if the user explicitly requests it.\"},{\"name\":\"glob\",\"input_schema\":{\"properties\":{\"pattern\":{\"description\":\"Glob pattern to match files (e.g., '*.py', '**/*.py', '/subdir/**/*.md'). A pattern without '/' matches the file name at any depth; a pattern containing '/' matches the search-root-relative path; a leading '/' anchors to the search root ('/*.py' matches only top-level files). Leading-dot names are excluded unless the pattern segment starts with '.', so prefer the bare form '*.py' over '**/*.py' -- '**' will not descend into dot-directories like '.github'.\",\"type\":\"string\"},\"path\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"null\"}],\"default\":null,\"description\":\"Base directory to search from. Defaults to the backend's default root.\"}},\"required\":[\"pattern\"],\"type\":\"object\"},\"description\":\"Find files matching a glob pattern, returning absolute paths.\\n\\nSupports `*` (any characters within a path segment), `**` (any directories), `?` (single character), `[abc]` (one character from a set), and `{a,b}` (alternatives), e.g. `*.py`, `src/**/*.py`, `*.{yml,yaml}`.\\n\\nA pattern without `/` matches the file name at any depth under the search root (`*.py` matches `src/app/main.py`). A pattern containing `/` matches the search-root-relative path (`src/**/*.py`). A leading `/` anchors to the search root (`/*.py` matches only top-level Python files).\\n\\nLeading-dot names are only matched when the pattern segment itself starts with `.` (use `.env`, or `.github/**/*.yml`). Because `**` will not descend into dot-directories, the bare form `*.yml` is *broader* than `**/*.yml` and is usually what you want.\"},{\"name\":\"grep\",\"input_schema\":{\"properties\":{\"pattern\":{\"description\":\"Text pattern to search for (literal string, not regex).\",\"type\":\"string\"},\"path\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"null\"}],\"default\":null,\"description\":\"Directory to search in. Defaults to current working directory.\"},\"glob\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"null\"}],\"default\":null,\"description\":\"Glob pattern (NOT regex) limiting which files are searched (e.g. '*.py', '*.ts'). A pattern without '/' matches the file name at any depth; a pattern containing '/' matches the search-root-relative path (e.g. 'src/**/*.py'). This is an in-tool file filter, not a call to the separate glob tool. Brace expansion (e.g. '*.{ts,tsx}') is not supported on all backends; run a separate search per extension for reliable results.\"},\"output_mode\":{\"default\":\"files_with_matches\",\"description\":\"Shape of the returned text. 'files_with_matches' (default): newline-separated matching file paths. 'content': matching lines grouped by file under a ':' header, each line indented and formatted ': ' (only the matched line, no surrounding context). 'count': one ': ' line per file.\",\"enum\":[\"files_with_matches\",\"content\",\"count\"],\"type\":\"string\"},\"max_count\":{\"anyOf\":[{\"exclusiveMinimum\":0,\"type\":\"integer\"},{\"type\":\"null\"}],\"default\":null,\"description\":\"Optional cap on the total number of matches returned across all files. Leave unset to use the configured default. When the cap is hit, results are truncated and a note says so; narrow the pattern or path to see the rest.\"}},\"required\":[\"pattern\"],\"type\":\"object\"},\"description\":\"Search for a LITERAL text pattern across files (NOT regex).\\n\\nThe pattern is matched verbatim: regex metacharacters are ordinary characters, not operators. To match any of several strings, run a separate grep for each; `grep(pattern=\\\"foo|bar\\\")` searches for the literal text \\\"foo|bar\\\", and `.*` or `\\\\.` match those characters literally.\\n\\nReturns matching files or content per `output_mode`. Offloaded large tool results live under the artifacts root (`/large_tool_results/` by default); grep that directory to search them when you do not know the exact path.\"},{\"name\":\"transfer_to_researcher\",\"input_schema\":{\"properties\":{},\"type\":\"object\"},\"description\":\"Request missing evidence from the researcher\"},{\"name\":\"transfer_to_skeptic\",\"input_schema\":{\"properties\":{},\"type\":\"object\"},\"description\":\"Request another critique from the skeptic\"},{\"name\":\"transfer_to_verifier\",\"input_schema\":{\"properties\":{},\"type\":\"object\"},\"description\":\"Request another source check from the verifier\"}]" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_chat_model" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "1" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "editor:42b8f95e-d208-6d0d-c59e-cc043f552134|model:5f0cee1e-bfe5-1171-9153-91c4b775587c" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "editor" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "editor:42b8f95e-d208-6d0d-c59e-cc043f552134" + } + }, + { + "key": "langsmith.metadata.ls_provider", + "value": { + "stringValue": "anthropic" + } + }, + { + "key": "langsmith.metadata.ls_model_name", + "value": { + "stringValue": "claude-sonnet" + } + }, + { + "key": "langsmith.metadata.ls_model_type", + "value": { + "stringValue": "chat" + } + }, + { + "key": "langsmith.metadata.ls_max_tokens", + "value": { + "intValue": "4096" + } + }, + { + "key": "langsmith.metadata.lc_versions", + "value": { + "stringValue": "{\"langchain-core\":\"1.6.6\",\"langchain\":\"1.4.3\",\"langchain-anthropic\":\"1.7.5\"}" + } + }, + { + "key": "langsmith.metadata.ls_method", + "value": { + "stringValue": "traceable" + } + }, + { + "key": "langsmith.metadata.model", + "value": { + "stringValue": "claude-sonnet" + } + }, + { + "key": "langsmith.metadata.max_tokens", + "value": { + "intValue": "4096" + } + }, + { + "key": "langsmith.metadata.model_kwargs", + "value": { + "stringValue": "{\"extra_body\":{\"extra_headers\":{\"anthropic-workspace-id\":\"[redacted workspace]\"}}}" + } + }, + { + "key": "langsmith.metadata.streaming", + "value": { + "boolValue": false + } + }, + { + "key": "langsmith.metadata.max_retries", + "value": { + "intValue": "2" + } + }, + { + "key": "langsmith.metadata._type", + "value": { + "stringValue": "anthropic-chat" + } + }, + { + "key": "langsmith.metadata.usage_metadata", + "value": { + "stringValue": "{\"input_tokens\":8127,\"output_tokens\":433,\"total_tokens\":8560,\"input_token_details\":{\"cache_read\":0,\"cache_creation\":0,\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_token_details\":{\"reasoning\":79}}" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "seq:step:1" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W1t7ImxjIjoxLCJ0eXBlIjoiY29uc3RydWN0b3IiLCJpZCI6WyJsYW5nY2hhaW4iLCJzY2hlbWEiLCJtZXNzYWdlcyIsIlN5c3RlbU1lc3NhZ2UiXSwia3dhcmdzIjp7ImNvbnRlbnQiOiJZb3UgYXJlIHRoZSBlZGl0b3IuIFVzZSB0aGUgc2hhcmVkIGNvbnZlcnNhdGlvbiB0byB3cml0ZSBvbmUgY29uY2lzZSBhbnN3ZXIgd2l0aCBzb3VyY2UgVVJMcy4gV3JpdGUgdGhlIGZpbmFsIGFuc3dlciB0byAvYW5zd2VyLm1kIGluIHRoZSBzaGFyZWQgdmlydHVhbCBmaWxlc3lzdGVtIGJlZm9yZSByZXBseWluZy4gSWYgaW1wb3J0YW50IGlzc3VlcyByZW1haW4sIGhhbmQgb2ZmIHRvIHRoZSByaWdodCBhZ2VudCBiZWZvcmUgYW5zd2VyaW5nLiIsInR5cGUiOiJzeXN0ZW0ifX0seyJsYyI6MSwidHlwZSI6ImNvbnN0cnVjdG9yIiwiaWQiOlsibGFuZ2NoYWluIiwic2NoZW1hIiwibWVzc2FnZXMiLCJIdW1hbk1lc3NhZ2UiXSwia3dhcmdzIjp7ImNvbnRlbnQiOiJGb3IgYSBzeW50aGV0aWMgdHJhY2luZyBmaXh0dXJlLCBleHBsYWluIFB5dGhvbiB0dXBsZSB2ZXJzdXMgbGlzdCBpbW11dGFiaWxpdHkgaW4gYXQgbW9zdCAxMjAgd29yZHMuIFVzZSBvbmx5IG9mZmljaWFsIFB5dGhvbiBkb2N1bWVudGF0aW9uIGlmIHlvdSBzZWFyY2guIEhhdmUgdGhlIHNrZXB0aWMgYW5kIHZlcmlmaWVyIGNoZWNrIGl0LCB0aGVuIGhhdmUgdGhlIGVkaXRvciB3cml0ZSAvYW5zd2VyLm1kLiBLZWVwIHRoZSBydW4gc2hvcnQgYW5kIGRvIG5vdCBhc2sgZm9sbG93LXVwIHF1ZXN0aW9ucy4iLCJ0eXBlIjoiaHVtYW4iLCJpZCI6IjQ2MjA0MzA1LWY4ZWUtNDM4NC05Y2U4LWZjZDgwYmZjZGY3NSJ9fSx7ImxjIjoxLCJ0eXBlIjoiY29uc3RydWN0b3IiLCJpZCI6WyJsYW5nY2hhaW4iLCJzY2hlbWEiLCJtZXNzYWdlcyIsIkFJTWVzc2FnZSJdLCJrd2FyZ3MiOnsiY29udGVudCI6W3siaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9LCJuYW1lIjoid2ViX3NlYXJjaCIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3OWFXOWJyZW5QcWd5YmZOd1AiLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6Mjg0NSwib3V0cHV0X3Rva2VucyI6NzMsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjB9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InJlc2VhcmNoZXIiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtMjFhMy03MDAwLWEzODEtMjg0YTZhMGMwM2Y3LTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoid2ViX3NlYXJjaCIsImFyZ3MiOnsicXVlcnkiOiJkb2NzLnB5dGhvbi5vcmcgdHVwbGVzIGltbXV0YWJsZSBzZXF1ZW5jZXMgbGlzdHMgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgdHV0b3JpYWwifSwiaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJ0eXBlIjoidG9vbF9jYWxsIn1dLCJ1c2FnZV9tZXRhZGF0YSI6eyJpbnB1dF90b2tlbnMiOjI4NDUsIm91dHB1dF90b2tlbnMiOjczLCJ0b3RhbF90b2tlbnMiOjI5MTgsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjB9fSwiaW52YWxpZF90b29sX2NhbGxzIjpbXX19LHsibGMiOjEsInR5cGUiOiJjb25zdHJ1Y3RvciIsImlkIjpbImxhbmdjaGFpbiIsInNjaGVtYSIsIm1lc3NhZ2VzIiwiVG9vbE1lc3NhZ2UiXSwia3dhcmdzIjp7ImNvbnRlbnQiOiJbe1widGl0bGVcIjogXCI1LiBEYXRhIFN0cnVjdHVyZXMgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3R1dG9yaWFsL2RhdGFzdHJ1Y3R1cmVzLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcIllvdSBtaWdodCBoYXZlIG5vdGljZWQgdGhhdCBtZXRob2RzIGxpa2VgaW5zZXJ0YCxgcmVtb3ZlYCBvcmBzb3J0YCB0aGF0IG9ubHkgbW9kaWZ5IHRoZSBsaXN0IGhhdmUgbm8gcmV0dXJuIHZhbHVlIHByaW50ZWQgXFx1MjAxMyB0aGV5IHJldHVybiB0aGUgZGVmYXVsdGBOb25lYC4gWzFdIFRoaXMgaXMgYSBkZXNpZ24gcHJpbmNpcGxlIGZvciBhbGwgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgaW4gUHl0aG9uLlxcbi4uLlxcbiMjIDUuMy4gVHVwbGVzIGFuZCBTZXF1ZW5jZXNcXHUwMGI2XFxuXFxuV2Ugc2F3IHRoYXQgbGlzdHMgYW5kIHN0cmluZ3MgaGF2ZSBtYW55IGNvbW1vbiBwcm9wZXJ0aWVzLCBzdWNoIGFzIGluZGV4aW5nIGFuZCBzbGljaW5nIG9wZXJhdGlvbnMuIFRoZXkgYXJlIHR3byBleGFtcGxlcyBvZiBzZXF1ZW5jZSBkYXRhIHR5cGVzIChzZWUgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBsaXN0LCB0dXBsZSwgcmFuZ2UpLiBTaW5jZSBQeXRob24gaXMgYW4gZXZvbHZpbmcgbGFuZ3VhZ2UsIG90aGVyIHNlcXVlbmNlIGRhdGEgdHlwZXMgbWF5IGJlIGFkZGVkLiBUaGVyZSBpcyBhbHNvIGFub3RoZXIgc3RhbmRhcmQgc2VxdWVuY2UgZGF0YSB0eXBlOiB0aGUgdHVwbGUuXFxuXFxuQSB0dXBsZSBjb25zaXN0cyBvZiBhIG51bWJlciBvZiB2YWx1ZXMgc2VwYXJhdGVkIGJ5IGNvbW1hcywgZm9yIGluc3RhbmNlOlxcbi4uLlxcbj4+PiAjIFR1cGxlcyBhcmUgaW1tdXRhYmxlOlxcbj4+PiB0WzBdID0gODg4ODhcXG5UcmFjZWJhY2sgKG1vc3QgcmVjZW50IGNhbGwgbGFzdCk6XFxuICBGaWxlIFxcXCI8c3RkaW4+XFxcIiwgbGluZSAxLCBpbiA8bW9kdWxlPlxcblR5cGVFcnJvcjogJ3R1cGxlJyBvYmplY3QgZG9lcyBub3Qgc3VwcG9ydCBpdGVtIGFzc2lnbm1lbnRcXG4+Pj4gIyBidXQgdGhleSBjYW4gY29udGFpbiBtdXRhYmxlIG9iamVjdHM6XFxuPj4+IHYgPSAoWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuPj4+IHZcXG4oWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuLi4uXFxuQXMgeW91IHNlZSwgb24gb3V0cHV0IHR1cGxlcyBhcmUgYWx3YXlzIGVuY2xvc2VkIGluIHBhcmVudGhlc2VzLCBzbyB0aGF0IG5lc3RlZCB0dXBsZXMgYXJlIGludGVycHJldGVkIGNvcnJlY3RseTsgdGhleSBtYXkgYmUgaW5wdXQgd2l0aCBvciB3aXRob3V0IHN1cnJvdW5kaW5nIHBhcmVudGhlc2VzLCBhbHRob3VnaCBvZnRlbiBwYXJlbnRoZXNlcyBhcmUgbmVjZXNzYXJ5IGFueXdheSAoaWYgdGhlIHR1cGxlIGlzIHBhcnQgb2YgYSBsYXJnZXIgZXhwcmVzc2lvbikuIEl0IGlzIG5vdCBwb3NzaWJsZSB0byBhc3NpZ24gdG8gdGhlIGluZGl2aWR1YWwgaXRlbXMgb2YgYSB0dXBsZSwgaG93ZXZlciBpdCBpcyBwb3NzaWJsZSB0byBjcmVhdGUgdHVwbGVzIHdoaWNoIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzLCBzdWNoIGFzIGxpc3RzLlxcblxcblRob3VnaCB0dXBsZXMgbWF5IHNlZW0gc2ltaWxhciB0byBsaXN0cywgdGhleSBhcmUgb2Z0ZW4gdXNlZCBpbiBkaWZmZXJlbnQgc2l0dWF0aW9ucyBhbmQgZm9yIGRpZmZlcmVudCBwdXJwb3Nlcy4gVHVwbGVzIGFyZSBpbW11dGFibGUsIGFuZCB1c3VhbGx5IGNvbnRhaW4gYSBoZXRlcm9nZW5lb3VzIHNlcXVlbmNlIG9mIGVsZW1lbnRzIHRoYXQgYXJlIGFjY2Vzc2VkIHZpYSB1bnBhY2tpbmcgKHNlZSBsYXRlciBpbiB0aGlzIHNlY3Rpb24pIG9yIGluZGV4aW5nIChvciBldmVuIGJ5IGF0dHJpYnV0ZSBpbiB0aGUgY2FzZSBvZiBuYW1lZHR1cGxlcykuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG5Bbm90aGVyIHVzZWZ1bCBkYXRhIHR5cGUgYnVpbHQgaW50byBQeXRob24gaXMgdGhlIGRpY3Rpb25hcnkgKHNlZSBNYXBwaW5nIFR5cGVzIFxcdTIwMTQgZGljdCkuIERpY3Rpb25hcmllcyBhcmUgc29tZXRpbWVzIGZvdW5kIGluIG90aGVyIGxhbmd1YWdlcyBhcyBcXHUyMDFjYXNzb2NpYXRpdmUgbWVtb3JpZXNcXHUyMDFkIG9yIFxcdTIwMWNhc3NvY2lhdGl2ZSBhcnJheXNcXHUyMDFkLiBVbmxpa2Ugc2VxdWVuY2VzLCB3aGljaCBhcmUgaW5kZXhlZCBieSBhIHJhbmdlIG9mIG51bWJlcnMsIGRpY3Rpb25hcmllcyBhcmUgaW5kZXhlZCBieSBrZXlzLCB3aGljaCBjYW4gYmUgYW55IGltbXV0YWJsZSB0eXBlOyBzdHJpbmdzIGFuZCBudW1iZXJzIGNhbiBhbHdheXMgYmUga2V5cy4gVHVwbGVzIGNhbiBiZSB1c2VkIGFzIGtleXMgaWYgdGhleSBjb250YWluIG9ubHkgc3RyaW5ncywgbnVtYmVycywgb3IgdHVwbGVzOyBpZiBhIHR1cGxlIGNvbnRhaW5zIGFueSBtdXRhYmxlIG9iamVjdCBlaXRoZXIgZGlyZWN0bHkgb3IgaW5kaXJlY3RseSwgaXQgY2Fubm90IGJlIHVzZWQgYXMgYSBrZXkuIFlvdSBjYW5cXHUyMDE5dCB1c2UgbGlzdHMgYXMga2V5cywgc2luY2UgbGlzdHMgY2FuIGJlIG1vZGlmaWVkIGluIHBsYWNlIHVzaW5nIGluZGV4IGFzc2lnbm1lbnRzLCBzbGljZSBhc3NpZ25tZW50cywgb3IgbWV0aG9kcyBsaWtlIGFwcGVuZCgpIGFuZCBleHRlbmQoKS5cIl19LCB7XCJ0aXRsZVwiOiBcIjMuIERhdGEgbW9kZWwgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3JlZmVyZW5jZS9kYXRhbW9kZWwuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiVGhlIHZhbHVlIG9mIHNvbWUgb2JqZWN0cyBjYW4gY2hhbmdlLiBPYmplY3RzIHdob3NlIHZhbHVlIGNhbiBjaGFuZ2UgYXJlIHNhaWQgdG8gYmUgbXV0YWJsZTsgb2JqZWN0cyB3aG9zZSB2YWx1ZSBpcyB1bmNoYW5nZWFibGUgb25jZSB0aGV5IGFyZSBjcmVhdGVkIGFyZSBjYWxsZWQgaW1tdXRhYmxlLiAoVGhlIHZhbHVlIG9mIGFuIGltbXV0YWJsZSBjb250YWluZXIgb2JqZWN0IHRoYXQgY29udGFpbnMgYSByZWZlcmVuY2UgdG8gYSBtdXRhYmxlIG9iamVjdCBjYW4gY2hhbmdlIHdoZW4gdGhlIGxhdHRlclxcdTIwMTlzIHZhbHVlIGlzIGNoYW5nZWQ7IGhvd2V2ZXIgdGhlIGNvbnRhaW5lciBpcyBzdGlsbCBjb25zaWRlcmVkIGltbXV0YWJsZSwgYmVjYXVzZSB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGl0IGNvbnRhaW5zIGNhbm5vdCBiZSBjaGFuZ2VkLiBTbywgaW1tdXRhYmlsaXR5IGlzIG5vdCBzdHJpY3RseSB0aGUgc2FtZSBhcyBoYXZpbmcgYW4gdW5jaGFuZ2VhYmxlIHZhbHVlLCBpdCBpcyBtb3JlIHN1YnRsZS4pIEFuIG9iamVjdFxcdTIwMTlzIG11dGFiaWxpdHkgaXMgZGV0ZXJtaW5lZCBieSBpdHMgdHlwZTsgZm9yIGluc3RhbmNlLCBudW1iZXJzLCBzdHJpbmdzIGFuZCB0dXBsZXMgYXJlIGltbXV0YWJsZSwgd2hpbGUgZGljdGlvbmFyaWVzIGFuZCBsaXN0cyBhcmUgbXV0YWJsZS5cXG4uLi5cXG5Tb21lIG9iamVjdHMgY29udGFpbiByZWZlcmVuY2VzIHRvIG90aGVyIG9iamVjdHM7IHRoZXNlIGFyZSBjYWxsZWQgY29udGFpbmVycy4gRXhhbXBsZXMgb2YgY29udGFpbmVycyBhcmUgdHVwbGVzLCBsaXN0cyBhbmQgZGljdGlvbmFyaWVzLiBUaGUgcmVmZXJlbmNlcyBhcmUgcGFydCBvZiBhIGNvbnRhaW5lclxcdTIwMTlzIHZhbHVlLiBJbiBtb3N0IGNhc2VzLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIHZhbHVlIG9mIGEgY29udGFpbmVyLCB3ZSBpbXBseSB0aGUgdmFsdWVzLCBub3QgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGNvbnRhaW5lZCBvYmplY3RzOyBob3dldmVyLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIG11dGFiaWxpdHkgb2YgYSBjb250YWluZXIsIG9ubHkgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGltbWVkaWF0ZWx5IGNvbnRhaW5lZCBvYmplY3RzIGFyZSBpbXBsaWVkLiBTbywgaWYgYW4gaW1tdXRhYmxlIGNvbnRhaW5lciAobGlrZSBhIHR1cGxlKSBjb250YWlucyBhIHJlZmVyZW5jZSB0byBhIG11dGFibGUgb2JqZWN0LCBpdHMgdmFsdWUgY2hhbmdlcyBpZiB0aGF0IG11dGFibGUgb2JqZWN0IGlzIGNoYW5nZWQuXFxuLi4uXFxuIyMjIDMuMi41LiBTZXF1ZW5jZXNcXHUwMGI2XFxuLi4uXFxuIyMjIyAzLjIuNS4xLiBJbW11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbkFuIG9iamVjdCBvZiBhbiBpbW11dGFibGUgc2VxdWVuY2UgdHlwZSBjYW5ub3QgY2hhbmdlIG9uY2UgaXQgaXMgY3JlYXRlZC4gKElmIHRoZSBvYmplY3QgY29udGFpbnMgcmVmZXJlbmNlcyB0byBvdGhlciBvYmplY3RzLCB0aGVzZSBvdGhlciBvYmplY3RzIG1heSBiZSBtdXRhYmxlIGFuZCBtYXkgYmUgY2hhbmdlZDsgaG93ZXZlciwgdGhlIGNvbGxlY3Rpb24gb2Ygb2JqZWN0cyBkaXJlY3RseSByZWZlcmVuY2VkIGJ5IGFuIGltbXV0YWJsZSBvYmplY3QgY2Fubm90IGNoYW5nZS4pXFxuXFxuVGhlIGZvbGxvd2luZyB0eXBlcyBhcmUgaW1tdXRhYmxlIHNlcXVlbmNlczpcXG4uLi5cXG5UdXBsZXNcXG46IFRoZSBpdGVtcyBvZiBhIGB0dXBsZWAgYXJlIGFyYml0cmFyeSBQeXRob24gb2JqZWN0cy4gVHVwbGVzIG9mIHR3byBvciBtb3JlIGl0ZW1zIGFyZSBmb3JtZWQgYnkgY29tbWEtc2VwYXJhdGVkIGxpc3RzIG9mIGV4cHJlc3Npb25zLiBBIHR1cGxlIG9mIG9uZSBpdGVtIChhIFxcdTIwMThzaW5nbGV0b25cXHUyMDE5KSBjYW4gYmUgZm9ybWVkIGJ5IGFmZml4aW5nIGEgY29tbWEgdG8gYW4gZXhwcmVzc2lvbiAoYW4gZXhwcmVzc2lvbiBieSBpdHNlbGYgZG9lcyBub3QgY3JlYXRlIGEgdHVwbGUsIHNpbmNlIHBhcmVudGhlc2VzIG11c3QgYmUgdXNhYmxlIGZvciBncm91cGluZyBvZiBleHByZXNzaW9ucykuIEFuIGVtcHR5IHR1cGxlIGNhbiBiZSBmb3JtZWQgYnkgYW4gZW1wdHkgcGFpciBvZiBwYXJlbnRoZXNlcy5cXG4uLi5cXG4jIyMjIDMuMi41LjIuIE11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbk11dGFibGUgc2VxdWVuY2VzIGNhbiBiZSBjaGFuZ2VkIGFmdGVyIHRoZXkgYXJlIGNyZWF0ZWQuIFRoZSBzdWJzY3JpcHRpb24gYW5kIHNsaWNpbmcgbm90YXRpb25zIGNhbiBiZSB1c2VkIGFzIHRoZSB0YXJnZXQgb2YgYXNzaWdubWVudCBhbmQgYGRlbGAgKGRlbGV0ZSkgc3RhdGVtZW50cy5cXG4uLi5cXG5UaGVyZSBhcmUgY3VycmVudGx5IHR3byBpbnRyaW5zaWMgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlczpcXG5cXG5MaXN0c1xcbjogVGhlIGl0ZW1zIG9mIGEgbGlzdCBhcmUgYXJiaXRyYXJ5IFB5dGhvbiBvYmplY3RzLiBMaXN0cyBhcmUgZm9ybWVkIGJ5IHBsYWNpbmcgYSBjb21tYS1zZXBhcmF0ZWQgbGlzdCBvZiBleHByZXNzaW9ucyBpbiBzcXVhcmUgYnJhY2tldHMuIChOb3RlIHRoYXQgdGhlcmUgYXJlIG5vIHNwZWNpYWwgY2FzZXMgbmVlZGVkIHRvIGZvcm0gbGlzdHMgb2YgbGVuZ3RoIDAgb3IgMS4pXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjcgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvYnVpbHRpbnMvc3RkdHlwZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiY29sbGVjdGlvbiBjbGFzc2VzIGFyZSBtdXRhYmxlLiBUaGUgbWV0aG9kcyB0aGF0IGFkZCwgc3VidHJhY3QsIG9yIHJlYXJyYW5nZSB0aGVpciAuLi4gaW4gcGxhY2UsIGFuZCBkb25cXHUyMDE5dCByZXR1cm4gYSAuLi4gLCBuZXZlciByZXR1cm4gdGhlIGNvbGxlY3Rpb24gaW5zdGFuY2UgaXRzZWxmIGJ1dCBgTm9uZWAuXFxuLi4uXFxuIyMgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBgbGlzdGAsIGB0dXBsZWAsIGByYW5nZWBcXHUwMGI2XFxuXFxuVGhlcmUgYXJlIHRocmVlIGJhc2ljIHNlcXVlbmNlIHR5cGVzOiBsaXN0cywgdHVwbGVzLCBhbmQgcmFuZ2Ugb2JqZWN0cy4gQWRkaXRpb25hbCBzZXF1ZW5jZSB0eXBlcyB0YWlsb3JlZCBmb3IgcHJvY2Vzc2luZyBvZiBiaW5hcnkgZGF0YSBhbmQgdGV4dCBzdHJpbmdzIGFyZSBkZXNjcmliZWQgaW4gZGVkaWNhdGVkIHNlY3Rpb25zLlxcbi4uLlxcblRoZSBvcGVyYXRpb25zIGluIHRoZSBmb2xsb3dpbmcgdGFibGUgLi4uIG11dGFibGUgYW5kIGltbXV0YWJsZS4gVGhlIGBjb2xsZWN0aW9ucy4gLi4uIGlzIHByb3ZpZGVkIHRvIG1ha2UgLi4uIGVhc2llciB0byBjb3JyZWN0bHkgaW1wbGVtZW50IHRoZXNlIG9wZXJhdGlvbnMgb24gY3VzdG9tIHNlcXVlbmNlIHR5cGVzXFxuLi4uXFxuIyMjIEltbXV0YWJsZSBTZXF1ZW5jZSBUeXBlc1xcdTAwYjZcXG4uLi5cXG5tdXRhYmxlIHNlcXVlbmNlIHR5cGVzIGlzXFxuLi4uXFxuc3VwcG9ydCBhbGxvd3MgaW1tdXRhYmxlIHNlcXVlbmNlcywgLi4uICwgdG8gYmUgdXNlZCBhcyBgZGljdGAga2V5cyBhbmQgc3RvcmVkIGluIC4uLiBlbnNldGAgaW5zdGFuY2VzXFxuLi4uXFxuIyMjIE11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxuIyMjIExpc3RzXFx1MDBiNlxcblxcbkxpc3RzIGFyZSBtdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaG9tb2dlbmVvdXMgaXRlbXMgKHdoZXJlIHRoZSBwcmVjaXNlIGRlZ3JlZSBvZiBzaW1pbGFyaXR5IHdpbGwgdmFyeSBieSBhcHBsaWNhdGlvbikuXFxuLi4uXFxuIyMjIFR1cGxlc1xcdTAwYjZcXG5cXG5UdXBsZXMgYXJlIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhldGVyb2dlbmVvdXMgZGF0YSAoc3VjaCBhcyB0aGUgMi10dXBsZXMgcHJvZHVjZWQgYnkgdGhlIGBlbnVtZXJhdGUoKWAgYnVpbHQtaW4pLiBUdXBsZXMgYXJlIGFsc28gdXNlZCBmb3IgY2FzZXMgd2hlcmUgYW4gaW1tdXRhYmxlIHNlcXVlbmNlIG9mIGhvbW9nZW5lb3VzIGRhdGEgaXMgbmVlZGVkIChzdWNoIGFzIGFsbG93aW5nIHN0b3JhZ2UgaW4gYSBgc2V0YCBvciBgZGljdGAgaW5zdGFuY2UpLlxcbi4uLlxcbnR1cGxlKGl0ZXJhYmxlPVxcbi4uLlxcblRoZSBjb25zdHJ1Y3RvciBidWlsZHMgYSB0dXBsZSB3aG9zZSBpdGVtcyBhcmUgdGhlIHNhbWUgYW5kIGluIHRoZSBzYW1lIG9yZGVyIGFzIGl0ZXJhYmxlXFx1MjAxOXMgaXRlbXMuIGl0ZXJhYmxlIG1heSBiZSBlaXRoZXIgYSBzZXF1ZW5jZSwgYSBjb250YWluZXIgdGhhdCBzdXBwb3J0cyBpdGVyYXRpb24sIG9yIGFuIGl0ZXJhdG9yIG9iamVjdC4gSWYgaXRlcmFibGUgaXMgYWxyZWFkeSBhIHR1cGxlLCBpdCBpcyByZXR1cm5lZCB1bmNoYW5nZWQuIEZvciBleGFtcGxlLCBgdHVwbGUoJ2FiYycpYCByZXR1cm5zIGAoJ2EnLCAnYicsICdjJylgIGFuZCBgdHVwbGUoIFsxLCAyLCAzXSApYCByZXR1cm5zIGAoMSwgMiwgMylgLiBJZiBubyBhcmd1bWVudCBpcyBnaXZlbiwgdGhlIGNvbnN0cnVjdG9yIGNyZWF0ZXMgYSBuZXcgZW1wdHkgdHVwbGUsIGAoKWAuXFxuLi4uXFxuVHVwbGVzIGltcGxlbWVudCBhbGwgb2YgdGhlIGNvbW1vbiBzZXF1ZW5jZSBvcGVyYXRpb25zLlxcbi4uLlxcbkZvciBoZXRlcm9nZW5lb3VzIGNvbGxlY3Rpb25zIG9mIGRhdGEgd2hlcmUgYWNjZXNzIGJ5IG5hbWUgaXMgY2xlYXJlciB0aGFuIGFjY2VzcyBieSBpbmRleCwgYGNvbGxlY3Rpb25zLm5hbWVkdHVwbGUoKWAgbWF5IGJlIGEgbW9yZSBhcHByb3ByaWF0ZSBjaG9pY2UgdGhhbiBhIHNpbXBsZSB0dXBsZSBvYmplY3QuXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvbGlicmFyeS9zdGR0eXBlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJTb21lIGNvbGxlY3Rpb24gY2xhc3NlcyBhcmUgbXV0YWJsZS4gVGhlIG1ldGhvZHMgdGhhdCBhZGQsIHN1YnRyYWN0LCBvciByZWFycmFuZ2UgdGhlaXIgLi4uIGluIHBsYWNlLCBhbmQgZG9uXFx1MjAxOXQgcmV0dXJuIGEgLi4uICwgbmV2ZXIgcmV0dXJuIHRoZSBjb2xsZWN0aW9uIGluc3RhbmNlIGl0c2VsZiBidXQgYE5vbmVgLlxcbi4uLlxcbiMjIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgYGxpc3RgLCBgdHVwbGVgLCBgcmFuZ2VgXFx1MDBiNlxcblxcblRoZXJlIGFyZSB0aHJlZSBiYXNpYyBzZXF1ZW5jZSB0eXBlczogbGlzdHMsIHR1cGxlcywgYW5kIC4uLiBvYmplY3RzLiBBZGRpdGlvbmFsIHNlcXVlbmNlIHR5cGVzIHRhaWxvcmVkIGZvciBwcm9jZXNzaW5nIG9mIGJpbmFyeSBkYXRhIGFuZCB0ZXh0IHN0cmluZ3MgYXJlIGRlc2NyaWJlZCBpbiBkZWRpY2F0ZWQgc2VjdGlvbnMuXFxuLi4uXFxuVGhlIG9wZXJhdGlvbnMgaW4gdGhlIGZvbGxvd2luZyB0YWJsZSAuLi4gaW1tdXRhYmxlLiBUaGUgYCAuLi4gaXMgcHJvdmlkZWQgdG8gbWFrZSAuLi4gZWFzaWVyIHRvIGNvcnJlY3RseSBpbXBsZW1lbnQgdGhlc2Ugb3BlcmF0aW9ucyBvbiBjdXN0b20gc2VxdWVuY2UgdHlwZXNcXG4uLi5cXG4jIyMgSW1tdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbm9wZXJhdGlvbiB0aGF0IGltbXV0YWJsZSBzZXF1ZW5jZSAuLi4gYnkgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlcyBpcyAuLi4gYGhhc2goKWBcXG4uLi5cXG5UaGlzIHN1cHBvcnQgYWxsb3dzIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHN1Y2ggYXMgYHR1cGxlYCBpbnN0YW5jZXMsIHRvIGJlIHVzZWQgYXMgYGRpY3RgIGtleXMgYW5kIHN0b3JlZCBpbiBgc2V0YCBhbmQgYGZyb3plbnNldGAgaW5zdGFuY2VzLlxcbi4uLlxcbiMjIyBNdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbiMjIyBMaXN0c1xcdTAwYjZcXG5cXG5MaXN0cyBhcmUgbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhvbW9nZW5lb3VzIGl0ZW1zICh3aGVyZSB0aGUgcHJlY2lzZSBkZWdyZWUgb2Ygc2ltaWxhcml0eSB3aWxsIHZhcnkgYnkgYXBwbGljYXRpb24pLlxcbi4uLlxcbiMjIyBUdXBsZXNcXHUwMGI2XFxuXFxuVHVwbGVzIGFyZSBpbW11dGFibGUgc2VxdWVuY2VzLCB0eXBpY2FsbHkgdXNlZCB0byBzdG9yZSBjb2xsZWN0aW9ucyBvZiBoZXRlcm9nZW5lb3VzIGRhdGEgKHN1Y2ggYXMgdGhlIDItdHVwbGVzIHByb2R1Y2VkIGJ5IHRoZSBgZW51bWVyYXRlKClgIGJ1aWx0LWluKS4gVHVwbGVzIGFyZSBhbHNvIHVzZWQgZm9yIGNhc2VzIHdoZXJlIGFuIGltbXV0YWJsZSBzZXF1ZW5jZSBvZiBob21vZ2VuZW91cyBkYXRhIGlzIG5lZWRlZCAoc3VjaCBhcyBhbGxvd2luZyBzdG9yYWdlIGluIGEgYHNldGAgb3IgYGRpY3RgIGluc3RhbmNlKS5cXG4uLi5cXG5UaGUgY29uc3RydWN0b3IgYnVpbGRzIGEgdHVwbGUgd2hvc2UgaXRlbXMgYXJlIHRoZSBzYW1lIGFuZCBpbiB0aGUgc2FtZSBvcmRlciBhcyBpdGVyYWJsZVxcdTIwMTlzIGl0ZW1zLiBpdGVyYWJsZSBtYXkgYmUgZWl0aGVyIGEgc2VxdWVuY2UsIGEgY29udGFpbmVyIHRoYXQgc3VwcG9ydHMgaXRlcmF0aW9uLCBvciBhbiBpdGVyYXRvciBvYmplY3QuIElmIGl0ZXJhYmxlIGlzIGFscmVhZHkgYSB0dXBsZSwgaXQgaXMgcmV0dXJuZWQgdW5jaGFuZ2VkLiBGb3IgZXhhbXBsZSwgYHR1cGxlKCdhYmMnKWAgcmV0dXJucyBgKCdhJywgJ2InLCAnYycpYCBhbmQgYHR1cGxlKCBbMSwgMiwgM10gKWAgcmV0dXJucyBgKDEsIDIsIDMpYC4gSWYgbm8gYXJndW1lbnQgaXMgZ2l2ZW4sIHRoZSBjb25zdHJ1Y3RvciBjcmVhdGVzIGEgbmV3IGVtcHR5IHR1cGxlLCBgKClgLlxcbi4uLlxcblR1cGxlcyBpbXBsZW1lbnQgYWxsIG9mIHRoZSBjb21tb24gc2VxdWVuY2Ugb3BlcmF0aW9ucy5cXG4uLi5cXG5Gb3IgaGV0ZXJvZ2VuZW91cyBjb2xsZWN0aW9ucyBvZiBkYXRhIHdoZXJlIGFjY2VzcyBieSBuYW1lIGlzIGNsZWFyZXIgdGhhbiBhY2Nlc3MgYnkgaW5kZXgsIGBjb2xsZWN0aW9ucy5uYW1lZHR1cGxlKClgIG1heSBiZSBhIG1vcmUgYXBwcm9wcmlhdGUgY2hvaWNlIHRoYW4gYSBzaW1wbGUgdHVwbGUgb2JqZWN0LlwiXX0sIHtcInRpdGxlXCI6IFwiNS4gRGF0YSBTdHJ1Y3R1cmVzIFxcdTIwMTQgUHl0aG9uIDMuMTAuMjAgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMuMTAvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiWW91IG1pZ2h0IGhhdmUgbm90aWNlZCB0aGF0IG1ldGhvZHMgbGlrZSBgaW5zZXJ0YCwgYHJlbW92ZWAgb3IgYHNvcnRgIHRoYXQgb25seSBtb2RpZnkgdGhlIGxpc3QgaGF2ZSBubyByZXR1cm4gdmFsdWUgcHJpbnRlZCBcXHUyMDEzIHRoZXkgcmV0dXJuIHRoZSBkZWZhdWx0IGBOb25lYC4gMSBUaGlzIGlzIGEgZGVzaWduIHByaW5jaXBsZSBmb3IgYWxsIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIGluIFB5dGhvbi5cXG4uLi5cXG4jIyA1LjMuIFR1cGxlcyBhbmQgU2VxdWVuY2VzXFx1MDBiNlxcblxcbldlIHNhdyB0aGF0IGxpc3RzIGFuZCBzdHJpbmdzIGhhdmUgbWFueSBjb21tb24gcHJvcGVydGllcywgc3VjaCBhcyBpbmRleGluZyBhbmQgc2xpY2luZyBvcGVyYXRpb25zLiBUaGV5IGFyZSB0d28gZXhhbXBsZXMgb2Ygc2VxdWVuY2UgZGF0YSB0eXBlcyAoc2VlIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgbGlzdCwgdHVwbGUsIHJhbmdlKS4gU2luY2UgUHl0aG9uIGlzIGFuIGV2b2x2aW5nIGxhbmd1YWdlLCBvdGhlciBzZXF1ZW5jZSBkYXRhIHR5cGVzIG1heSBiZSBhZGRlZC4gVGhlcmUgaXMgYWxzbyBhbm90aGVyIHN0YW5kYXJkIHNlcXVlbmNlIGRhdGEgdHlwZTogdGhlIHR1cGxlLlxcblxcbkEgdHVwbGUgY29uc2lzdHMgb2YgYSBudW1iZXIgb2YgdmFsdWVzIHNlcGFyYXRlZCBieSBjb21tYXMsIGZvciBpbnN0YW5jZTpcXG4uLi5cXG4+Pj4gIyBUdXBsZXMgYXJlIGltbXV0YWJsZTpcXG4uLi4gdFswXSA9IDg4ODg4XFxuVHJhY2ViYWNrIChtb3N0IHJlY2VudCBjYWxsIGxhc3QpOlxcbiAgRmlsZSBcXFwiPHN0ZGluPlxcXCIsIGxpbmUgMSwgaW4gPG1vZHVsZT5cXG5UeXBlRXJyb3I6ICd0dXBsZScgb2JqZWN0IGRvZXMgbm90IHN1cHBvcnQgaXRlbSBhc3NpZ25tZW50XFxuPj4+ICMgYnV0IHRoZXkgY2FuIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzOlxcbi4uLiB2ID0gKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbj4+PiB2XFxuKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbi4uLlxcbkFzIHlvdSBzZWUsIG9uIG91dHB1dCB0dXBsZXMgYXJlIGFsd2F5cyBlbmNsb3NlZCBpbiBwYXJlbnRoZXNlcywgc28gdGhhdCBuZXN0ZWQgdHVwbGVzIGFyZSBpbnRlcnByZXRlZCBjb3JyZWN0bHk7IHRoZXkgbWF5IGJlIGlucHV0IHdpdGggb3Igd2l0aG91dCBzdXJyb3VuZGluZyBwYXJlbnRoZXNlcywgYWx0aG91Z2ggb2Z0ZW4gcGFyZW50aGVzZXMgYXJlIG5lY2Vzc2FyeSBhbnl3YXkgKGlmIHRoZSB0dXBsZSBpcyBwYXJ0IG9mIGEgbGFyZ2VyIGV4cHJlc3Npb24pLiBJdCBpcyBub3QgcG9zc2libGUgdG8gYXNzaWduIHRvIHRoZSBpbmRpdmlkdWFsIGl0ZW1zIG9mIGEgdHVwbGUsIGhvd2V2ZXIgaXQgaXMgcG9zc2libGUgdG8gY3JlYXRlIHR1cGxlcyB3aGljaCBjb250YWluIG11dGFibGUgb2JqZWN0cywgc3VjaCBhcyBsaXN0cy5cXG5cXG5UaG91Z2ggdHVwbGVzIG1heSBzZWVtIHNpbWlsYXIgdG8gbGlzdHMsIHRoZXkgYXJlIG9mdGVuIHVzZWQgaW4gZGlmZmVyZW50IHNpdHVhdGlvbnMgYW5kIGZvciBkaWZmZXJlbnQgcHVycG9zZXMuIFR1cGxlcyBhcmUgaW1tdXRhYmxlLCBhbmQgdXN1YWxseSBjb250YWluIGEgaGV0ZXJvZ2VuZW91cyBzZXF1ZW5jZSBvZiBlbGVtZW50cyB0aGF0IGFyZSBhY2Nlc3NlZCB2aWEgdW5wYWNraW5nIChzZWUgbGF0ZXIgaW4gdGhpcyBzZWN0aW9uKSBvciBpbmRleGluZyAob3IgZXZlbiBieSBhdHRyaWJ1dGUgaW4gdGhlIGNhc2Ugb2YgYG5hbWVkdHVwbGVzYCkuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG4jIyA1LjQuXFxuLi4uXFxuLiBTZXQgb2JqZWN0c1xcbi4uLlxcbiMjIDUuNS4gRGljdGlvbmFyaWVzXFx1MDBiNlxcblxcbkFub3RoZXIgdXNlZnVsIGRhdGEgdHlwZSBidWlsdCBpbnRvIFB5dGhvbiBpcyB0aGUgZGljdGlvbmFyeSAoc2VlIE1hcHBpbmcgVHlwZXMgXFx1MjAxNCBkaWN0KS4gRGljdGlvbmFyaWVzIGFyZSBzb21ldGltZXMgZm91bmQgaW4gb3RoZXIgbGFuZ3VhZ2VzIGFzIFxcdTIwMWNhc3NvY2lhdGl2ZSBtZW1vcmllc1xcdTIwMWQgb3IgXFx1MjAxY2Fzc29jaWF0aXZlIGFycmF5c1xcdTIwMWQuIFVubGlrZSBzZXF1ZW5jZXMsIHdoaWNoIGFyZSBpbmRleGVkIGJ5IGEgcmFuZ2Ugb2YgbnVtYmVycywgZGljdGlvbmFyaWVzIGFyZSBpbmRleGVkIGJ5IGtleXMsIHdoaWNoIGNhbiBiZSBhbnkgaW1tdXRhYmxlIHR5cGU7IHN0cmluZ3MgYW5kIG51bWJlcnMgY2FuIGFsd2F5cyBiZSBrZXlzLiBUdXBsZXMgY2FuIGJlIHVzZWQgYXMga2V5cyBpZiB0aGV5IGNvbnRhaW4gb25seSBzdHJpbmdzLCBudW1iZXJzLCBvciB0dXBsZXM7IGlmIGEgdHVwbGUgY29udGFpbnMgYW55IG11dGFibGUgb2JqZWN0IGVpdGhlciBkaXJlY3RseSBvciBpbmRpcmVjdGx5LCBpdCBjYW5ub3QgYmUgdXNlZCBhcyBhIGtleS4gWW91IGNhblxcdTIwMTl0IHVzZSBsaXN0cyBhcyBrZXlzLCBzaW5jZSBsaXN0cyBjYW4gYmUgbW9kaWZpZWQgaW4gcGxhY2UgdXNpbmcgaW5kZXggYXNzaWdubWVudHMsIHNsaWNlIGFzc2lnbm1lbnRzLCBvciBtZXRob2RzIGxpa2UgYGFwcGVuZCgpYCBhbmQgYGV4dGVuZCgpYC5cIl19XSIsInR5cGUiOiJ0b29sIiwibmFtZSI6IndlYl9zZWFyY2giLCJpZCI6IjZlNTEyNWFjLTMxNjQtNGE3Ni05YzUyLWFjMzQ2MGNlMzZlNCIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsInN0YXR1cyI6InN1Y2Nlc3MifX0seyJsYyI6MSwidHlwZSI6ImNvbnN0cnVjdG9yIiwiaWQiOlsibGFuZ2NoYWluIiwic2NoZW1hIiwibWVzc2FnZXMiLCJBSU1lc3NhZ2UiXSwia3dhcmdzIjp7ImNvbnRlbnQiOlt7ImlkIjoidG9vbHVfMDFYamozYVJkV3loQkpmUG1USkJ4UWE5IiwiY2FsbGVyIjp7InR5cGUiOiJkaXJlY3QifSwiaW5wdXQiOnt9LCJuYW1lIjoidHJhbnNmZXJfdG9fc2tlcHRpYyIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QTZGNGNDTjV5aEQ0Q3JzOU0iLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjB9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InJlc2VhcmNoZXIiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtM2NkOC03OWYxLTg4YTEtY2U2Y2VlMjk0ZTc5LTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoidHJhbnNmZXJfdG9fc2tlcHRpYyIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsInR5cGUiOiJ0b29sX2NhbGwifV0sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsInRvdGFsX3Rva2VucyI6NzQxMSwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19LCJpbnZhbGlkX3Rvb2xfY2FsbHMiOltdfX0seyJsYyI6MSwidHlwZSI6ImNvbnN0cnVjdG9yIiwiaWQiOlsibGFuZ2NoYWluIiwic2NoZW1hIiwibWVzc2FnZXMiLCJUb29sTWVzc2FnZSJdLCJrd2FyZ3MiOnsiY29udGVudCI6IlN1Y2Nlc3NmdWxseSB0cmFuc2ZlcnJlZCB0byBza2VwdGljIiwidHlwZSI6InRvb2wiLCJuYW1lIjoidHJhbnNmZXJfdG9fc2tlcHRpYyIsImlkIjoiMzg5MDU4ZDEtMDgzZi00NjMyLWJiYzUtMDliZmQzYTBmZmQyIiwidG9vbF9jYWxsX2lkIjoidG9vbHVfMDFYamozYVJkV3loQkpmUG1USkJ4UWE5Iiwic3RhdHVzIjoic3VjY2VzcyJ9fSx7ImxjIjoxLCJ0eXBlIjoiY29uc3RydWN0b3IiLCJpZCI6WyJsYW5nY2hhaW4iLCJzY2hlbWEiLCJtZXNzYWdlcyIsIkFJTWVzc2FnZSJdLCJrd2FyZ3MiOnsiY29udGVudCI6W3sic2lnbmF0dXJlIjoiQ0FRUzFRZ0tFQWdTR0FJNEFVSUlkR2hwYm10cGJtY1NETjZUZlU2TkRNOTcxbEVienhvTXZSVUExSjlveDNmSlFZVDlJakFyQ1FRR0hCUDRwUkhQYXZYelJsYk1iYVlIQzU3WWorR2J4Z3IwaGVsdmQyT0lML282UmJ0NFBGQTNGYUlGVWxncThnYzdLUUk5RnRHbnZTQWtMQlJ6SXNVcC8yZDVSZDJ6Z0VzZmtGVmVYc244V1lZWmtpbkFRVlZ5NkE3QjNMYUVKVzB4Yi94ODhEaTY3ZmVkd3gwUStGbFp0d2NtVDNEY2FhMTFKdmV5SFJyM2RtK3pHU1pQNkNYZVhOSjhuclRaN1cxWVJUeXNENnc3bzdMcDhoMVJ3cTRmNG84aitRVWZZMHZqZSt6SlpzeEhQbVFOdDN5a2VLaDk5aGduNmRiaXo2L0VIcW1iSGpTQXUxTTRTcUVEZnFJS1RoSFllbmFtQVptY1E0WEFPbzZtZFE2Kzc3VmdwVjFsQzEvazUySWlVb0pqa0dsUXJuTEN2dXRpSTdLTitzZ0s4YzIrMktDSnJQUkN2VlYrQmtTdHZQNWpXV3F6eXRwS1Erbkh6aEw0NDhPcklBOWdkSkMzOUM4ZURyWGVSbkk3dDRGcWxrWEdvTWxaQVY5VU1jclFIUnhPTWk4dE95VHppOW95QkZEVFNzUStMSnFlcjB0ejFGdDlnZWs2dnpnNTBTcFZBRlFkTEVSdjhSMVlxTWFlSSt4c3ZUQUVXOU1SZ3ZSTWt6THV6UlQyZ3V3M2pEK0ZqaXAzN3hnSzVRSTBSK0NzT0FtUVBvVFppenpjSXlBVUF1Q0pGQ2dxL2dEL0NpYkx4b3ltaVZVSTFhbjlTbnQzS3BOUm9XZHZXSW5lMzNWMHB2OFBIZzl5VzJKdUxPcVFHaWxMQ3NLL1Ezb1FyM3VwRU1YeFhHakNMN1dVaXp2UlpHRkVFVitWcGxZdDd2NHRLWk1qNXBrM3ZkZU0wTUo1WFpnN1B2VXNSenljeGNuclQzS25vYzNxTSt0dHFaZlRzYjhkNlUvcFFpZnE3R3VhaEZPSHJ2cm9xZ0pMaisvODFZeTFwWkZramZKZ1kwWGtsekFuQTZDRnEvWVQwZ3hiVitUSDVJUytXYVlPVXpLMzZTOHM1cXFNd2huZ3Y0SXpFM2N0WFhxbmhJWlhOem55VkJxYllhL0d4MmJDamNFajJuNVJ5R1lSUy8rTWN1bGdMQnpuUTlNa0cyRDdRUDNNcS9ud0hYS01JYUxTOFZLOGhHM2QwckFqbHZuYXVYU2hOckp4dGtqNHFrd254RDRMckVqazF1ZStQTGNiTHFZV3dBcjhuMHMrNDFqcnNJUFZ5eUs5dkZhRVpyQTlLYUJ4a0p5aXF4NTBHWk5uN2NreCtta0kxOGdINTJpeVZQdFU4RktNWGp1Z0NsNm1QTGlIWWlEUWQrRzYzTHVkbU9LeXM5aXRWbkJEQk5NUDJiL0k3K2JJVlJsaGVHYVpXcjZNQVRLbDg2QjN5KzBrdU9yTFJNeVdjZGliYzJpUWtabSs4ZjdkVFV3YktiQkpzS09GWTczLzhubEwvSmlZcGp6Nm5VSWsrSXFTamFGeUs2WEZsQ2N1a1dwV0hwREpuU3ZiRXpleTdxSHlDcUorSStvbjhpUDhHT3RlbXBRYVMwLzRrWkdTd2xyRTZ4T2o2RXdHcGZqYkRaS0hrUC8yUkFkTllQUXEzejdiZFZKTUpObkZNejZlMzdtek1wQ0JpaHdQZFNGVFJFM1NDWTY2d3ZUZ0p6R0NVeWhIYlNpNWkxYXp6T1hkOHVIQ1NwVC82TDFvNlZVMHBqb1I4S2FhNHdDV0hHZjdQL1dFZCthdnlwMDJKbnUxY21wVzArR3Erdks5aDB0ZGIySUNsTS9lTmZkb3VRUCtlcDhNSlVqcmFEUklDbUFKUVk1b2JTNzF2bmlPbytyZWFGeTB6UmdCIiwidGhpbmtpbmciOiIiLCJ0eXBlIjoidGhpbmtpbmcifSx7InNpZ25hdHVyZSI6IkNBUVMvUkVLRVFnU0dBSTRBVUlKYm1GeWNtRjBhVzl1RWd3RmgzQVZKWlAzczh5MUhxMGFERzc2dWhIT05hU2NYQUdTaGlJd0dnakxYSFV4MXlacmtMUGxqanpUT1F5Y3BMSmlLVndYY3htL0V6S0ptMXM4TTVXeHVSRVdFbGRYY29hTjAvbXNLcGtSTVQrZUxiNWloOXJBSFVMVG9tVU5zVXdKZXUwanJKZTRHTGF6eTdzUDVLdGpFT0VCS1FzWFVmb1E3ZkRrWk9RVUhaaTl6elFONGhGTm1ZSlVmM09WcWY5VFFOZEMxK20vL0hBWlBmV21BeTZIdER2bTdrZ0JKa0NFb2lGbm03dG4vUFgxaFhHUFJnV2M2QlRYVU1yMldBOGFLeXFnZ1JVYk1IMitKWXY1aVdaZGRDYzF1cDFOYnIzc0Y3V1Bjb3Q5WkJoeUhRWHI2OTJ6ZUprNlNKZ3lnSVZyQVNiTWx5MmJCeFRXSkI2MjNnVmd6cENtVlNMWkZwa0tYZ1BjcDhoR3lES092RjZTaVFTSHFHUnhRb2ZyN1ZzMWFQaGZaaXhST0pHRCtNbHJrZ2dWQW5HTXlZSURVWnBhdnRDZWlVMUptQlN6MDA4NGJYUHRUL3RucHNLT25TUFlaUW8zQjBCWEticUEvV3NLbmc4L1YzWjhVdWFqbzZJVloyaGdBMVdEZHVxUU9Ra0V4R1U2cEZ1TGhlYklqdVd4U05JL0RmZnZSLy9MTnFDZnZkbmYwaHZvUHREeGZ4L1Nhd0l4NC9zcHE5eGJ3RnpIdExnb0hnMUYyd0Z3N2NsUERsZzFQeUQvdjJ5SkczVjlUOEdMNmpHUEU2cmhmaWwvdTRITnZmN0JXMGRRekNUVFhjcVp5NG5CeXkwK3ArUzdaeGZNbDRLcHhhMmdPaGhTTmpmcGZ1WDBRcEw0a01FOEoxbmxpRk4xZDhLcUVQdURJMThCUUhVaEQvVWZZMjlvQTB4NTEwRmpNL0JzTFo0bjVzcHBrb0lpN0NGb1FxMmFBS1ZOZkMxS2lsaElwUkQrMWR5RHRaUWtsTjhnWnBPRVNIdzZNTDFUdFUweWZBY2JtcW94SjZoaGp6d0g2Q01zVFFFRXk4TWJSQk0rUUtLa0lSZFJDUEhHRlcyTGt1UjUwK0owcEYvdHI2cHFWdkplb3pmaU1uckVpbEIyN1RaUDRuS1RKRzhyZC8vdkNXZW4weTdDQS84ZEJKOWdkcDV0eDdOVkE0NWsxZ2RjeEVSMWdZb3VOWDNuMXZHMy9CMUY0Kzkzd1hXOU1xbzZVczU5VFFtcmI5TnFhN3BTUU5TckgwNncxR3h5QkFsa0ZmcUFsYWZrMTZhTXBxNDE2VEpZcEd5NDNXQ1VpQ2NMV2xoaVdnejJzZ09MQVhQOWE1d2VXQUtSQzhKNmprY1VrRGVYblcrSEZTaVBNNis5Qm82a1ZoTFVoTjVyYzRtSFpPWnBGN3B0UHBzUU40WENxTFBzcXBSSC84bzVYcHlDNlpuZVRlRjJFeHRqaHdaVmpYRHdNYkl6VllJYncxeTJQUEd3OGRDN3BjNHNJVlNuSTRvUUFIMHY2NTBnaGZobU1ldldXNGg0UHlsYncvREc0ZmRlVDMrOHowZjhNT1NUdGZpcHgwNS9RdHRIbEF2T0RSeGE1eURDdk96Nm4zZjFCTzBOLzRNYkd5T00yN3FXNHg4bUJWb3J5VVpXZnNtaGZpMC9HZk52QjgreWxUR2JZTlgyN09jRUY1V1p0TnFiV0V5N1Y1R2JtWlVTeHNwY2c3RkJaM2ozLzNMN211dlpYb1ppMWY1ZXJxNHkzbFNIK29GN3FQRm52aWV5TE9Dd3ViY1F5K295b3dqNGNEMGlkUmVjbnArcys1MkQ3K216RjFncFcrZitjSmFzb3hpZ2J4a09sb1M0eWVpS3ZnTTRuV1NwR0VqMTJSUHg1bzgwRW16aVhOOXQ0ekp6VzFodEE1aFhKSmVGeW1wTVl0eUNVQ2V2UldWbXJSWWt0UTdVbkNhNnRML0FWUGkxbmk1WENYSGZMT3VxZks1S0xZeGp3VnRkaGhmbUNMdnBhS1ZOQ0VPQ2Y0bk5HSHFEOTh3VWIwZWpXTndEWUJuSkdrQlJ3OVJWc3Bmd05kb1dvVmNuMnF2OHl3R1VwN3ljL3NrMkxwSTZWemlSeXZXcWJHTkcxWkpxVlZ1dFJjNDYrc09FNFZTZVMyL0NUTktOYTZOTUNmL2h4eGFVVnNZZktkUCtTMUxjNGlhbktwMHNVdWdQcG10b3lkQzBDUDJSd2puSExjbnp4Q252cXhsTFFXWE1OUFRzdTJ5QmNIbFdKTHRjbWRnL01jTGFneTdsNWtHNnNON1p4SkxEL2tBeDhFNnVxb2hQK0svak1Tcll1UUpuaHI0dGY0YmlXRlkzSnpmL0twQVl5VlNxK3BiZkZLWE1iTDluSEVvWlFUMTVXeWFiZDZETUFxM1FaT3RoTTVwVXpkTnFVWGtjckkxUHdqQVNBZzRkVlUvMm8rOThtVFhueWhQN1ZsRWtUbmRkcHd5SkYrWUNDYzAxcHFhNlpYOTlwblIrWU9wSXJiNUR0UGNLcTB0NWNGQ0R5UXhOMWxVdXFaY1l1anZYL0dDNys2SXBSVHovTmtFWS81Y250T2ZaSnlkNlNrVXgvSlI1QUpJUlBDTmVpYjByY1dubkZuMFhLOHMyMng0MHdMakV0dWZYRE85K3lyYWY0a0h4bEJJM1NCMG1lTWplcFg1MENKaEF4N05la2tSRm53b2VjTGZxSXlyalhyZ0Fac1RNUGlYOTRzbmpkcnppc0FYdEJLYW51bGlsU3hPZlcySFNtZm5rMEwyR1hISkozUyt2Q2JnUkVtbDlkZWtmbUhobXdFWXR2M2tmaHRoN0VSSXdWVDhqNjlPdElCQ21lVElKdUE3bzczUFhjMUxKMndodVpLN0ZaZTZOVE9zNjFNUDdKVmxJYzZ0Ymh0T0JrM05YVThwQlBPQ1k2UTU1ZDYzRnRwVVUvS0NiUTBLMldROW81ak5Hd05OSjZDdzVGREN0K3g1WUplUDFkMlF6RFNpcStjU1pxd3FDWlZjTm9BZnRHR3pnclJaNHZrcGJQMHhRWkhDdFdzVXdIVlRqclk0TmZSRGpIZFZQUm44SjlYdG9vYy9oSVdrcXpGYWN6WVRsYXB1MlJDSnFlOGFFbzNqeldaQ0VKN0FpQnpCWXJhWURnT2ZlaVF5ZUpVc2JiWEhkR0tpaXcyMkJkcDRNK1l6NjcxTVowaWU5RVdSa3MwUTJESEdVVG5lOVM0dEFBSlBHYlVmNHh0Q01vSjQwUTRRVzA1UUxTWnkzd05GQVVGWW1YRTRFQ0RGQjgrSUVlTkZGRi90Z0FuZmg4R3ZGaC8xeFkrTEUrb3pNbWlualNSd2hxMDl6UUFFa05GeXUwSUtndkRreU9JK2ljY0Y3ZG9XTFRVYXNQeExIMXhHUEJTS2tlV0NuU29YLzRDMmM5ZHU2NkJiUlhhbUhxYWUrVkNpUzZCTjk1M2lZS091OHJNa25lVytURmpWMEN6QWY4UE5HOUxCYXlVZ1k1VXc5eDBTYXA2ekk0UHBhWktINGQ5a3RrRlUrOTF5QmJONVNSdGRkOC9FUWF0SlhtQk5mdzdrUnJsbCtMandad2VXTEViMGpUMVJwR3VEcUhMZExUTU1nb3dTY3pycDJ6Q3NKZFFIL3NlYVlycHdrQ0ZSOGRSak03SlJVa1ZhMUEwU3ZkWkFYNzV2dUJ2dkxCek4yYytSYnQ1b3F5OTNscUlCSWpLYjd3MlVrUlRiNHBCV2NOS05QVWd0M2F6L1B4UXhFYXNhbHRkbXV5dnNYWWJ1cjNWUE5uOEp6Ti82SjVOK1FDbno1NzFUY2NhcENta1B2RkUrZ2F5WFUrVUYxVktZeUZjVXZsZXY3bHNRbG1qcXpLMlFBOFg3UTNUekZhcHFodkpLbHQ3ZHpCSHJhcVpCZ1BnYjlONG9LTmF5aThGWlQ3MStrYVloSWhGckVTS1lSSGZIa2o1bjNrZmhnTHkreCtqTERiS3hNZ2drZ0huSno5UnVFSFpYQld0a1lBUT09IiwidGhpbmtpbmciOiIiLCJ0eXBlIjoidGhpbmtpbmcifSx7ImlkIjoidG9vbHVfMDFINmZWOEp2UkpOdlh5NDdlUm5kM2pIIiwiY2FsbGVyIjp7InR5cGUiOiJkaXJlY3QifSwiaW5wdXQiOnt9LCJuYW1lIjoidHJhbnNmZXJfdG9fdmVyaWZpZXIiLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2Zkd0FNUmRTMjFGZUpmelFoTTNzIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjczOTYsIm91dHB1dF90b2tlbnMiOjc3MCwib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6NzM5fSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCIsIm1vZGVsX3Byb3ZpZGVyIjoiYW50aHJvcGljIn0sInR5cGUiOiJhaSIsIm5hbWUiOiJza2VwdGljIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTRhZTktNzZjMC04OWE2LTRmMGIxMTRlMzAzMC0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6InRyYW5zZmVyX3RvX3ZlcmlmaWVyIiwiYXJncyI6e30sImlkIjoidG9vbHVfMDFINmZWOEp2UkpOdlh5NDdlUm5kM2pIIiwidHlwZSI6InRvb2xfY2FsbCJ9XSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjo3Mzk2LCJvdXRwdXRfdG9rZW5zIjo3NzAsInRvdGFsX3Rva2VucyI6ODE2NiwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6NzM5fX0sImludmFsaWRfdG9vbF9jYWxscyI6W119fSx7ImxjIjoxLCJ0eXBlIjoiY29uc3RydWN0b3IiLCJpZCI6WyJsYW5nY2hhaW4iLCJzY2hlbWEiLCJtZXNzYWdlcyIsIlRvb2xNZXNzYWdlIl0sImt3YXJncyI6eyJjb250ZW50IjoiU3VjY2Vzc2Z1bGx5IHRyYW5zZmVycmVkIHRvIHZlcmlmaWVyIiwidHlwZSI6InRvb2wiLCJuYW1lIjoidHJhbnNmZXJfdG9fdmVyaWZpZXIiLCJpZCI6ImMwNjQ1NjNhLTk3NGYtNGYwZi1hYjRhLTA4MGU2M2MwMWNmNSIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsInN0YXR1cyI6InN1Y2Nlc3MifX0seyJsYyI6MSwidHlwZSI6ImNvbnN0cnVjdG9yIiwiaWQiOlsibGFuZ2NoYWluIiwic2NoZW1hIiwibWVzc2FnZXMiLCJBSU1lc3NhZ2UiXSwia3dhcmdzIjp7ImNvbnRlbnQiOlt7InNpZ25hdHVyZSI6IkNBUVM5Z1VLRUFnU0dBSTRBVUlJZEdocGJtdHBibWNTREdGeUoxTVhvWStBaTZOa2J4b01SZDJUclVmVVU3V3JQcEEySWpEZ3k4ZFVwbGNaS2M3RG5Kd05MVEJmSDVVTlIrbkduYWtTUFFpYThVVkl6cmprT25KMmZMcldVc0Eyb0VPOHlSc3Frd1hra0RDc3loQjk3ZHdqNTdSTXQ5MVh6Z1doTDlsdU5FR085MnlXMHJORkdvTVNVVk94UElyRmM1Ump5cmhLRWMzKzFSbnB4Q2t5bHErTE5sUTVORnhMbjZ2MmNGNUR6cXB2QkhOUjdYQWlnK3VySklVM2dYeWRDa2FHWlpEZXRKdFFPVU9YZ1ppOXo5MTAwTnBkQUVrWERvMklDSENHVEdwVmMwSi9wYlh4V1JCeGI2QjEvR2wwWWpuNjhWNkhsZXNIN0lvQzdHUi8zSHdSdTlOU0RCRzR0dGZRakVXY2crUGVBZWE0Um43anp4K1RtOUg3TDFqb1RLVmdEYlFnNWlLTUxmanZQcWxua3NDTHRYT3Q4alNJUzVPWUxON2EvN1RYNi93NWQzNHZudzh2UERtVTFVWWkwUTcycVVXaFVhMnZ3OE9YTnBjS0w3QnAwbE5mQlM4cnZIWSt2Qlk4L2hhQW9UTDN3dkxGVTFmVCs0ei92QTNCY3NpZ1pEWWxmSmVEWWN4WG1SdXF4enhFR3BYQ1JlOXMxSFd0SUQrNDFwSHU2bHBaSGtYYWdocGM2VFpITHhOd2NWaFRiNmZNUTdRbysyTDZQdmtsaDFtMzJwZW9keGVnMllpUGhkcGwzdy94a09FdTUvSkVGN1pCeHBkVUwrdTN3a081cFVsc3FmZ1g3dWh6djdIQzJXcFNXVmltcndpUXdRT1V4MCtKUHJMdHcrbFlmcTdJM2pydW8wR1cwZ3VXdm51ZjdKTTNZTGo4NHErSmdIb21uU1Nabm5abXJMMFVhUnZyVnlMbzg3N0YzOWNjTFM3WG9abm51aThKNFA1OFpUbWlMa0VKZUdjeDFJdWFnMmUzMzMzOU83WWV3LzU5dExiUXRJR2g5VldNd0hCbkZ4MzlTOC80TEVGcks1SGxkUFFoT3Jsb3lUdGtSczY2aVFSS3R4TUtTMFZJNlFUS1pEQWJzN2E3SDF5QnYrbUppOVhnNHVLbXlVNnFubjd6RUtqalkyYklsQ3dleng0TExHUDVIRDNacjZmWnNtZEcxVUh6RHNCdllHMm9DSTBvcUVLTSt2YWxzVDZoVThNUmpiUUJEL0pKOEpKTi9pQkdsaEJBVUhvMFVtaGh0UjJldVRWcFpXbE1uaG1wWEFkS0RYU2ZQZGFQb2hnQiIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJpZCI6InRvb2x1XzAxWEVBdWhRSENCZHlDd3R5bVp3RmZEaSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3JlZF90ZWFtIiwidHlwZSI6InRvb2xfdXNlIiwidG9vbHNldF9uYW1lIjpudWxsfV0sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHdBc2FiRHF1ZWdIaTZZVXBDbiIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo3NTA5LCJvdXRwdXRfdG9rZW5zIjo4OCwib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6NTV9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InZlcmlmaWVyIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTY2ODItN2QyMS04YzE1LWZmYmNlOTQyMmM5Zi0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6InRyYW5zZmVyX3RvX3JlZF90ZWFtIiwiYXJncyI6e30sImlkIjoidG9vbHVfMDFYRUF1aFFIQ0JkeUN3dHltWndGZkRpIiwidHlwZSI6InRvb2xfY2FsbCJ9XSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjo3NTA5LCJvdXRwdXRfdG9rZW5zIjo4OCwidG90YWxfdG9rZW5zIjo3NTk3LCJpbnB1dF90b2tlbl9kZXRhaWxzIjp7ImNhY2hlX3JlYWQiOjAsImNhY2hlX2NyZWF0aW9uIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VuX2RldGFpbHMiOnsicmVhc29uaW5nIjo1NX19LCJpbnZhbGlkX3Rvb2xfY2FsbHMiOltdfX0seyJsYyI6MSwidHlwZSI6ImNvbnN0cnVjdG9yIiwiaWQiOlsibGFuZ2NoYWluIiwic2NoZW1hIiwibWVzc2FnZXMiLCJUb29sTWVzc2FnZSJdLCJrd2FyZ3MiOnsiY29udGVudCI6IlN1Y2Nlc3NmdWxseSB0cmFuc2ZlcnJlZCB0byByZWRfdGVhbSIsInR5cGUiOiJ0b29sIiwibmFtZSI6InRyYW5zZmVyX3RvX3JlZF90ZWFtIiwiaWQiOiI5ODJjMDEyNi1lNmFiLTQ1OGEtOTIyZS03YjI2NmU4M2MyYjIiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMVhFQXVoUUhDQmR5Q3d0eW1ad0ZmRGkiLCJzdGF0dXMiOiJzdWNjZXNzIn19LHsibGMiOjEsInR5cGUiOiJjb25zdHJ1Y3RvciIsImlkIjpbImxhbmdjaGFpbiIsInNjaGVtYSIsIm1lc3NhZ2VzIiwiQUlNZXNzYWdlIl0sImt3YXJncyI6eyJjb250ZW50IjpbeyJzaWduYXR1cmUiOiJDQVFTd2djS0VBZ1NHQUk0QVVJSWRHaHBibXRwYm1jU0RPUlhnZTl1TDE4MzJSTVF3Qm9NalF3QXdBSGI5aVAxb2pQR0lqRERTY2FsNDg2YmVDSmJ0RzgvRDJZOWc5UzV4SVBmam9IZy9keHdYNUh5cFNRRUJiakZpVFNiUEVDWFNGelY2WUlxM3diTGNteVBidnVzR25EZXhGZ3grM3NJWHp2cXlGVi82OXhoaWhNblNwZlRDSHkwKzB6U3UxV3BZVS9XVXBJZm9hU1hUY1RhMXFrTTh4OEVrL3kvMWprS01aVWZERmE5M3VoVUp4T0M0K1FnUFdCbC8rWHkydDgyTDJ4L2VicTdRTFFUcXRSMGpYZWFVUlFtM0NvNkp2T2dXbUR2TXBFZFRlVFdFM2ZEaDF0enpWS3NmbHVtNGQ4OWI2Q2FLdG44bmxRZDdSaE45OXZOUU1RSEc1VjRPdVZOZHJIUFpacEZPdWhkZWpzTUZXWHlCKzU1c2xHQWQ4d0VYZ0ZLYkkva25PWFZHekwvTkRGOEd0eTF0TlpsdTJmZFNDQXNZV01zRlFQMWNid0xlUUdzenlYekpNVlpKMkk5eEJQa3l6MUhPWENQeWg1RitqMmdSVFBpa1ZCM0FSQzZ0ZkIvbERTVWVLQlZaUFFTOHBZL0I0ZUNzK0lqamFCTldydUx6ZnFDc2M2eEwwQ3VRUFNZeVFYYVlGallyU1JOY0M3MFdRQldUNVZWaHAwbUE5eVpER1liUlp0OUk1T0s5UWtRbXVKNXpERmFNRU9YelNyc0M0bHRlUDZNZGltaU1panZzSDNpcStjNldVWW9sUmozbmVJODhFYUhJekxBR1JxNjRCTkNMemRBWUwycEVVMnNiNXVHNzhQS1ZvVVVDYlZKTDVqcERPaExPVE8xYlFzVHVvVXBuUlJyTFFEL2tJelYrbGpwVkJ3d2ZKY3lxNzVUSkEvQ3RQci94cjJTWjlCMWIyc2JqRzZqRDBsaHRvQ2RkZURnY3FnbC9MU1NrY3VhWVk4eFBXQ3krTHROaGNxVGhmbWZ2bkhMSklwTDZnamJqVy9PUXBSZlZVd2Z3MWpPYjliV1Z3Y2dIaE5ZZktZR1B0TThvdGg1eHdMWVJSVmd6cU1LT3ZzN2ZhbFhSaFZ6WjJTUDZJdzBCTW1BQVo2SGpmQ2w5TCt0UHFKTmxuUVFPSHBvV2p1bTRCK1hzWWxkRkVLR1FXUXFzZDU5aU5QdmFvOXAvc0NPdDdyR3FDb3pjbWdpRW00dzY2ZXovUUlEeWI0Nm4wK3RjTkpQVWNvQkt2US9qRHdFNlZ3bWtRL25FQ2pGcDNVZERUK2tXWVFTTmQ4ajhWQlpPcEc4TGxxVVlyRVpDdjBxN0JzMXZjTnJkWWlhRm9TMzJvc2F5bldrK0h0M09IdUZXYkVGOEpsc0ZUUUFPMDVWdllrZWNmMFYvb1RIc0c1aHlPT3M3VUVhREZxN3VhSnV6V1ZoaDVwQW9qSWFCY1I4ZlZsbjJQTFJUWDdpczJuc0ErUXJkaWRlUGNadEZ0Z3hBaUtZZTFpMnNTRmk0dm04RU9rQ2JRNjN3ZFFlWTRVcnI1UHVvTUV3eFVVTGpnS2FnaUFtc0pEcTRSclZtN3NPNCs2WXVBOWo2bDJ1eWxrT0VjNjd2Zi81ZmtYdUNzaUFwTkw2a2ViOGMrdkwwbjRGWEdkemorYXBTRGZGR0JnQiIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJpZCI6InRvb2x1XzAxVkJyV2RXRVdZZ2l0NGtOWWVoQk1VNSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX2VkaXRvciIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QXpmanFVb3BvRW1aTGFxbTEiLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzQxNiwib3V0cHV0X3Rva2VucyI6MTUxLCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjoxMTl9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InJlZF90ZWFtIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTZjZTYtNzI1MS1iNzk2LTRmNjQzMzNmNjdjNi0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6InRyYW5zZmVyX3RvX2VkaXRvciIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxVkJyV2RXRVdZZ2l0NGtOWWVoQk1VNSIsInR5cGUiOiJ0b29sX2NhbGwifV0sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzQxNiwib3V0cHV0X3Rva2VucyI6MTUxLCJ0b3RhbF90b2tlbnMiOjc1NjcsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjExOX19LCJpbnZhbGlkX3Rvb2xfY2FsbHMiOltdfX0seyJsYyI6MSwidHlwZSI6ImNvbnN0cnVjdG9yIiwiaWQiOlsibGFuZ2NoYWluIiwic2NoZW1hIiwibWVzc2FnZXMiLCJUb29sTWVzc2FnZSJdLCJrd2FyZ3MiOnsiY29udGVudCI6IlN1Y2Nlc3NmdWxseSB0cmFuc2ZlcnJlZCB0byBlZGl0b3IiLCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ0cmFuc2Zlcl90b19lZGl0b3IiLCJpZCI6IjVkMzllMzZiLWM1YjUtNDQyNi1hOWFhLWJlMmY3ZDM4MTZkNiIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxVkJyV2RXRVdZZ2l0NGtOWWVoQk1VNSIsInN0YXR1cyI6InN1Y2Nlc3MifX1dXX0=" + } + }, + { + "key": "gen_ai.usage.input_tokens", + "value": { + "intValue": "8127" + } + }, + { + "key": "gen_ai.usage.output_tokens", + "value": { + "intValue": "433" + } + }, + { + "key": "gen_ai.usage.total_tokens", + "value": { + "intValue": "8560" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJnZW5lcmF0aW9ucyI6W1t7InRleHQiOiIiLCJnZW5lcmF0aW9uX2luZm8iOm51bGwsInR5cGUiOiJDaGF0R2VuZXJhdGlvbiIsIm1lc3NhZ2UiOnsibGMiOjEsInR5cGUiOiJjb25zdHJ1Y3RvciIsImlkIjpbImxhbmdjaGFpbiIsInNjaGVtYSIsIm1lc3NhZ2VzIiwiQUlNZXNzYWdlIl0sImt3YXJncyI6eyJjb250ZW50IjpbeyJzaWduYXR1cmUiOiJDQVFTNWdZS0VBZ1NHQUk0QVVJSWRHaHBibXRwYm1jU0RJNFFJYjdMMnc1YVczY00vUm9NTGkyd0V4RGtxaHV4SkRpWUlqQXFHeE00V1V3aXE0dVpIcmlDYnVDNGtWd2E3aWdkVUxvb2NaYmk1ZmV2S2Jmd3FIU2JyTE5UNTFOR1g0WHNMdHNxZ3dZL3RhYkhqbGx6cXU3blJXdjgrRno0UjZuOWphN0NwajI0WnV4RnVLa2tRT0JtQTRUZjFhcGszUlBxY0VRK2ord2VkdTBsTytyZEVDeWQzbFhoZmdFUkZGcWNNNmF1Y3hlbDRSYnc1b0FIWHU5K1hlVHR6bCtOOHRBZVo5aUdaMlhUdFU1T2I4YVBUMUt1WCt1cDhrUTRWVVhrQlRmZDJmNUpQOVJ4WkVxOXdTWWFac3lIZHYwbisxVTQ2QU9zdktabXA5UVhCd28yZjRPOTBqUDhlZ2lyZW05RWJaYm1hWU9KOFdLN0N0S1JWYlkyeTQrOXhSVUowNG5zZEl6NUY1RkZLRXh4Tk9yQjRtYy9ORTgzYWIzWjY0aVd6T3dxb1VHQUdRU2thSDBkNWNFdW1abzRmWlpBaFB5dUplUUR4dWxNQ2V3bjdFR0NucEp3L256aTB3b3lFYVFMNFk3aDk5UDdhWHhGcjI4cDVLQ3lKRUpURGVmZUlwVmxBd01DQWp6K3hDNjlBQUxtd3lUUStjRkVNK0tBZ2VJU3BoTFZ5OEFQcjJGU2hheEJOclhRZ3oxczlzMmRoTFpRT0Jpd3poeG1FZ2RLNys4SmFxaEIwZUVFZC8xUlZBdUlZZGV6QTVIQnJ3RTlEeVFpQ2wzVU9zYlNuTkVWZStnckhwNXAzdG1NQnE2OGdMVjgyb0F6RllabCtDZXd4NzF2NWRQRk92S1BFSFV6cFFZT1ZMcm5CMUVaUllyWno3Mm4yd2ZTVnhCT3pwVGNuQnhWNkhlZENQeUl6S1haTTkreUVlVEppOFF0dlZnWjF3K3YwaG1DRkdwbm1aTjdtWGxncUwxczZNWFZ5ZHBQT0tQbE0zTmE3Q2RIblBGWWl3cjJ6THZra2NJTTdDc2svQzMrWk1BV1RLK0M5QjF2Ykd4UGx0RTNPdjhrTzk4UGdTWG0xQUp1RWJYeStyM2EzRDNRbFpKM2RFS2UyTE5kQUJ5clBNaFNjS1U4R2xmSC85TnRncmh0YWtnbGxEL01mQWxUc2JOd05EclV0V1J4bUR1Si8wWitoa2svWU53RURZQVphOFRtd1ZvV09kQkhwUlBIdlo5RTl1c2V1bHRtYStKazFnUkxEdHZFWmtxWFVFU2tNY3FVdFZ3WU01YWpKQTVONTJjTmlaMTVCVGY4WGpHaEtXZE9WNDArOEgwUE9TeE0ySmlmd0NPMEM1eEZXNUJvM0lpSU52MFVPdFk1ZjhrdlBkTTNQamtoYU9ySzF5VEE4ZUVFNDR2dW5MOVBrb0dlY2JZbStYRTVLekwzUXNFbjJZMUlKd085dzB4SFhRWkVlaVA1KzB1M1luRGM4cTlkQkxoR1czRnkzMGdZQVE9PSIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJpZCI6InRvb2x1XzAxQm04QVU5RXNRVXdxb1ZMWXB4S0E3NyIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7ImZpbGVfcGF0aCI6Ii9hbnN3ZXIubWQiLCJjb250ZW50IjoiKipUdXBsZSB2cy4gbGlzdCBpbW11dGFiaWxpdHkgKFB5dGhvbikqKlxuXG5BIHR1cGxlIGlzIGltbXV0YWJsZTogb25jZSBjcmVhdGVkLCB5b3UgY2FuJ3QgYXNzaWduIHRvIGl0cyBpdGVtcywgYW5kIHRoZSBjb2xsZWN0aW9uIG9mIG9iamVjdHMgaXQgcmVmZXJlbmNlcyBjYW4ndCBjaGFuZ2UuIGB0WzBdID0gMWAgcmFpc2VzIGBUeXBlRXJyb3JgLiBBIGxpc3QgaXMgbXV0YWJsZTogeW91IGNhbiBjaGFuZ2UgaXQgaW4gcGxhY2Ugd2l0aCBpbmRleCBvciBzbGljZSBhc3NpZ25tZW50LCBgYXBwZW5kKClgLCBgZXh0ZW5kKClgLCBgc29ydCgpYCBhbmQgc2ltaWxhciBtZXRob2RzLlxuXG5UdXBsZSBpbW11dGFiaWxpdHkgaXMgc2hhbGxvdy4gSWYgYSB0dXBsZSBob2xkcyBhIG11dGFibGUgb2JqZWN0LCBzdWNoIGFzIGEgbGlzdCwgdGhhdCBpbm5lciBvYmplY3QgY2FuIHN0aWxsIGNoYW5nZSwgYW5kIHNvIHRoZSB0dXBsZSdzIHZhbHVlIGNoYW5nZXMuIEJlY2F1c2Ugb2YgdGhpcywgYSB0dXBsZSBjYW4gYmUgYSBgZGljdGAga2V5IG9yIGBzZXRgIG1lbWJlciBvbmx5IGlmIGV2ZXJ5dGhpbmcgaW5zaWRlIGl0IGlzIGhhc2hhYmxlLiBBIGxpc3QgY2FuIG5ldmVyIGJlIGEga2V5LlxuXG5Tb3VyY2VzOlxuLSBodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3R1dG9yaWFsL2RhdGFzdHJ1Y3R1cmVzLmh0bWxcbi0gaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9yZWZlcmVuY2UvZGF0YW1vZGVsLmh0bWxcbi0gaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9saWJyYXJ5L3N0ZHR5cGVzLmh0bWxcbiJ9LCJuYW1lIjoid3JpdGVfZmlsZSIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QkJhYTlnak1DY2UxNlRVZW4iLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6ODEyNywib3V0cHV0X3Rva2VucyI6NDMzLCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjo3OX0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtNzZlOS03Y2EzLWE1ZmYtNjNkZTY2ZGQ4ZGQxLTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoid3JpdGVfZmlsZSIsImFyZ3MiOnsiZmlsZV9wYXRoIjoiL2Fuc3dlci5tZCIsImNvbnRlbnQiOiIqKlR1cGxlIHZzLiBsaXN0IGltbXV0YWJpbGl0eSAoUHl0aG9uKSoqXG5cbkEgdHVwbGUgaXMgaW1tdXRhYmxlOiBvbmNlIGNyZWF0ZWQsIHlvdSBjYW4ndCBhc3NpZ24gdG8gaXRzIGl0ZW1zLCBhbmQgdGhlIGNvbGxlY3Rpb24gb2Ygb2JqZWN0cyBpdCByZWZlcmVuY2VzIGNhbid0IGNoYW5nZS4gYHRbMF0gPSAxYCByYWlzZXMgYFR5cGVFcnJvcmAuIEEgbGlzdCBpcyBtdXRhYmxlOiB5b3UgY2FuIGNoYW5nZSBpdCBpbiBwbGFjZSB3aXRoIGluZGV4IG9yIHNsaWNlIGFzc2lnbm1lbnQsIGBhcHBlbmQoKWAsIGBleHRlbmQoKWAsIGBzb3J0KClgIGFuZCBzaW1pbGFyIG1ldGhvZHMuXG5cblR1cGxlIGltbXV0YWJpbGl0eSBpcyBzaGFsbG93LiBJZiBhIHR1cGxlIGhvbGRzIGEgbXV0YWJsZSBvYmplY3QsIHN1Y2ggYXMgYSBsaXN0LCB0aGF0IGlubmVyIG9iamVjdCBjYW4gc3RpbGwgY2hhbmdlLCBhbmQgc28gdGhlIHR1cGxlJ3MgdmFsdWUgY2hhbmdlcy4gQmVjYXVzZSBvZiB0aGlzLCBhIHR1cGxlIGNhbiBiZSBhIGBkaWN0YCBrZXkgb3IgYHNldGAgbWVtYmVyIG9ubHkgaWYgZXZlcnl0aGluZyBpbnNpZGUgaXQgaXMgaGFzaGFibGUuIEEgbGlzdCBjYW4gbmV2ZXIgYmUgYSBrZXkuXG5cblNvdXJjZXM6XG4tIGh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFxuLSBodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3JlZmVyZW5jZS9kYXRhbW9kZWwuaHRtbFxuLSBodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL2xpYnJhcnkvc3RkdHlwZXMuaHRtbFxuIn0sImlkIjoidG9vbHVfMDFCbThBVTlFc1FVd3FvVkxZcHhLQTc3IiwidHlwZSI6InRvb2xfY2FsbCJ9XSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjo4MTI3LCJvdXRwdXRfdG9rZW5zIjo0MzMsInRvdGFsX3Rva2VucyI6ODU2MCwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6Nzl9fSwiaW52YWxpZF90b29sX2NhbGxzIjpbXX19fV1dLCJsbG1fb3V0cHV0Ijp7ImlkIjoibXNnXzAxMUNmZHdCQmFhOWdqTUNjZTE2VFVlbiIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo4MTI3LCJvdXRwdXRfdG9rZW5zIjo0MzMsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjc5fSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCJ9LCJydW4iOm51bGwsInR5cGUiOiJMTE1SZXN1bHQifQ==" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "04ffc0db9ce0f358", + "parentSpanId": "e8da3a18a9815276", + "name": "LangGraph", + "kind": 1, + "startTimeUnixNano": "1790968013178249984", + "endTimeUnixNano": "1790968042919047168", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "LangGraph" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langgraph" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W3sicm9sZSI6InVzZXIiLCJjb250ZW50IjoiRm9yIGEgc3ludGhldGljIHRyYWNpbmcgZml4dHVyZSwgZXhwbGFpbiBQeXRob24gdHVwbGUgdmVyc3VzIGxpc3QgaW1tdXRhYmlsaXR5IGluIGF0IG1vc3QgMTIwIHdvcmRzLiBVc2Ugb25seSBvZmZpY2lhbCBQeXRob24gZG9jdW1lbnRhdGlvbiBpZiB5b3Ugc2VhcmNoLiBIYXZlIHRoZSBza2VwdGljIGFuZCB2ZXJpZmllciBjaGVjayBpdCwgdGhlbiBoYXZlIHRoZSBlZGl0b3Igd3JpdGUgL2Fuc3dlci5tZC4gS2VlcCB0aGUgcnVuIHNob3J0IGFuZCBkbyBub3QgYXNrIGZvbGxvdy11cCBxdWVzdGlvbnMuIn1dfQ==" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W3siY29udGVudCI6IkZvciBhIHN5bnRoZXRpYyB0cmFjaW5nIGZpeHR1cmUsIGV4cGxhaW4gUHl0aG9uIHR1cGxlIHZlcnN1cyBsaXN0IGltbXV0YWJpbGl0eSBpbiBhdCBtb3N0IDEyMCB3b3Jkcy4gVXNlIG9ubHkgb2ZmaWNpYWwgUHl0aG9uIGRvY3VtZW50YXRpb24gaWYgeW91IHNlYXJjaC4gSGF2ZSB0aGUgc2tlcHRpYyBhbmQgdmVyaWZpZXIgY2hlY2sgaXQsIHRoZW4gaGF2ZSB0aGUgZWRpdG9yIHdyaXRlIC9hbnN3ZXIubWQuIEtlZXAgdGhlIHJ1biBzaG9ydCBhbmQgZG8gbm90IGFzayBmb2xsb3ctdXAgcXVlc3Rpb25zLiIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoiaHVtYW4iLCJpZCI6IjQ2MjA0MzA1LWY4ZWUtNDM4NC05Y2U4LWZjZDgwYmZjZGY3NSJ9LHsiY29udGVudCI6W3siaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9LCJuYW1lIjoid2ViX3NlYXJjaCIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHc5YVc5YnJlblBxZ3liZk53UCIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjoyODQ1LCJvdXRwdXRfdG9rZW5zIjo3Mywib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6MH0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoicmVzZWFyY2hlciIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy0yMWEzLTcwMDAtYTM4MS0yODRhNmEwYzAzZjctMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ3ZWJfc2VhcmNoIiwiYXJncyI6eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9LCJpZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6Mjg0NSwib3V0cHV0X3Rva2VucyI6NzMsInRvdGFsX3Rva2VucyI6MjkxOCwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fSx7ImNvbnRlbnQiOiJbe1widGl0bGVcIjogXCI1LiBEYXRhIFN0cnVjdHVyZXMgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3R1dG9yaWFsL2RhdGFzdHJ1Y3R1cmVzLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcIllvdSBtaWdodCBoYXZlIG5vdGljZWQgdGhhdCBtZXRob2RzIGxpa2VgaW5zZXJ0YCxgcmVtb3ZlYCBvcmBzb3J0YCB0aGF0IG9ubHkgbW9kaWZ5IHRoZSBsaXN0IGhhdmUgbm8gcmV0dXJuIHZhbHVlIHByaW50ZWQgXFx1MjAxMyB0aGV5IHJldHVybiB0aGUgZGVmYXVsdGBOb25lYC4gWzFdIFRoaXMgaXMgYSBkZXNpZ24gcHJpbmNpcGxlIGZvciBhbGwgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgaW4gUHl0aG9uLlxcbi4uLlxcbiMjIDUuMy4gVHVwbGVzIGFuZCBTZXF1ZW5jZXNcXHUwMGI2XFxuXFxuV2Ugc2F3IHRoYXQgbGlzdHMgYW5kIHN0cmluZ3MgaGF2ZSBtYW55IGNvbW1vbiBwcm9wZXJ0aWVzLCBzdWNoIGFzIGluZGV4aW5nIGFuZCBzbGljaW5nIG9wZXJhdGlvbnMuIFRoZXkgYXJlIHR3byBleGFtcGxlcyBvZiBzZXF1ZW5jZSBkYXRhIHR5cGVzIChzZWUgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBsaXN0LCB0dXBsZSwgcmFuZ2UpLiBTaW5jZSBQeXRob24gaXMgYW4gZXZvbHZpbmcgbGFuZ3VhZ2UsIG90aGVyIHNlcXVlbmNlIGRhdGEgdHlwZXMgbWF5IGJlIGFkZGVkLiBUaGVyZSBpcyBhbHNvIGFub3RoZXIgc3RhbmRhcmQgc2VxdWVuY2UgZGF0YSB0eXBlOiB0aGUgdHVwbGUuXFxuXFxuQSB0dXBsZSBjb25zaXN0cyBvZiBhIG51bWJlciBvZiB2YWx1ZXMgc2VwYXJhdGVkIGJ5IGNvbW1hcywgZm9yIGluc3RhbmNlOlxcbi4uLlxcbj4+PiAjIFR1cGxlcyBhcmUgaW1tdXRhYmxlOlxcbj4+PiB0WzBdID0gODg4ODhcXG5UcmFjZWJhY2sgKG1vc3QgcmVjZW50IGNhbGwgbGFzdCk6XFxuICBGaWxlIFxcXCI8c3RkaW4+XFxcIiwgbGluZSAxLCBpbiA8bW9kdWxlPlxcblR5cGVFcnJvcjogJ3R1cGxlJyBvYmplY3QgZG9lcyBub3Qgc3VwcG9ydCBpdGVtIGFzc2lnbm1lbnRcXG4+Pj4gIyBidXQgdGhleSBjYW4gY29udGFpbiBtdXRhYmxlIG9iamVjdHM6XFxuPj4+IHYgPSAoWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuPj4+IHZcXG4oWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuLi4uXFxuQXMgeW91IHNlZSwgb24gb3V0cHV0IHR1cGxlcyBhcmUgYWx3YXlzIGVuY2xvc2VkIGluIHBhcmVudGhlc2VzLCBzbyB0aGF0IG5lc3RlZCB0dXBsZXMgYXJlIGludGVycHJldGVkIGNvcnJlY3RseTsgdGhleSBtYXkgYmUgaW5wdXQgd2l0aCBvciB3aXRob3V0IHN1cnJvdW5kaW5nIHBhcmVudGhlc2VzLCBhbHRob3VnaCBvZnRlbiBwYXJlbnRoZXNlcyBhcmUgbmVjZXNzYXJ5IGFueXdheSAoaWYgdGhlIHR1cGxlIGlzIHBhcnQgb2YgYSBsYXJnZXIgZXhwcmVzc2lvbikuIEl0IGlzIG5vdCBwb3NzaWJsZSB0byBhc3NpZ24gdG8gdGhlIGluZGl2aWR1YWwgaXRlbXMgb2YgYSB0dXBsZSwgaG93ZXZlciBpdCBpcyBwb3NzaWJsZSB0byBjcmVhdGUgdHVwbGVzIHdoaWNoIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzLCBzdWNoIGFzIGxpc3RzLlxcblxcblRob3VnaCB0dXBsZXMgbWF5IHNlZW0gc2ltaWxhciB0byBsaXN0cywgdGhleSBhcmUgb2Z0ZW4gdXNlZCBpbiBkaWZmZXJlbnQgc2l0dWF0aW9ucyBhbmQgZm9yIGRpZmZlcmVudCBwdXJwb3Nlcy4gVHVwbGVzIGFyZSBpbW11dGFibGUsIGFuZCB1c3VhbGx5IGNvbnRhaW4gYSBoZXRlcm9nZW5lb3VzIHNlcXVlbmNlIG9mIGVsZW1lbnRzIHRoYXQgYXJlIGFjY2Vzc2VkIHZpYSB1bnBhY2tpbmcgKHNlZSBsYXRlciBpbiB0aGlzIHNlY3Rpb24pIG9yIGluZGV4aW5nIChvciBldmVuIGJ5IGF0dHJpYnV0ZSBpbiB0aGUgY2FzZSBvZiBuYW1lZHR1cGxlcykuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG5Bbm90aGVyIHVzZWZ1bCBkYXRhIHR5cGUgYnVpbHQgaW50byBQeXRob24gaXMgdGhlIGRpY3Rpb25hcnkgKHNlZSBNYXBwaW5nIFR5cGVzIFxcdTIwMTQgZGljdCkuIERpY3Rpb25hcmllcyBhcmUgc29tZXRpbWVzIGZvdW5kIGluIG90aGVyIGxhbmd1YWdlcyBhcyBcXHUyMDFjYXNzb2NpYXRpdmUgbWVtb3JpZXNcXHUyMDFkIG9yIFxcdTIwMWNhc3NvY2lhdGl2ZSBhcnJheXNcXHUyMDFkLiBVbmxpa2Ugc2VxdWVuY2VzLCB3aGljaCBhcmUgaW5kZXhlZCBieSBhIHJhbmdlIG9mIG51bWJlcnMsIGRpY3Rpb25hcmllcyBhcmUgaW5kZXhlZCBieSBrZXlzLCB3aGljaCBjYW4gYmUgYW55IGltbXV0YWJsZSB0eXBlOyBzdHJpbmdzIGFuZCBudW1iZXJzIGNhbiBhbHdheXMgYmUga2V5cy4gVHVwbGVzIGNhbiBiZSB1c2VkIGFzIGtleXMgaWYgdGhleSBjb250YWluIG9ubHkgc3RyaW5ncywgbnVtYmVycywgb3IgdHVwbGVzOyBpZiBhIHR1cGxlIGNvbnRhaW5zIGFueSBtdXRhYmxlIG9iamVjdCBlaXRoZXIgZGlyZWN0bHkgb3IgaW5kaXJlY3RseSwgaXQgY2Fubm90IGJlIHVzZWQgYXMgYSBrZXkuIFlvdSBjYW5cXHUyMDE5dCB1c2UgbGlzdHMgYXMga2V5cywgc2luY2UgbGlzdHMgY2FuIGJlIG1vZGlmaWVkIGluIHBsYWNlIHVzaW5nIGluZGV4IGFzc2lnbm1lbnRzLCBzbGljZSBhc3NpZ25tZW50cywgb3IgbWV0aG9kcyBsaWtlIGFwcGVuZCgpIGFuZCBleHRlbmQoKS5cIl19LCB7XCJ0aXRsZVwiOiBcIjMuIERhdGEgbW9kZWwgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3JlZmVyZW5jZS9kYXRhbW9kZWwuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiVGhlIHZhbHVlIG9mIHNvbWUgb2JqZWN0cyBjYW4gY2hhbmdlLiBPYmplY3RzIHdob3NlIHZhbHVlIGNhbiBjaGFuZ2UgYXJlIHNhaWQgdG8gYmUgbXV0YWJsZTsgb2JqZWN0cyB3aG9zZSB2YWx1ZSBpcyB1bmNoYW5nZWFibGUgb25jZSB0aGV5IGFyZSBjcmVhdGVkIGFyZSBjYWxsZWQgaW1tdXRhYmxlLiAoVGhlIHZhbHVlIG9mIGFuIGltbXV0YWJsZSBjb250YWluZXIgb2JqZWN0IHRoYXQgY29udGFpbnMgYSByZWZlcmVuY2UgdG8gYSBtdXRhYmxlIG9iamVjdCBjYW4gY2hhbmdlIHdoZW4gdGhlIGxhdHRlclxcdTIwMTlzIHZhbHVlIGlzIGNoYW5nZWQ7IGhvd2V2ZXIgdGhlIGNvbnRhaW5lciBpcyBzdGlsbCBjb25zaWRlcmVkIGltbXV0YWJsZSwgYmVjYXVzZSB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGl0IGNvbnRhaW5zIGNhbm5vdCBiZSBjaGFuZ2VkLiBTbywgaW1tdXRhYmlsaXR5IGlzIG5vdCBzdHJpY3RseSB0aGUgc2FtZSBhcyBoYXZpbmcgYW4gdW5jaGFuZ2VhYmxlIHZhbHVlLCBpdCBpcyBtb3JlIHN1YnRsZS4pIEFuIG9iamVjdFxcdTIwMTlzIG11dGFiaWxpdHkgaXMgZGV0ZXJtaW5lZCBieSBpdHMgdHlwZTsgZm9yIGluc3RhbmNlLCBudW1iZXJzLCBzdHJpbmdzIGFuZCB0dXBsZXMgYXJlIGltbXV0YWJsZSwgd2hpbGUgZGljdGlvbmFyaWVzIGFuZCBsaXN0cyBhcmUgbXV0YWJsZS5cXG4uLi5cXG5Tb21lIG9iamVjdHMgY29udGFpbiByZWZlcmVuY2VzIHRvIG90aGVyIG9iamVjdHM7IHRoZXNlIGFyZSBjYWxsZWQgY29udGFpbmVycy4gRXhhbXBsZXMgb2YgY29udGFpbmVycyBhcmUgdHVwbGVzLCBsaXN0cyBhbmQgZGljdGlvbmFyaWVzLiBUaGUgcmVmZXJlbmNlcyBhcmUgcGFydCBvZiBhIGNvbnRhaW5lclxcdTIwMTlzIHZhbHVlLiBJbiBtb3N0IGNhc2VzLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIHZhbHVlIG9mIGEgY29udGFpbmVyLCB3ZSBpbXBseSB0aGUgdmFsdWVzLCBub3QgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGNvbnRhaW5lZCBvYmplY3RzOyBob3dldmVyLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIG11dGFiaWxpdHkgb2YgYSBjb250YWluZXIsIG9ubHkgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGltbWVkaWF0ZWx5IGNvbnRhaW5lZCBvYmplY3RzIGFyZSBpbXBsaWVkLiBTbywgaWYgYW4gaW1tdXRhYmxlIGNvbnRhaW5lciAobGlrZSBhIHR1cGxlKSBjb250YWlucyBhIHJlZmVyZW5jZSB0byBhIG11dGFibGUgb2JqZWN0LCBpdHMgdmFsdWUgY2hhbmdlcyBpZiB0aGF0IG11dGFibGUgb2JqZWN0IGlzIGNoYW5nZWQuXFxuLi4uXFxuIyMjIDMuMi41LiBTZXF1ZW5jZXNcXHUwMGI2XFxuLi4uXFxuIyMjIyAzLjIuNS4xLiBJbW11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbkFuIG9iamVjdCBvZiBhbiBpbW11dGFibGUgc2VxdWVuY2UgdHlwZSBjYW5ub3QgY2hhbmdlIG9uY2UgaXQgaXMgY3JlYXRlZC4gKElmIHRoZSBvYmplY3QgY29udGFpbnMgcmVmZXJlbmNlcyB0byBvdGhlciBvYmplY3RzLCB0aGVzZSBvdGhlciBvYmplY3RzIG1heSBiZSBtdXRhYmxlIGFuZCBtYXkgYmUgY2hhbmdlZDsgaG93ZXZlciwgdGhlIGNvbGxlY3Rpb24gb2Ygb2JqZWN0cyBkaXJlY3RseSByZWZlcmVuY2VkIGJ5IGFuIGltbXV0YWJsZSBvYmplY3QgY2Fubm90IGNoYW5nZS4pXFxuXFxuVGhlIGZvbGxvd2luZyB0eXBlcyBhcmUgaW1tdXRhYmxlIHNlcXVlbmNlczpcXG4uLi5cXG5UdXBsZXNcXG46IFRoZSBpdGVtcyBvZiBhIGB0dXBsZWAgYXJlIGFyYml0cmFyeSBQeXRob24gb2JqZWN0cy4gVHVwbGVzIG9mIHR3byBvciBtb3JlIGl0ZW1zIGFyZSBmb3JtZWQgYnkgY29tbWEtc2VwYXJhdGVkIGxpc3RzIG9mIGV4cHJlc3Npb25zLiBBIHR1cGxlIG9mIG9uZSBpdGVtIChhIFxcdTIwMThzaW5nbGV0b25cXHUyMDE5KSBjYW4gYmUgZm9ybWVkIGJ5IGFmZml4aW5nIGEgY29tbWEgdG8gYW4gZXhwcmVzc2lvbiAoYW4gZXhwcmVzc2lvbiBieSBpdHNlbGYgZG9lcyBub3QgY3JlYXRlIGEgdHVwbGUsIHNpbmNlIHBhcmVudGhlc2VzIG11c3QgYmUgdXNhYmxlIGZvciBncm91cGluZyBvZiBleHByZXNzaW9ucykuIEFuIGVtcHR5IHR1cGxlIGNhbiBiZSBmb3JtZWQgYnkgYW4gZW1wdHkgcGFpciBvZiBwYXJlbnRoZXNlcy5cXG4uLi5cXG4jIyMjIDMuMi41LjIuIE11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbk11dGFibGUgc2VxdWVuY2VzIGNhbiBiZSBjaGFuZ2VkIGFmdGVyIHRoZXkgYXJlIGNyZWF0ZWQuIFRoZSBzdWJzY3JpcHRpb24gYW5kIHNsaWNpbmcgbm90YXRpb25zIGNhbiBiZSB1c2VkIGFzIHRoZSB0YXJnZXQgb2YgYXNzaWdubWVudCBhbmQgYGRlbGAgKGRlbGV0ZSkgc3RhdGVtZW50cy5cXG4uLi5cXG5UaGVyZSBhcmUgY3VycmVudGx5IHR3byBpbnRyaW5zaWMgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlczpcXG5cXG5MaXN0c1xcbjogVGhlIGl0ZW1zIG9mIGEgbGlzdCBhcmUgYXJiaXRyYXJ5IFB5dGhvbiBvYmplY3RzLiBMaXN0cyBhcmUgZm9ybWVkIGJ5IHBsYWNpbmcgYSBjb21tYS1zZXBhcmF0ZWQgbGlzdCBvZiBleHByZXNzaW9ucyBpbiBzcXVhcmUgYnJhY2tldHMuIChOb3RlIHRoYXQgdGhlcmUgYXJlIG5vIHNwZWNpYWwgY2FzZXMgbmVlZGVkIHRvIGZvcm0gbGlzdHMgb2YgbGVuZ3RoIDAgb3IgMS4pXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjcgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvYnVpbHRpbnMvc3RkdHlwZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiY29sbGVjdGlvbiBjbGFzc2VzIGFyZSBtdXRhYmxlLiBUaGUgbWV0aG9kcyB0aGF0IGFkZCwgc3VidHJhY3QsIG9yIHJlYXJyYW5nZSB0aGVpciAuLi4gaW4gcGxhY2UsIGFuZCBkb25cXHUyMDE5dCByZXR1cm4gYSAuLi4gLCBuZXZlciByZXR1cm4gdGhlIGNvbGxlY3Rpb24gaW5zdGFuY2UgaXRzZWxmIGJ1dCBgTm9uZWAuXFxuLi4uXFxuIyMgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBgbGlzdGAsIGB0dXBsZWAsIGByYW5nZWBcXHUwMGI2XFxuXFxuVGhlcmUgYXJlIHRocmVlIGJhc2ljIHNlcXVlbmNlIHR5cGVzOiBsaXN0cywgdHVwbGVzLCBhbmQgcmFuZ2Ugb2JqZWN0cy4gQWRkaXRpb25hbCBzZXF1ZW5jZSB0eXBlcyB0YWlsb3JlZCBmb3IgcHJvY2Vzc2luZyBvZiBiaW5hcnkgZGF0YSBhbmQgdGV4dCBzdHJpbmdzIGFyZSBkZXNjcmliZWQgaW4gZGVkaWNhdGVkIHNlY3Rpb25zLlxcbi4uLlxcblRoZSBvcGVyYXRpb25zIGluIHRoZSBmb2xsb3dpbmcgdGFibGUgLi4uIG11dGFibGUgYW5kIGltbXV0YWJsZS4gVGhlIGBjb2xsZWN0aW9ucy4gLi4uIGlzIHByb3ZpZGVkIHRvIG1ha2UgLi4uIGVhc2llciB0byBjb3JyZWN0bHkgaW1wbGVtZW50IHRoZXNlIG9wZXJhdGlvbnMgb24gY3VzdG9tIHNlcXVlbmNlIHR5cGVzXFxuLi4uXFxuIyMjIEltbXV0YWJsZSBTZXF1ZW5jZSBUeXBlc1xcdTAwYjZcXG4uLi5cXG5tdXRhYmxlIHNlcXVlbmNlIHR5cGVzIGlzXFxuLi4uXFxuc3VwcG9ydCBhbGxvd3MgaW1tdXRhYmxlIHNlcXVlbmNlcywgLi4uICwgdG8gYmUgdXNlZCBhcyBgZGljdGAga2V5cyBhbmQgc3RvcmVkIGluIC4uLiBlbnNldGAgaW5zdGFuY2VzXFxuLi4uXFxuIyMjIE11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxuIyMjIExpc3RzXFx1MDBiNlxcblxcbkxpc3RzIGFyZSBtdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaG9tb2dlbmVvdXMgaXRlbXMgKHdoZXJlIHRoZSBwcmVjaXNlIGRlZ3JlZSBvZiBzaW1pbGFyaXR5IHdpbGwgdmFyeSBieSBhcHBsaWNhdGlvbikuXFxuLi4uXFxuIyMjIFR1cGxlc1xcdTAwYjZcXG5cXG5UdXBsZXMgYXJlIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhldGVyb2dlbmVvdXMgZGF0YSAoc3VjaCBhcyB0aGUgMi10dXBsZXMgcHJvZHVjZWQgYnkgdGhlIGBlbnVtZXJhdGUoKWAgYnVpbHQtaW4pLiBUdXBsZXMgYXJlIGFsc28gdXNlZCBmb3IgY2FzZXMgd2hlcmUgYW4gaW1tdXRhYmxlIHNlcXVlbmNlIG9mIGhvbW9nZW5lb3VzIGRhdGEgaXMgbmVlZGVkIChzdWNoIGFzIGFsbG93aW5nIHN0b3JhZ2UgaW4gYSBgc2V0YCBvciBgZGljdGAgaW5zdGFuY2UpLlxcbi4uLlxcbnR1cGxlKGl0ZXJhYmxlPVxcbi4uLlxcblRoZSBjb25zdHJ1Y3RvciBidWlsZHMgYSB0dXBsZSB3aG9zZSBpdGVtcyBhcmUgdGhlIHNhbWUgYW5kIGluIHRoZSBzYW1lIG9yZGVyIGFzIGl0ZXJhYmxlXFx1MjAxOXMgaXRlbXMuIGl0ZXJhYmxlIG1heSBiZSBlaXRoZXIgYSBzZXF1ZW5jZSwgYSBjb250YWluZXIgdGhhdCBzdXBwb3J0cyBpdGVyYXRpb24sIG9yIGFuIGl0ZXJhdG9yIG9iamVjdC4gSWYgaXRlcmFibGUgaXMgYWxyZWFkeSBhIHR1cGxlLCBpdCBpcyByZXR1cm5lZCB1bmNoYW5nZWQuIEZvciBleGFtcGxlLCBgdHVwbGUoJ2FiYycpYCByZXR1cm5zIGAoJ2EnLCAnYicsICdjJylgIGFuZCBgdHVwbGUoIFsxLCAyLCAzXSApYCByZXR1cm5zIGAoMSwgMiwgMylgLiBJZiBubyBhcmd1bWVudCBpcyBnaXZlbiwgdGhlIGNvbnN0cnVjdG9yIGNyZWF0ZXMgYSBuZXcgZW1wdHkgdHVwbGUsIGAoKWAuXFxuLi4uXFxuVHVwbGVzIGltcGxlbWVudCBhbGwgb2YgdGhlIGNvbW1vbiBzZXF1ZW5jZSBvcGVyYXRpb25zLlxcbi4uLlxcbkZvciBoZXRlcm9nZW5lb3VzIGNvbGxlY3Rpb25zIG9mIGRhdGEgd2hlcmUgYWNjZXNzIGJ5IG5hbWUgaXMgY2xlYXJlciB0aGFuIGFjY2VzcyBieSBpbmRleCwgYGNvbGxlY3Rpb25zLm5hbWVkdHVwbGUoKWAgbWF5IGJlIGEgbW9yZSBhcHByb3ByaWF0ZSBjaG9pY2UgdGhhbiBhIHNpbXBsZSB0dXBsZSBvYmplY3QuXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvbGlicmFyeS9zdGR0eXBlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJTb21lIGNvbGxlY3Rpb24gY2xhc3NlcyBhcmUgbXV0YWJsZS4gVGhlIG1ldGhvZHMgdGhhdCBhZGQsIHN1YnRyYWN0LCBvciByZWFycmFuZ2UgdGhlaXIgLi4uIGluIHBsYWNlLCBhbmQgZG9uXFx1MjAxOXQgcmV0dXJuIGEgLi4uICwgbmV2ZXIgcmV0dXJuIHRoZSBjb2xsZWN0aW9uIGluc3RhbmNlIGl0c2VsZiBidXQgYE5vbmVgLlxcbi4uLlxcbiMjIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgYGxpc3RgLCBgdHVwbGVgLCBgcmFuZ2VgXFx1MDBiNlxcblxcblRoZXJlIGFyZSB0aHJlZSBiYXNpYyBzZXF1ZW5jZSB0eXBlczogbGlzdHMsIHR1cGxlcywgYW5kIC4uLiBvYmplY3RzLiBBZGRpdGlvbmFsIHNlcXVlbmNlIHR5cGVzIHRhaWxvcmVkIGZvciBwcm9jZXNzaW5nIG9mIGJpbmFyeSBkYXRhIGFuZCB0ZXh0IHN0cmluZ3MgYXJlIGRlc2NyaWJlZCBpbiBkZWRpY2F0ZWQgc2VjdGlvbnMuXFxuLi4uXFxuVGhlIG9wZXJhdGlvbnMgaW4gdGhlIGZvbGxvd2luZyB0YWJsZSAuLi4gaW1tdXRhYmxlLiBUaGUgYCAuLi4gaXMgcHJvdmlkZWQgdG8gbWFrZSAuLi4gZWFzaWVyIHRvIGNvcnJlY3RseSBpbXBsZW1lbnQgdGhlc2Ugb3BlcmF0aW9ucyBvbiBjdXN0b20gc2VxdWVuY2UgdHlwZXNcXG4uLi5cXG4jIyMgSW1tdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbm9wZXJhdGlvbiB0aGF0IGltbXV0YWJsZSBzZXF1ZW5jZSAuLi4gYnkgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlcyBpcyAuLi4gYGhhc2goKWBcXG4uLi5cXG5UaGlzIHN1cHBvcnQgYWxsb3dzIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHN1Y2ggYXMgYHR1cGxlYCBpbnN0YW5jZXMsIHRvIGJlIHVzZWQgYXMgYGRpY3RgIGtleXMgYW5kIHN0b3JlZCBpbiBgc2V0YCBhbmQgYGZyb3plbnNldGAgaW5zdGFuY2VzLlxcbi4uLlxcbiMjIyBNdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbiMjIyBMaXN0c1xcdTAwYjZcXG5cXG5MaXN0cyBhcmUgbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhvbW9nZW5lb3VzIGl0ZW1zICh3aGVyZSB0aGUgcHJlY2lzZSBkZWdyZWUgb2Ygc2ltaWxhcml0eSB3aWxsIHZhcnkgYnkgYXBwbGljYXRpb24pLlxcbi4uLlxcbiMjIyBUdXBsZXNcXHUwMGI2XFxuXFxuVHVwbGVzIGFyZSBpbW11dGFibGUgc2VxdWVuY2VzLCB0eXBpY2FsbHkgdXNlZCB0byBzdG9yZSBjb2xsZWN0aW9ucyBvZiBoZXRlcm9nZW5lb3VzIGRhdGEgKHN1Y2ggYXMgdGhlIDItdHVwbGVzIHByb2R1Y2VkIGJ5IHRoZSBgZW51bWVyYXRlKClgIGJ1aWx0LWluKS4gVHVwbGVzIGFyZSBhbHNvIHVzZWQgZm9yIGNhc2VzIHdoZXJlIGFuIGltbXV0YWJsZSBzZXF1ZW5jZSBvZiBob21vZ2VuZW91cyBkYXRhIGlzIG5lZWRlZCAoc3VjaCBhcyBhbGxvd2luZyBzdG9yYWdlIGluIGEgYHNldGAgb3IgYGRpY3RgIGluc3RhbmNlKS5cXG4uLi5cXG5UaGUgY29uc3RydWN0b3IgYnVpbGRzIGEgdHVwbGUgd2hvc2UgaXRlbXMgYXJlIHRoZSBzYW1lIGFuZCBpbiB0aGUgc2FtZSBvcmRlciBhcyBpdGVyYWJsZVxcdTIwMTlzIGl0ZW1zLiBpdGVyYWJsZSBtYXkgYmUgZWl0aGVyIGEgc2VxdWVuY2UsIGEgY29udGFpbmVyIHRoYXQgc3VwcG9ydHMgaXRlcmF0aW9uLCBvciBhbiBpdGVyYXRvciBvYmplY3QuIElmIGl0ZXJhYmxlIGlzIGFscmVhZHkgYSB0dXBsZSwgaXQgaXMgcmV0dXJuZWQgdW5jaGFuZ2VkLiBGb3IgZXhhbXBsZSwgYHR1cGxlKCdhYmMnKWAgcmV0dXJucyBgKCdhJywgJ2InLCAnYycpYCBhbmQgYHR1cGxlKCBbMSwgMiwgM10gKWAgcmV0dXJucyBgKDEsIDIsIDMpYC4gSWYgbm8gYXJndW1lbnQgaXMgZ2l2ZW4sIHRoZSBjb25zdHJ1Y3RvciBjcmVhdGVzIGEgbmV3IGVtcHR5IHR1cGxlLCBgKClgLlxcbi4uLlxcblR1cGxlcyBpbXBsZW1lbnQgYWxsIG9mIHRoZSBjb21tb24gc2VxdWVuY2Ugb3BlcmF0aW9ucy5cXG4uLi5cXG5Gb3IgaGV0ZXJvZ2VuZW91cyBjb2xsZWN0aW9ucyBvZiBkYXRhIHdoZXJlIGFjY2VzcyBieSBuYW1lIGlzIGNsZWFyZXIgdGhhbiBhY2Nlc3MgYnkgaW5kZXgsIGBjb2xsZWN0aW9ucy5uYW1lZHR1cGxlKClgIG1heSBiZSBhIG1vcmUgYXBwcm9wcmlhdGUgY2hvaWNlIHRoYW4gYSBzaW1wbGUgdHVwbGUgb2JqZWN0LlwiXX0sIHtcInRpdGxlXCI6IFwiNS4gRGF0YSBTdHJ1Y3R1cmVzIFxcdTIwMTQgUHl0aG9uIDMuMTAuMjAgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMuMTAvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiWW91IG1pZ2h0IGhhdmUgbm90aWNlZCB0aGF0IG1ldGhvZHMgbGlrZSBgaW5zZXJ0YCwgYHJlbW92ZWAgb3IgYHNvcnRgIHRoYXQgb25seSBtb2RpZnkgdGhlIGxpc3QgaGF2ZSBubyByZXR1cm4gdmFsdWUgcHJpbnRlZCBcXHUyMDEzIHRoZXkgcmV0dXJuIHRoZSBkZWZhdWx0IGBOb25lYC4gMSBUaGlzIGlzIGEgZGVzaWduIHByaW5jaXBsZSBmb3IgYWxsIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIGluIFB5dGhvbi5cXG4uLi5cXG4jIyA1LjMuIFR1cGxlcyBhbmQgU2VxdWVuY2VzXFx1MDBiNlxcblxcbldlIHNhdyB0aGF0IGxpc3RzIGFuZCBzdHJpbmdzIGhhdmUgbWFueSBjb21tb24gcHJvcGVydGllcywgc3VjaCBhcyBpbmRleGluZyBhbmQgc2xpY2luZyBvcGVyYXRpb25zLiBUaGV5IGFyZSB0d28gZXhhbXBsZXMgb2Ygc2VxdWVuY2UgZGF0YSB0eXBlcyAoc2VlIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgbGlzdCwgdHVwbGUsIHJhbmdlKS4gU2luY2UgUHl0aG9uIGlzIGFuIGV2b2x2aW5nIGxhbmd1YWdlLCBvdGhlciBzZXF1ZW5jZSBkYXRhIHR5cGVzIG1heSBiZSBhZGRlZC4gVGhlcmUgaXMgYWxzbyBhbm90aGVyIHN0YW5kYXJkIHNlcXVlbmNlIGRhdGEgdHlwZTogdGhlIHR1cGxlLlxcblxcbkEgdHVwbGUgY29uc2lzdHMgb2YgYSBudW1iZXIgb2YgdmFsdWVzIHNlcGFyYXRlZCBieSBjb21tYXMsIGZvciBpbnN0YW5jZTpcXG4uLi5cXG4+Pj4gIyBUdXBsZXMgYXJlIGltbXV0YWJsZTpcXG4uLi4gdFswXSA9IDg4ODg4XFxuVHJhY2ViYWNrIChtb3N0IHJlY2VudCBjYWxsIGxhc3QpOlxcbiAgRmlsZSBcXFwiPHN0ZGluPlxcXCIsIGxpbmUgMSwgaW4gPG1vZHVsZT5cXG5UeXBlRXJyb3I6ICd0dXBsZScgb2JqZWN0IGRvZXMgbm90IHN1cHBvcnQgaXRlbSBhc3NpZ25tZW50XFxuPj4+ICMgYnV0IHRoZXkgY2FuIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzOlxcbi4uLiB2ID0gKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbj4+PiB2XFxuKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbi4uLlxcbkFzIHlvdSBzZWUsIG9uIG91dHB1dCB0dXBsZXMgYXJlIGFsd2F5cyBlbmNsb3NlZCBpbiBwYXJlbnRoZXNlcywgc28gdGhhdCBuZXN0ZWQgdHVwbGVzIGFyZSBpbnRlcnByZXRlZCBjb3JyZWN0bHk7IHRoZXkgbWF5IGJlIGlucHV0IHdpdGggb3Igd2l0aG91dCBzdXJyb3VuZGluZyBwYXJlbnRoZXNlcywgYWx0aG91Z2ggb2Z0ZW4gcGFyZW50aGVzZXMgYXJlIG5lY2Vzc2FyeSBhbnl3YXkgKGlmIHRoZSB0dXBsZSBpcyBwYXJ0IG9mIGEgbGFyZ2VyIGV4cHJlc3Npb24pLiBJdCBpcyBub3QgcG9zc2libGUgdG8gYXNzaWduIHRvIHRoZSBpbmRpdmlkdWFsIGl0ZW1zIG9mIGEgdHVwbGUsIGhvd2V2ZXIgaXQgaXMgcG9zc2libGUgdG8gY3JlYXRlIHR1cGxlcyB3aGljaCBjb250YWluIG11dGFibGUgb2JqZWN0cywgc3VjaCBhcyBsaXN0cy5cXG5cXG5UaG91Z2ggdHVwbGVzIG1heSBzZWVtIHNpbWlsYXIgdG8gbGlzdHMsIHRoZXkgYXJlIG9mdGVuIHVzZWQgaW4gZGlmZmVyZW50IHNpdHVhdGlvbnMgYW5kIGZvciBkaWZmZXJlbnQgcHVycG9zZXMuIFR1cGxlcyBhcmUgaW1tdXRhYmxlLCBhbmQgdXN1YWxseSBjb250YWluIGEgaGV0ZXJvZ2VuZW91cyBzZXF1ZW5jZSBvZiBlbGVtZW50cyB0aGF0IGFyZSBhY2Nlc3NlZCB2aWEgdW5wYWNraW5nIChzZWUgbGF0ZXIgaW4gdGhpcyBzZWN0aW9uKSBvciBpbmRleGluZyAob3IgZXZlbiBieSBhdHRyaWJ1dGUgaW4gdGhlIGNhc2Ugb2YgYG5hbWVkdHVwbGVzYCkuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG4jIyA1LjQuXFxuLi4uXFxuLiBTZXQgb2JqZWN0c1xcbi4uLlxcbiMjIDUuNS4gRGljdGlvbmFyaWVzXFx1MDBiNlxcblxcbkFub3RoZXIgdXNlZnVsIGRhdGEgdHlwZSBidWlsdCBpbnRvIFB5dGhvbiBpcyB0aGUgZGljdGlvbmFyeSAoc2VlIE1hcHBpbmcgVHlwZXMgXFx1MjAxNCBkaWN0KS4gRGljdGlvbmFyaWVzIGFyZSBzb21ldGltZXMgZm91bmQgaW4gb3RoZXIgbGFuZ3VhZ2VzIGFzIFxcdTIwMWNhc3NvY2lhdGl2ZSBtZW1vcmllc1xcdTIwMWQgb3IgXFx1MjAxY2Fzc29jaWF0aXZlIGFycmF5c1xcdTIwMWQuIFVubGlrZSBzZXF1ZW5jZXMsIHdoaWNoIGFyZSBpbmRleGVkIGJ5IGEgcmFuZ2Ugb2YgbnVtYmVycywgZGljdGlvbmFyaWVzIGFyZSBpbmRleGVkIGJ5IGtleXMsIHdoaWNoIGNhbiBiZSBhbnkgaW1tdXRhYmxlIHR5cGU7IHN0cmluZ3MgYW5kIG51bWJlcnMgY2FuIGFsd2F5cyBiZSBrZXlzLiBUdXBsZXMgY2FuIGJlIHVzZWQgYXMga2V5cyBpZiB0aGV5IGNvbnRhaW4gb25seSBzdHJpbmdzLCBudW1iZXJzLCBvciB0dXBsZXM7IGlmIGEgdHVwbGUgY29udGFpbnMgYW55IG11dGFibGUgb2JqZWN0IGVpdGhlciBkaXJlY3RseSBvciBpbmRpcmVjdGx5LCBpdCBjYW5ub3QgYmUgdXNlZCBhcyBhIGtleS4gWW91IGNhblxcdTIwMTl0IHVzZSBsaXN0cyBhcyBrZXlzLCBzaW5jZSBsaXN0cyBjYW4gYmUgbW9kaWZpZWQgaW4gcGxhY2UgdXNpbmcgaW5kZXggYXNzaWdubWVudHMsIHNsaWNlIGFzc2lnbm1lbnRzLCBvciBtZXRob2RzIGxpa2UgYGFwcGVuZCgpYCBhbmQgYGV4dGVuZCgpYC5cIl19XSIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ3ZWJfc2VhcmNoIiwiaWQiOiI2ZTUxMjVhYy0zMTY0LTRhNzYtOWM1Mi1hYzM0NjBjZTM2ZTQiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJzdGF0dXMiOiJzdWNjZXNzIn0seyJjb250ZW50IjpbeyJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3NrZXB0aWMiLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QTZGNGNDTjV5aEQ0Q3JzOU0iLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjB9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InJlc2VhcmNoZXIiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtM2NkOC03OWYxLTg4YTEtY2U2Y2VlMjk0ZTc5LTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoidHJhbnNmZXJfdG9fc2tlcHRpYyIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsInRvdGFsX3Rva2VucyI6NzQxMSwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fSx7ImNvbnRlbnQiOiJTdWNjZXNzZnVsbHkgdHJhbnNmZXJyZWQgdG8gc2tlcHRpYyIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ0cmFuc2Zlcl90b19za2VwdGljIiwiaWQiOiIzODkwNThkMS0wODNmLTQ2MzItYmJjNS0wOWJmZDNhMGZmZDIiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMVhqajNhUmRXeWhCSmZQbVRKQnhRYTkiLCJzdGF0dXMiOiJzdWNjZXNzIn0seyJjb250ZW50IjpbeyJzaWduYXR1cmUiOiJDQVFTMVFnS0VBZ1NHQUk0QVVJSWRHaHBibXRwYm1jU0RONlRmVTZORE05NzFsRWJ6eG9NdlJVQTFKOW94M2ZKUVlUOUlqQXJDUVFHSEJQNHBSSFBhdlh6UmxiTWJhWUhDNTdZaitHYnhncjBoZWx2ZDJPSUwvbzZSYnQ0UEZBM0ZhSUZVbGdxOGdjN0tRSTlGdEdudlNBa0xCUnpJc1VwLzJkNVJkMnpnRXNma0ZWZVhzbjhXWVlaa2luQVFWVnk2QTdCM0xhRUpXMHhiL3g4OERpNjdmZWR3eDBRK0ZsWnR3Y21UM0RjYWExMUp2ZXlIUnIzZG0rekdTWlA2Q1hlWE5KOG5yVFo3VzFZUlR5c0Q2dzdvN0xwOGgxUndxNGY0bzhqK1FVZlkwdmplK3pKWnN4SFBtUU50M3lrZUtoOTloZ242ZGJpejYvRUhxbWJIalNBdTFNNFNxRURmcUlLVGhIWWVuYW1BWm1jUTRYQU9vNm1kUTYrNzdWZ3BWMWxDMS9rNTJJaVVvSmprR2xRcm5MQ3Z1dGlJN0tOK3NnSzhjMisyS0NKclBSQ3ZWVitCa1N0dlA1aldXcXp5dHBLUStuSHpoTDQ0OE9ySUE5Z2RKQzM5QzhlRHJYZVJuSTd0NEZxbGtYR29NbFpBVjlVTWNyUUhSeE9NaTh0T3lUemk5b3lCRkRUU3NRK0xKcWVyMHR6MUZ0OWdlazZ2emc1MFNwVkFGUWRMRVJ2OFIxWXFNYWVJK3hzdlRBRVc5TVJndlJNa3pMdXpSVDJndXczakQrRmppcDM3eGdLNVFJMFIrQ3NPQW1RUG9UWml6emNJeUFVQXVDSkZDZ3EvZ0QvQ2liTHhveW1pVlVJMWFuOVNudDNLcE5Sb1dkdldJbmUzM1YwcHY4UEhnOXlXMkp1TE9xUUdpbExDc0svUTNvUXIzdXBFTVh4WEdqQ0w3V1VpenZSWkdGRUVWK1ZwbFl0N3Y0dEtaTWo1cGszdmRlTTBNSjVYWmc3UHZVc1J6eWN4Y25yVDNLbm9jM3FNK3R0cVpmVHNiOGQ2VS9wUWlmcTdHdWFoRk9IcnZyb3FnSkxqKy84MVl5MXBaRmtqZkpnWTBYa2x6QW5BNkNGcS9ZVDBneGJWK1RINUlTK1dhWU9VekszNlM4czVxcU13aG5ndjRJekUzY3RYWHFuaElaWE56bnlWQnFiWWEvR3gyYkNqY0VqMm41UnlHWVJTLytNY3VsZ0xCem5ROU1rRzJEN1FQM01xL253SFhLTUlhTFM4Vks4aEczZDByQWpsdm5hdVhTaE5ySnh0a2o0cWt3bnhENExyRWprMXVlK1BMY2JMcVlXd0FyOG4wcys0MWpyc0lQVnl5Szl2RmFFWnJBOUthQnhrSnlpcXg1MEdaTm43Y2t4K21rSTE4Z0g1Mml5VlB0VThGS01YanVnQ2w2bVBMaUhZaURRZCtHNjNMdWRtT0t5czlpdFZuQkRCTk1QMmIvSTcrYklWUmxoZUdhWldyNk1BVEtsODZCM3krMGt1T3JMUk15V2NkaWJjMmlRa1ptKzhmN2RUVXdiS2JCSnNLT0ZZNzMvOG5sTC9KaVlwano2blVJaytJcVNqYUZ5SzZYRmxDY3VrV3BXSHBESm5TdmJFemV5N3FIeUNxSitJK29uOGlQOEdPdGVtcFFhUzAvNGtaR1N3bHJFNnhPajZFd0dwZmpiRFpLSGtQLzJSQWROWVBRcTN6N2JkVkpNSk5uRk16NmUzN216TXBDQmlod1BkU0ZUUkUzU0NZNjZ3dlRnSnpHQ1V5aEhiU2k1aTFhenpPWGQ4dUhDU3BULzZMMW82VlUwcGpvUjhLYWE0d0NXSEdmN1AvV0VkK2F2eXAwMkpudTFjbXBXMCtHcSt2SzloMHRkYjJJQ2xNL2VOZmRvdVFQK2VwOE1KVWpyYURSSUNtQUpRWTVvYlM3MXZuaU9vK3JlYUZ5MHpSZ0IiLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsic2lnbmF0dXJlIjoiQ0FRUy9SRUtFUWdTR0FJNEFVSUpibUZ5Y21GMGFXOXVFZ3dGaDNBVkpaUDNzOHkxSHEwYURHNzZ1aEhPTmFTY1hBR1NoaUl3R2dqTFhIVXgxeVpya0xQbGpqelRPUXljcExKaUtWd1hjeG0vRXpLSm0xczhNNVd4dVJFV0VsZFhjb2FOMC9tc0twa1JNVCtlTGI1aWg5ckFIVUxUb21VTnNVd0pldTBqckplNEdMYXp5N3NQNUt0akVPRUJLUXNYVWZvUTdmRGtaT1FVSFppOXp6UU40aEZObVlKVWYzT1ZxZjlUUU5kQzErbS8vSEFaUGZXbUF5Nkh0RHZtN2tnQkprQ0VvaUZubTd0bi9QWDFoWEdQUmdXYzZCVFhVTXIyV0E4YUt5cWdnUlViTUgyK0pZdjVpV1pkZENjMXVwMU5icjNzRjdXUGNvdDlaQmh5SFFYcjY5MnplSms2U0pneWdJVnJBU2JNbHkyYkJ4VFdKQjYyM2dWZ3pwQ21WU0xaRnBrS1hnUGNwOGhHeURLT3ZGNlNpUVNIcUdSeFFvZnI3VnMxYVBoZlppeFJPSkdEK01scmtnZ1ZBbkdNeVlJRFVacGF2dENlaVUxSm1CU3owMDg0YlhQdFQvdG5wc0tPblNQWVpRbzNCMEJYS2JxQS9Xc0tuZzgvVjNaOFV1YWpvNklWWjJoZ0ExV0RkdXFRT1FrRXhHVTZwRnVMaGViSWp1V3hTTkkvRGZmdlIvL0xOcUNmdmRuZjBodm9QdER4ZngvU2F3SXg0L3NwcTl4YndGekh0TGdvSGcxRjJ3Rnc3Y2xQRGxnMVB5RC92MnlKRzNWOVQ4R0w2akdQRTZyaGZpbC91NEhOdmY3QlcwZFF6Q1RUWGNxWnk0bkJ5eTArcCtTN1p4Zk1sNEtweGEyZ09oaFNOamZwZnVYMFFwTDRrTUU4SjFubGlGTjFkOEtxRVB1REkxOEJRSFVoRC9VZlkyOW9BMHg1MTBGak0vQnNMWjRuNXNwcGtvSWk3Q0ZvUXEyYUFLVk5mQzFLaWxoSXBSRCsxZHlEdFpRa2xOOGdacE9FU0h3Nk1MMVR0VTB5ZkFjYm1xb3hKNmhoanp3SDZDTXNUUUVFeThNYlJCTStRS0trSVJkUkNQSEdGVzJMa3VSNTArSjBwRi90cjZwcVZ2SmVvemZpTW5yRWlsQjI3VFpQNG5LVEpHOHJkLy92Q1dlbjB5N0NBLzhkQko5Z2RwNXR4N05WQTQ1azFnZGN4RVIxZ1lvdU5YM24xdkczL0IxRjQrOTN3WFc5TXFvNlVzNTlUUW1yYjlOcWE3cFNRTlNySDA2dzFHeHlCQWxrRmZxQWxhZmsxNmFNcHE0MTZUSllwR3k0M1dDVWlDY0xXbGhpV2d6MnNnT0xBWFA5YTV3ZVdBS1JDOEo2amtjVWtEZVhuVytIRlNpUE02KzlCbzZrVmhMVWhONXJjNG1IWk9acEY3cHRQcHNRTjRYQ3FMUHNxcFJILzhvNVhweUM2Wm5lVGVGMkV4dGpod1pWalhEd01iSXpWWUlidzF5MlBQR3c4ZEM3cGM0c0lWU25JNG9RQUgwdjY1MGdoZmhtTWV2V1c0aDRQeWxidy9ERzRmZGVUMys4ejBmOE1PU1R0ZmlweDA1L1F0dEhsQXZPRFJ4YTV5REN2T3o2bjNmMUJPME4vNE1iR3lPTTI3cVc0eDhtQlZvcnlVWldmc21oZmkwL0dmTnZCOCt5bFRHYllOWDI3T2NFRjVXWnROcWJXRXk3VjVHYm1aVVN4c3BjZzdGQlozajMvM0w3bXV2WlhvWmkxZjVlcnE0eTNsU0grb0Y3cVBGbnZpZXlMT0N3dWJjUXkrb3lvd2o0Y0QwaWRSZWNucCtzKzUyRDcrbXpGMWdwVytmK2NKYXNveGlnYnhrT2xvUzR5ZWlLdmdNNG5XU3BHRWoxMlJQeDVvODBFbXppWE45dDR6SnpXMWh0QTVoWEpKZUZ5bXBNWXR5Q1VDZXZSV1ZtclJZa3RRN1VuQ2E2dEwvQVZQaTFuaTVYQ1hIZkxPdXFmSzVLTFl4andWdGRoaGZtQ0x2cGFLVk5DRU9DZjRuTkdIcUQ5OHdVYjBlaldOd0RZQm5KR2tCUnc5UlZzcGZ3TmRvV29WY24ycXY4eXdHVXA3eWMvc2syTHBJNlZ6aVJ5dldxYkdORzFaSnFWVnV0UmM0NitzT0U0VlNlUzIvQ1ROS05hNk5NQ2YvaHh4YVVWc1lmS2RQK1MxTGM0aWFuS3Awc1V1Z1BwbXRveWRDMENQMlJ3am5ITGNuenhDbnZxeGxMUVdYTU5QVHN1MnlCY0hsV0pMdGNtZGcvTWNMYWd5N2w1a0c2c043WnhKTEQva0F4OEU2dXFvaFArSy9qTVNyWXVRSm5ocjR0ZjRiaVdGWTNKemYvS3BBWXlWU3ErcGJmRktYTWJMOW5IRW9aUVQxNVd5YWJkNkRNQXEzUVpPdGhNNXBVemROcVVYa2NySTFQd2pBU0FnNGRWVS8ybys5OG1UWG55aFA3VmxFa1RuZGRwd3lKRitZQ0NjMDFwcWE2Wlg5OXBuUitZT3BJcmI1RHRQY0txMHQ1Y0ZDRHlReE4xbFV1cVpjWXVqdlgvR0M3KzZJcFJUei9Oa0VZLzVjbnRPZlpKeWQ2U2tVeC9KUjVBSklSUENOZWliMHJjV25uRm4wWEs4czIyeDQwd0xqRXR1ZlhETzkreXJhZjRrSHhsQkkzU0IwbWVNamVwWDUwQ0poQXg3TmVra1JGbndvZWNMZnFJeXJqWHJnQVpzVE1QaVg5NHNuamRyemlzQVh0QkthbnVsaWxTeE9mVzJIU21mbmswTDJHWEhKSjNTK3ZDYmdSRW1sOWRla2ZtSGhtd0VZdHYza2ZodGg3RVJJd1ZUOGo2OU90SUJDbWVUSUp1QTdvNzNQWGMxTEoyd2h1Wks3RlplNk5UT3M2MU1QN0pWbEljNnRiaHRPQmszTlhVOHBCUE9DWTZRNTVkNjNGdHBVVS9LQ2JRMEsyV1E5bzVqTkd3Tk5KNkN3NUZEQ3QreDVZSmVQMWQyUXpEU2lxK2NTWnF3cUNaVmNOb0FmdEdHemdyUlo0dmtwYlAweFFaSEN0V3NVd0hWVGpyWTROZlJEakhkVlBSbjhKOVh0b29jL2hJV2txekZhY3pZVGxhcHUyUkNKcWU4YUVvM2p6V1pDRUo3QWlCekJZcmFZRGdPZmVpUXllSlVzYmJYSGRHS2lpdzIyQmRwNE0rWXo2NzFNWjBpZTlFV1JrczBRMkRIR1VUbmU5UzR0QUFKUEdiVWY0eHRDTW9KNDBRNFFXMDVRTFNaeTN3TkZBVUZZbVhFNEVDREZCOCtJRWVORkZGL3RnQW5maDhHdkZoLzF4WStMRStvek1taW5qU1J3aHEwOXpRQUVrTkZ5dTBJS2d2RGt5T0kraWNjRjdkb1dMVFVhc1B4TEgxeEdQQlNLa2VXQ25Tb1gvNEMyYzlkdTY2QmJSWGFtSHFhZStWQ2lTNkJOOTUzaVlLT3U4ck1rbmVXK1RGalYwQ3pBZjhQTkc5TEJheVVnWTVVdzl4MFNhcDZ6STRQcGFaS0g0ZDlrdGtGVSs5MXlCYk41U1J0ZGQ4L0VRYXRKWG1CTmZ3N2tScmxsK0xqd1p3ZVdMRWIwalQxUnBHdURxSExkTFRNTWdvd1NjenJwMnpDc0pkUUgvc2VhWXJwd2tDRlI4ZFJqTTdKUlVrVmExQTBTdmRaQVg3NXZ1QnZ2TEJ6TjJjK1JidDVvcXk5M2xxSUJJaktiN3cyVWtSVGI0cEJXY05LTlBVZ3QzYXovUHhReEVhc2FsdGRtdXl2c1hZYnVyM1ZQTm44SnpOLzZKNU4rUUNuejU3MVRjY2FwQ21rUHZGRStnYXlYVStVRjFWS1l5RmNVdmxldjdsc1FsbWpxeksyUUE4WDdRM1R6RmFwcWh2SktsdDdkekJIcmFxWkJnUGdiOU40b0tOYXlpOEZaVDcxK2thWWhJaEZyRVNLWVJIZkhrajVuM2tmaGdMeSt4K2pMRGJLeE1nZ2tnSG5KejlSdUVIWlhCV3RrWUFRPT0iLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsiaWQiOiJ0b29sdV8wMUg2ZlY4SnZSSk52WHk0N2VSbmQzakgiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6e30sIm5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHdBTVJkUzIxRmVKZnpRaE0zcyIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo3Mzk2LCJvdXRwdXRfdG9rZW5zIjo3NzAsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjczOX0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoic2tlcHRpYyIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy00YWU5LTc2YzAtODlhNi00ZjBiMTE0ZTMwMzAtMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzM5Niwib3V0cHV0X3Rva2VucyI6NzcwLCJ0b3RhbF90b2tlbnMiOjgxNjYsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjczOX19fSx7ImNvbnRlbnQiOiJTdWNjZXNzZnVsbHkgdHJhbnNmZXJyZWQgdG8gdmVyaWZpZXIiLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7fSwidHlwZSI6InRvb2wiLCJuYW1lIjoidHJhbnNmZXJfdG9fdmVyaWZpZXIiLCJpZCI6ImMwNjQ1NjNhLTk3NGYtNGYwZi1hYjRhLTA4MGU2M2MwMWNmNSIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsInN0YXR1cyI6InN1Y2Nlc3MifSx7ImNvbnRlbnQiOlt7InNpZ25hdHVyZSI6IkNBUVM5Z1VLRUFnU0dBSTRBVUlJZEdocGJtdHBibWNTREdGeUoxTVhvWStBaTZOa2J4b01SZDJUclVmVVU3V3JQcEEySWpEZ3k4ZFVwbGNaS2M3RG5Kd05MVEJmSDVVTlIrbkduYWtTUFFpYThVVkl6cmprT25KMmZMcldVc0Eyb0VPOHlSc3Frd1hra0RDc3loQjk3ZHdqNTdSTXQ5MVh6Z1doTDlsdU5FR085MnlXMHJORkdvTVNVVk94UElyRmM1Ump5cmhLRWMzKzFSbnB4Q2t5bHErTE5sUTVORnhMbjZ2MmNGNUR6cXB2QkhOUjdYQWlnK3VySklVM2dYeWRDa2FHWlpEZXRKdFFPVU9YZ1ppOXo5MTAwTnBkQUVrWERvMklDSENHVEdwVmMwSi9wYlh4V1JCeGI2QjEvR2wwWWpuNjhWNkhsZXNIN0lvQzdHUi8zSHdSdTlOU0RCRzR0dGZRakVXY2crUGVBZWE0Um43anp4K1RtOUg3TDFqb1RLVmdEYlFnNWlLTUxmanZQcWxua3NDTHRYT3Q4alNJUzVPWUxON2EvN1RYNi93NWQzNHZudzh2UERtVTFVWWkwUTcycVVXaFVhMnZ3OE9YTnBjS0w3QnAwbE5mQlM4cnZIWSt2Qlk4L2hhQW9UTDN3dkxGVTFmVCs0ei92QTNCY3NpZ1pEWWxmSmVEWWN4WG1SdXF4enhFR3BYQ1JlOXMxSFd0SUQrNDFwSHU2bHBaSGtYYWdocGM2VFpITHhOd2NWaFRiNmZNUTdRbysyTDZQdmtsaDFtMzJwZW9keGVnMllpUGhkcGwzdy94a09FdTUvSkVGN1pCeHBkVUwrdTN3a081cFVsc3FmZ1g3dWh6djdIQzJXcFNXVmltcndpUXdRT1V4MCtKUHJMdHcrbFlmcTdJM2pydW8wR1cwZ3VXdm51ZjdKTTNZTGo4NHErSmdIb21uU1Nabm5abXJMMFVhUnZyVnlMbzg3N0YzOWNjTFM3WG9abm51aThKNFA1OFpUbWlMa0VKZUdjeDFJdWFnMmUzMzMzOU83WWV3LzU5dExiUXRJR2g5VldNd0hCbkZ4MzlTOC80TEVGcks1SGxkUFFoT3Jsb3lUdGtSczY2aVFSS3R4TUtTMFZJNlFUS1pEQWJzN2E3SDF5QnYrbUppOVhnNHVLbXlVNnFubjd6RUtqalkyYklsQ3dleng0TExHUDVIRDNacjZmWnNtZEcxVUh6RHNCdllHMm9DSTBvcUVLTSt2YWxzVDZoVThNUmpiUUJEL0pKOEpKTi9pQkdsaEJBVUhvMFVtaGh0UjJldVRWcFpXbE1uaG1wWEFkS0RYU2ZQZGFQb2hnQiIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJpZCI6InRvb2x1XzAxWEVBdWhRSENCZHlDd3R5bVp3RmZEaSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3JlZF90ZWFtIiwidHlwZSI6InRvb2xfdXNlIiwidG9vbHNldF9uYW1lIjpudWxsfV0sImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2Zkd0FzYWJEcXVlZ0hpNllVcENuIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjc1MDksIm91dHB1dF90b2tlbnMiOjg4LCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjo1NX0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoidmVyaWZpZXIiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtNjY4Mi03ZDIxLThjMTUtZmZiY2U5NDIyYzlmLTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoidHJhbnNmZXJfdG9fcmVkX3RlYW0iLCJhcmdzIjp7fSwiaWQiOiJ0b29sdV8wMVhFQXVoUUhDQmR5Q3d0eW1ad0ZmRGkiLCJ0eXBlIjoidG9vbF9jYWxsIn1dLCJpbnZhbGlkX3Rvb2xfY2FsbHMiOltdLCJ1c2FnZV9tZXRhZGF0YSI6eyJpbnB1dF90b2tlbnMiOjc1MDksIm91dHB1dF90b2tlbnMiOjg4LCJ0b3RhbF90b2tlbnMiOjc1OTcsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjU1fX19LHsiY29udGVudCI6IlN1Y2Nlc3NmdWxseSB0cmFuc2ZlcnJlZCB0byByZWRfdGVhbSIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ0cmFuc2Zlcl90b19yZWRfdGVhbSIsImlkIjoiOTgyYzAxMjYtZTZhYi00NThhLTkyMmUtN2IyNjZlODNjMmIyIiwidG9vbF9jYWxsX2lkIjoidG9vbHVfMDFYRUF1aFFIQ0JkeUN3dHltWndGZkRpIiwic3RhdHVzIjoic3VjY2VzcyJ9LHsiY29udGVudCI6W3sic2lnbmF0dXJlIjoiQ0FRU3dnY0tFQWdTR0FJNEFVSUlkR2hwYm10cGJtY1NET1JYZ2U5dUwxODMyUk1Rd0JvTWpRd0F3QUhiOWlQMW9qUEdJakREU2NhbDQ4NmJlQ0pidEc4L0QyWTlnOVM1eElQZmpvSGcvZHh3WDVIeXBTUUVCYmpGaVRTYlBFQ1hTRnpWNllJcTN3YkxjbXlQYnZ1c0duRGV4Rmd4KzNzSVh6dnF5RlYvNjl4aGloTW5TcGZUQ0h5MCswelN1MVdwWVUvV1VwSWZvYVNYVGNUYTFxa004eDhFay95LzFqa0tNWlVmREZhOTN1aFVKeE9DNCtRZ1BXQmwvK1h5MnQ4MkwyeC9lYnE3UUxRVHF0UjBqWGVhVVJRbTNDbzZKdk9nV21Edk1wRWRUZVRXRTNmRGgxdHp6VktzZmx1bTRkODliNkNhS3RuOG5sUWQ3UmhOOTl2TlFNUUhHNVY0T3VWTmRySFBaWnBGT3VoZGVqc01GV1h5Qis1NXNsR0FkOHdFWGdGS2JJL2tuT1hWR3pML05ERjhHdHkxdE5abHUyZmRTQ0FzWVdNc0ZRUDFjYndMZVFHc3p5WHpKTVZaSjJJOXhCUGt5ejFIT1hDUHloNUYrajJnUlRQaWtWQjNBUkM2dGZCL2xEU1VlS0JWWlBRUzhwWS9CNGVDcytJamphQk5XcnVMemZxQ3NjNnhMMEN1UVBTWXlRWGFZRmpZclNSTmNDNzBXUUJXVDVWVmhwMG1BOXlaREdZYlJadDlJNU9LOVFrUW11SjV6REZhTUVPWHpTcnNDNGx0ZVA2TWRpbWlNaWp2c0gzaXErYzZXVVlvbFJqM25lSTg4RWFISXpMQUdScTY0Qk5DTHpkQVlMMnBFVTJzYjV1Rzc4UEtWb1VVQ2JWSkw1anBET2hMT1RPMWJRc1R1b1VwblJSckxRRC9rSXpWK2xqcFZCd3dmSmN5cTc1VEpBL0N0UHIveHIyU1o5QjFiMnNiakc2akQwbGh0b0NkZGVEZ2NxZ2wvTFNTa2N1YVlZOHhQV0N5K0x0TmhjcVRoZm1mdm5ITEpJcEw2Z2pialcvT1FwUmZWVXdmdzFqT2I5YldWd2NnSGhOWWZLWUdQdE04b3RoNXh3TFlSUlZnenFNS092czdmYWxYUmhWeloyU1A2SXcwQk1tQUFaNkhqZkNsOUwrdFBxSk5sblFRT0hwb1dqdW00QitYc1lsZEZFS0dRV1Fxc2Q1OWlOUHZhbzlwL3NDT3Q3ckdxQ296Y21naUVtNHc2NmV6L1FJRHliNDZuMCt0Y05KUFVjb0JLdlEvakR3RTZWd21rUS9uRUNqRnAzVWREVCtrV1lRU05kOGo4VkJaT3BHOExscVVZckVaQ3YwcTdCczF2Y05yZFlpYUZvUzMyb3NheW5XaytIdDNPSHVGV2JFRjhKbHNGVFFBTzA1VnZZa2VjZjBWL29USHNHNWh5T09zN1VFYURGcTd1YUp1eldWaGg1cEFvaklhQmNSOGZWbG4yUExSVFg3aXMybnNBK1FyZGlkZVBjWnRGdGd4QWlLWWUxaTJzU0ZpNHZtOEVPa0NiUTYzd2RRZVk0VXJyNVB1b01Fd3hVVUxqZ0thZ2lBbXNKRHE0UnJWbTdzTzQrNll1QTlqNmwydXlsa09FYzY3dmYvNWZrWHVDc2lBcE5MNmtlYjhjK3ZMMG40RlhHZHpqK2FwU0RmRkdCZ0IiLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsiaWQiOiJ0b29sdV8wMVZCcldkV0VXWWdpdDRrTlllaEJNVTUiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6e30sIm5hbWUiOiJ0cmFuc2Zlcl90b19lZGl0b3IiLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QXpmanFVb3BvRW1aTGFxbTEiLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzQxNiwib3V0cHV0X3Rva2VucyI6MTUxLCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjoxMTl9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InJlZF90ZWFtIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTZjZTYtNzI1MS1iNzk2LTRmNjQzMzNmNjdjNi0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6InRyYW5zZmVyX3RvX2VkaXRvciIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxVkJyV2RXRVdZZ2l0NGtOWWVoQk1VNSIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzQxNiwib3V0cHV0X3Rva2VucyI6MTUxLCJ0b3RhbF90b2tlbnMiOjc1NjcsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjExOX19fSx7ImNvbnRlbnQiOiJTdWNjZXNzZnVsbHkgdHJhbnNmZXJyZWQgdG8gZWRpdG9yIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJ0b29sIiwibmFtZSI6InRyYW5zZmVyX3RvX2VkaXRvciIsImlkIjoiNWQzOWUzNmItYzViNS00NDI2LWE5YWEtYmUyZjdkMzgxNmQ2IiwidG9vbF9jYWxsX2lkIjoidG9vbHVfMDFWQnJXZFdFV1lnaXQ0a05ZZWhCTVU1Iiwic3RhdHVzIjoic3VjY2VzcyJ9LHsiY29udGVudCI6W3sic2lnbmF0dXJlIjoiQ0FRUzVnWUtFQWdTR0FJNEFVSUlkR2hwYm10cGJtY1NESTRRSWI3TDJ3NWFXM2NNL1JvTUxpMndFeERrcWh1eEpEaVlJakFxR3hNNFdVd2lxNHVaSHJpQ2J1QzRrVndhN2lnZFVMb29jWmJpNWZldktiZndxSFNickxOVDUxTkdYNFhzTHRzcWd3WS90YWJIamxsenF1N25SV3Y4K0Z6NFI2bjlqYTdDcGoyNFp1eEZ1S2trUU9CbUE0VGYxYXBrM1JQcWNFUStqK3dlZHUwbE8rcmRFQ3lkM2xYaGZnRVJGRnFjTTZhdWN4ZWw0UmJ3NW9BSFh1OStYZVR0emwrTjh0QWVaOWlHWjJYVHRVNU9iOGFQVDFLdVgrdXA4a1E0VlVYa0JUZmQyZjVKUDlSeFpFcTl3U1lhWnN5SGR2MG4rMVU0NkFPc3ZLWm1wOVFYQndvMmY0TzkwalA4ZWdpcmVtOUViWmJtYVlPSjhXSzdDdEtSVmJZMnk0Kzl4UlVKMDRuc2RJejVGNUZGS0V4eE5PckI0bWMvTkU4M2FiM1o2NGlXek93cW9VR0FHUVNrYUgwZDVjRXVtWm80ZlpaQWhQeXVKZVFEeHVsTUNld243RUdDbnBKdy9uemkwd295RWFRTDRZN2g5OVA3YVh4RnIyOHA1S0N5SkVKVERlZmVJcFZsQXdNQ0Fqeit4QzY5QUFMbXd5VFErY0ZFTStLQWdlSVNwaExWeThBUHIyRlNoYXhCTnJYUWd6MXM5czJkaExaUU9CaXd6aHhtRWdkSzcrOEphcWhCMGVFRWQvMVJWQXVJWWRlekE1SEJyd0U5RHlRaUNsM1VPc2JTbk5FVmUrZ3JIcDVwM3RtTUJxNjhnTFY4Mm9BekZZWmwrQ2V3eDcxdjVkUEZPdktQRUhVenBRWU9WTHJuQjFFWlJZclp6NzJuMndmU1Z4Qk96cFRjbkJ4VjZIZWRDUHlJektYWk05K3lFZVRKaThRdHZWZ1oxdyt2MGhtQ0ZHcG5tWk43bVhsZ3FMMXM2TVhWeWRwUE9LUGxNM05hN0NkSG5QRllpd3Iyekx2a2tjSU03Q3NrL0MzK1pNQVdUSytDOUIxdmJHeFBsdEUzT3Y4a085OFBnU1htMUFKdUViWHkrcjNhM0QzUWxaSjNkRUtlMkxOZEFCeXJQTWhTY0tVOEdsZkgvOU50Z3JodGFrZ2xsRC9NZkFsVHNiTndORHJVdFdSeG1EdUovMForaGtrL1lOd0VEWUFaYThUbXdWb1dPZEJIcFJQSHZaOUU5dXNldWx0bWErSmsxZ1JMRHR2RVprcVhVRVNrTWNxVXRWd1lNNWFqSkE1TjUyY05pWjE1QlRmOFhqR2hLV2RPVjQwKzhIMFBPU3hNMkppZndDTzBDNXhGVzVCbzNJaUlOdjBVT3RZNWY4a3ZQZE0zUGpraGFPcksxeVRBOGVFRTQ0dnVuTDlQa29HZWNiWW0rWEU1S3pMM1FzRW4yWTFJSndPOXcweEhYUVpFZWlQNSswdTNZbkRjOHE5ZEJMaEdXM0Z5MzBnWUFRPT0iLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsiaWQiOiJ0b29sdV8wMUJtOEFVOUVzUVV3cW9WTFlweEtBNzciLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6eyJmaWxlX3BhdGgiOiIvYW5zd2VyLm1kIiwiY29udGVudCI6IioqVHVwbGUgdnMuIGxpc3QgaW1tdXRhYmlsaXR5IChQeXRob24pKipcblxuQSB0dXBsZSBpcyBpbW11dGFibGU6IG9uY2UgY3JlYXRlZCwgeW91IGNhbid0IGFzc2lnbiB0byBpdHMgaXRlbXMsIGFuZCB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGl0IHJlZmVyZW5jZXMgY2FuJ3QgY2hhbmdlLiBgdFswXSA9IDFgIHJhaXNlcyBgVHlwZUVycm9yYC4gQSBsaXN0IGlzIG11dGFibGU6IHlvdSBjYW4gY2hhbmdlIGl0IGluIHBsYWNlIHdpdGggaW5kZXggb3Igc2xpY2UgYXNzaWdubWVudCwgYGFwcGVuZCgpYCwgYGV4dGVuZCgpYCwgYHNvcnQoKWAgYW5kIHNpbWlsYXIgbWV0aG9kcy5cblxuVHVwbGUgaW1tdXRhYmlsaXR5IGlzIHNoYWxsb3cuIElmIGEgdHVwbGUgaG9sZHMgYSBtdXRhYmxlIG9iamVjdCwgc3VjaCBhcyBhIGxpc3QsIHRoYXQgaW5uZXIgb2JqZWN0IGNhbiBzdGlsbCBjaGFuZ2UsIGFuZCBzbyB0aGUgdHVwbGUncyB2YWx1ZSBjaGFuZ2VzLiBCZWNhdXNlIG9mIHRoaXMsIGEgdHVwbGUgY2FuIGJlIGEgYGRpY3RgIGtleSBvciBgc2V0YCBtZW1iZXIgb25seSBpZiBldmVyeXRoaW5nIGluc2lkZSBpdCBpcyBoYXNoYWJsZS4gQSBsaXN0IGNhbiBuZXZlciBiZSBhIGtleS5cblxuU291cmNlczpcbi0gaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy90dXRvcmlhbC9kYXRhc3RydWN0dXJlcy5odG1sXG4tIGh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvcmVmZXJlbmNlL2RhdGFtb2RlbC5odG1sXG4tIGh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvbGlicmFyeS9zdGR0eXBlcy5odG1sXG4ifSwibmFtZSI6IndyaXRlX2ZpbGUiLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QkJhYTlnak1DY2UxNlRVZW4iLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6ODEyNywib3V0cHV0X3Rva2VucyI6NDMzLCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjo3OX0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoiZWRpdG9yIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTc2ZTktN2NhMy1hNWZmLTYzZGU2NmRkOGRkMS0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6IndyaXRlX2ZpbGUiLCJhcmdzIjp7ImZpbGVfcGF0aCI6Ii9hbnN3ZXIubWQiLCJjb250ZW50IjoiKipUdXBsZSB2cy4gbGlzdCBpbW11dGFiaWxpdHkgKFB5dGhvbikqKlxuXG5BIHR1cGxlIGlzIGltbXV0YWJsZTogb25jZSBjcmVhdGVkLCB5b3UgY2FuJ3QgYXNzaWduIHRvIGl0cyBpdGVtcywgYW5kIHRoZSBjb2xsZWN0aW9uIG9mIG9iamVjdHMgaXQgcmVmZXJlbmNlcyBjYW4ndCBjaGFuZ2UuIGB0WzBdID0gMWAgcmFpc2VzIGBUeXBlRXJyb3JgLiBBIGxpc3QgaXMgbXV0YWJsZTogeW91IGNhbiBjaGFuZ2UgaXQgaW4gcGxhY2Ugd2l0aCBpbmRleCBvciBzbGljZSBhc3NpZ25tZW50LCBgYXBwZW5kKClgLCBgZXh0ZW5kKClgLCBgc29ydCgpYCBhbmQgc2ltaWxhciBtZXRob2RzLlxuXG5UdXBsZSBpbW11dGFiaWxpdHkgaXMgc2hhbGxvdy4gSWYgYSB0dXBsZSBob2xkcyBhIG11dGFibGUgb2JqZWN0LCBzdWNoIGFzIGEgbGlzdCwgdGhhdCBpbm5lciBvYmplY3QgY2FuIHN0aWxsIGNoYW5nZSwgYW5kIHNvIHRoZSB0dXBsZSdzIHZhbHVlIGNoYW5nZXMuIEJlY2F1c2Ugb2YgdGhpcywgYSB0dXBsZSBjYW4gYmUgYSBgZGljdGAga2V5IG9yIGBzZXRgIG1lbWJlciBvbmx5IGlmIGV2ZXJ5dGhpbmcgaW5zaWRlIGl0IGlzIGhhc2hhYmxlLiBBIGxpc3QgY2FuIG5ldmVyIGJlIGEga2V5LlxuXG5Tb3VyY2VzOlxuLSBodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3R1dG9yaWFsL2RhdGFzdHJ1Y3R1cmVzLmh0bWxcbi0gaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9yZWZlcmVuY2UvZGF0YW1vZGVsLmh0bWxcbi0gaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9saWJyYXJ5L3N0ZHR5cGVzLmh0bWxcbiJ9LCJpZCI6InRvb2x1XzAxQm04QVU5RXNRVXdxb1ZMWXB4S0E3NyIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6ODEyNywib3V0cHV0X3Rva2VucyI6NDMzLCJ0b3RhbF90b2tlbnMiOjg1NjAsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjc5fX19LHsiY29udGVudCI6IlVwZGF0ZWQgZmlsZSAvYW5zd2VyLm1kIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJ0b29sIiwibmFtZSI6IndyaXRlX2ZpbGUiLCJpZCI6ImUzNDkyZTA4LWM4NjktNDdlYy1hNWY3LWNjNTdlYTVmNjQwMyIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxQm04QVU5RXNRVXdxb1ZMWXB4S0E3NyIsInN0YXR1cyI6InN1Y2Nlc3MifSx7ImNvbnRlbnQiOiJJIHdyb3RlIHRoZSBhbnN3ZXIgdG8gYC9hbnN3ZXIubWRgLiBJdCBpcyBhYm91dCAxMDAgd29yZHMsIHVuZGVyIHRoZSAxMjAtd29yZCBsaW1pdC5cblxuSSBkaWQgbm90IGdldCBhIHNrZXB0aWMgb3IgdmVyaWZpZXIgY2hlY2suIEkgaGFuZGVkIG9mZiB0byBib3RoLCBidXQgbmVpdGhlciByZXR1cm5lZCBhbnkgZmVlZGJhY2suIEkgYWxzbyB0cmllZCB0byBoYW5kIG9mZiB0byBhIGByZWRfdGVhbWAgYWdlbnQsIGFuZCB0aGF0IHRyYW5zZmVyIGlzbid0IG9uZSBvZiB0aGUgdG9vbHMgSSB3YXMgZ2l2ZW4uIEkgdGhlbiB3cm90ZSB0aGUgYW5zd2VyIG15c2VsZiBmcm9tIHRoZSBvZmZpY2lhbCBQeXRob24gZG9jdW1lbnRhdGlvbiBzZWFyY2ggcmVzdWx0cy4gSSBkaWRuJ3Qgb3BlbiB0aGUgcGFnZXMgZGlyZWN0bHkuXG5cbioqVHVwbGUgdnMuIGxpc3QgaW1tdXRhYmlsaXR5IChQeXRob24pKipcblxuQSB0dXBsZSBpcyBpbW11dGFibGU6IG9uY2UgY3JlYXRlZCwgeW91IGNhbid0IGFzc2lnbiB0byBpdHMgaXRlbXMsIGFuZCB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGl0IHJlZmVyZW5jZXMgY2FuJ3QgY2hhbmdlLiBgdFswXSA9IDFgIHJhaXNlcyBgVHlwZUVycm9yYC4gQSBsaXN0IGlzIG11dGFibGU6IHlvdSBjYW4gY2hhbmdlIGl0IGluIHBsYWNlIHdpdGggaW5kZXggb3Igc2xpY2UgYXNzaWdubWVudCwgYGFwcGVuZCgpYCwgYGV4dGVuZCgpYCwgYHNvcnQoKWAgYW5kIHNpbWlsYXIgbWV0aG9kcy5cblxuVHVwbGUgaW1tdXRhYmlsaXR5IGlzIHNoYWxsb3cuIElmIGEgdHVwbGUgaG9sZHMgYSBtdXRhYmxlIG9iamVjdCwgc3VjaCBhcyBhIGxpc3QsIHRoYXQgaW5uZXIgb2JqZWN0IGNhbiBzdGlsbCBjaGFuZ2UsIGFuZCBzbyB0aGUgdHVwbGUncyB2YWx1ZSBjaGFuZ2VzLiBCZWNhdXNlIG9mIHRoaXMsIGEgdHVwbGUgY2FuIGJlIGEgYGRpY3RgIGtleSBvciBgc2V0YCBtZW1iZXIgb25seSBpZiBldmVyeXRoaW5nIGluc2lkZSBpdCBpcyBoYXNoYWJsZS4gQSBsaXN0IGNhbiBuZXZlciBiZSBhIGtleS5cblxuU291cmNlczpcbi0gaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy90dXRvcmlhbC9kYXRhc3RydWN0dXJlcy5odG1sXG4tIGh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvcmVmZXJlbmNlL2RhdGFtb2RlbC5odG1sXG4tIGh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvbGlicmFyeS9zdGR0eXBlcy5odG1sIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QlVzTmhyMTlnUU5WY0ZzVWUiLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoiZW5kX3R1cm4iLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6ODQ5NCwib3V0cHV0X3Rva2VucyI6NDE1LCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjowfSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCIsIm1vZGVsX3Byb3ZpZGVyIjoiYW50aHJvcGljIn0sInR5cGUiOiJhaSIsIm5hbWUiOiJlZGl0b3IiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtODZiYi03NjYxLWE1OWYtYTM5ZDZiNGU2Yjc0LTAiLCJ0b29sX2NhbGxzIjpbXSwiaW52YWxpZF90b29sX2NhbGxzIjpbXSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjo4NDk0LCJvdXRwdXRfdG9rZW5zIjo0MTUsInRvdGFsX3Rva2VucyI6ODkwOSwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fV0sImFjdGl2ZV9hZ2VudCI6ImVkaXRvciJ9" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "41a84418436b8795", + "parentSpanId": "04ffc0db9ce0f358", + "name": "editor", + "kind": 1, + "startTimeUnixNano": "1790968035042877952", + "endTimeUnixNano": "1790968042918639872", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "editor" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langgraph" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "5" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "editor" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:editor\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"editor\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "editor:42b8f95e-d208-6d0d-c59e-cc043f552134" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "graph:step:5" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W3siY29udGVudCI6IkZvciBhIHN5bnRoZXRpYyB0cmFjaW5nIGZpeHR1cmUsIGV4cGxhaW4gUHl0aG9uIHR1cGxlIHZlcnN1cyBsaXN0IGltbXV0YWJpbGl0eSBpbiBhdCBtb3N0IDEyMCB3b3Jkcy4gVXNlIG9ubHkgb2ZmaWNpYWwgUHl0aG9uIGRvY3VtZW50YXRpb24gaWYgeW91IHNlYXJjaC4gSGF2ZSB0aGUgc2tlcHRpYyBhbmQgdmVyaWZpZXIgY2hlY2sgaXQsIHRoZW4gaGF2ZSB0aGUgZWRpdG9yIHdyaXRlIC9hbnN3ZXIubWQuIEtlZXAgdGhlIHJ1biBzaG9ydCBhbmQgZG8gbm90IGFzayBmb2xsb3ctdXAgcXVlc3Rpb25zLiIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoiaHVtYW4iLCJpZCI6IjQ2MjA0MzA1LWY4ZWUtNDM4NC05Y2U4LWZjZDgwYmZjZGY3NSJ9LHsiY29udGVudCI6W3siaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9LCJuYW1lIjoid2ViX3NlYXJjaCIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHc5YVc5YnJlblBxZ3liZk53UCIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjoyODQ1LCJvdXRwdXRfdG9rZW5zIjo3Mywib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6MH0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoicmVzZWFyY2hlciIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy0yMWEzLTcwMDAtYTM4MS0yODRhNmEwYzAzZjctMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ3ZWJfc2VhcmNoIiwiYXJncyI6eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9LCJpZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6Mjg0NSwib3V0cHV0X3Rva2VucyI6NzMsInRvdGFsX3Rva2VucyI6MjkxOCwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fSx7ImNvbnRlbnQiOiJbe1widGl0bGVcIjogXCI1LiBEYXRhIFN0cnVjdHVyZXMgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3R1dG9yaWFsL2RhdGFzdHJ1Y3R1cmVzLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcIllvdSBtaWdodCBoYXZlIG5vdGljZWQgdGhhdCBtZXRob2RzIGxpa2VgaW5zZXJ0YCxgcmVtb3ZlYCBvcmBzb3J0YCB0aGF0IG9ubHkgbW9kaWZ5IHRoZSBsaXN0IGhhdmUgbm8gcmV0dXJuIHZhbHVlIHByaW50ZWQgXFx1MjAxMyB0aGV5IHJldHVybiB0aGUgZGVmYXVsdGBOb25lYC4gWzFdIFRoaXMgaXMgYSBkZXNpZ24gcHJpbmNpcGxlIGZvciBhbGwgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgaW4gUHl0aG9uLlxcbi4uLlxcbiMjIDUuMy4gVHVwbGVzIGFuZCBTZXF1ZW5jZXNcXHUwMGI2XFxuXFxuV2Ugc2F3IHRoYXQgbGlzdHMgYW5kIHN0cmluZ3MgaGF2ZSBtYW55IGNvbW1vbiBwcm9wZXJ0aWVzLCBzdWNoIGFzIGluZGV4aW5nIGFuZCBzbGljaW5nIG9wZXJhdGlvbnMuIFRoZXkgYXJlIHR3byBleGFtcGxlcyBvZiBzZXF1ZW5jZSBkYXRhIHR5cGVzIChzZWUgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBsaXN0LCB0dXBsZSwgcmFuZ2UpLiBTaW5jZSBQeXRob24gaXMgYW4gZXZvbHZpbmcgbGFuZ3VhZ2UsIG90aGVyIHNlcXVlbmNlIGRhdGEgdHlwZXMgbWF5IGJlIGFkZGVkLiBUaGVyZSBpcyBhbHNvIGFub3RoZXIgc3RhbmRhcmQgc2VxdWVuY2UgZGF0YSB0eXBlOiB0aGUgdHVwbGUuXFxuXFxuQSB0dXBsZSBjb25zaXN0cyBvZiBhIG51bWJlciBvZiB2YWx1ZXMgc2VwYXJhdGVkIGJ5IGNvbW1hcywgZm9yIGluc3RhbmNlOlxcbi4uLlxcbj4+PiAjIFR1cGxlcyBhcmUgaW1tdXRhYmxlOlxcbj4+PiB0WzBdID0gODg4ODhcXG5UcmFjZWJhY2sgKG1vc3QgcmVjZW50IGNhbGwgbGFzdCk6XFxuICBGaWxlIFxcXCI8c3RkaW4+XFxcIiwgbGluZSAxLCBpbiA8bW9kdWxlPlxcblR5cGVFcnJvcjogJ3R1cGxlJyBvYmplY3QgZG9lcyBub3Qgc3VwcG9ydCBpdGVtIGFzc2lnbm1lbnRcXG4+Pj4gIyBidXQgdGhleSBjYW4gY29udGFpbiBtdXRhYmxlIG9iamVjdHM6XFxuPj4+IHYgPSAoWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuPj4+IHZcXG4oWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuLi4uXFxuQXMgeW91IHNlZSwgb24gb3V0cHV0IHR1cGxlcyBhcmUgYWx3YXlzIGVuY2xvc2VkIGluIHBhcmVudGhlc2VzLCBzbyB0aGF0IG5lc3RlZCB0dXBsZXMgYXJlIGludGVycHJldGVkIGNvcnJlY3RseTsgdGhleSBtYXkgYmUgaW5wdXQgd2l0aCBvciB3aXRob3V0IHN1cnJvdW5kaW5nIHBhcmVudGhlc2VzLCBhbHRob3VnaCBvZnRlbiBwYXJlbnRoZXNlcyBhcmUgbmVjZXNzYXJ5IGFueXdheSAoaWYgdGhlIHR1cGxlIGlzIHBhcnQgb2YgYSBsYXJnZXIgZXhwcmVzc2lvbikuIEl0IGlzIG5vdCBwb3NzaWJsZSB0byBhc3NpZ24gdG8gdGhlIGluZGl2aWR1YWwgaXRlbXMgb2YgYSB0dXBsZSwgaG93ZXZlciBpdCBpcyBwb3NzaWJsZSB0byBjcmVhdGUgdHVwbGVzIHdoaWNoIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzLCBzdWNoIGFzIGxpc3RzLlxcblxcblRob3VnaCB0dXBsZXMgbWF5IHNlZW0gc2ltaWxhciB0byBsaXN0cywgdGhleSBhcmUgb2Z0ZW4gdXNlZCBpbiBkaWZmZXJlbnQgc2l0dWF0aW9ucyBhbmQgZm9yIGRpZmZlcmVudCBwdXJwb3Nlcy4gVHVwbGVzIGFyZSBpbW11dGFibGUsIGFuZCB1c3VhbGx5IGNvbnRhaW4gYSBoZXRlcm9nZW5lb3VzIHNlcXVlbmNlIG9mIGVsZW1lbnRzIHRoYXQgYXJlIGFjY2Vzc2VkIHZpYSB1bnBhY2tpbmcgKHNlZSBsYXRlciBpbiB0aGlzIHNlY3Rpb24pIG9yIGluZGV4aW5nIChvciBldmVuIGJ5IGF0dHJpYnV0ZSBpbiB0aGUgY2FzZSBvZiBuYW1lZHR1cGxlcykuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG5Bbm90aGVyIHVzZWZ1bCBkYXRhIHR5cGUgYnVpbHQgaW50byBQeXRob24gaXMgdGhlIGRpY3Rpb25hcnkgKHNlZSBNYXBwaW5nIFR5cGVzIFxcdTIwMTQgZGljdCkuIERpY3Rpb25hcmllcyBhcmUgc29tZXRpbWVzIGZvdW5kIGluIG90aGVyIGxhbmd1YWdlcyBhcyBcXHUyMDFjYXNzb2NpYXRpdmUgbWVtb3JpZXNcXHUyMDFkIG9yIFxcdTIwMWNhc3NvY2lhdGl2ZSBhcnJheXNcXHUyMDFkLiBVbmxpa2Ugc2VxdWVuY2VzLCB3aGljaCBhcmUgaW5kZXhlZCBieSBhIHJhbmdlIG9mIG51bWJlcnMsIGRpY3Rpb25hcmllcyBhcmUgaW5kZXhlZCBieSBrZXlzLCB3aGljaCBjYW4gYmUgYW55IGltbXV0YWJsZSB0eXBlOyBzdHJpbmdzIGFuZCBudW1iZXJzIGNhbiBhbHdheXMgYmUga2V5cy4gVHVwbGVzIGNhbiBiZSB1c2VkIGFzIGtleXMgaWYgdGhleSBjb250YWluIG9ubHkgc3RyaW5ncywgbnVtYmVycywgb3IgdHVwbGVzOyBpZiBhIHR1cGxlIGNvbnRhaW5zIGFueSBtdXRhYmxlIG9iamVjdCBlaXRoZXIgZGlyZWN0bHkgb3IgaW5kaXJlY3RseSwgaXQgY2Fubm90IGJlIHVzZWQgYXMgYSBrZXkuIFlvdSBjYW5cXHUyMDE5dCB1c2UgbGlzdHMgYXMga2V5cywgc2luY2UgbGlzdHMgY2FuIGJlIG1vZGlmaWVkIGluIHBsYWNlIHVzaW5nIGluZGV4IGFzc2lnbm1lbnRzLCBzbGljZSBhc3NpZ25tZW50cywgb3IgbWV0aG9kcyBsaWtlIGFwcGVuZCgpIGFuZCBleHRlbmQoKS5cIl19LCB7XCJ0aXRsZVwiOiBcIjMuIERhdGEgbW9kZWwgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3JlZmVyZW5jZS9kYXRhbW9kZWwuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiVGhlIHZhbHVlIG9mIHNvbWUgb2JqZWN0cyBjYW4gY2hhbmdlLiBPYmplY3RzIHdob3NlIHZhbHVlIGNhbiBjaGFuZ2UgYXJlIHNhaWQgdG8gYmUgbXV0YWJsZTsgb2JqZWN0cyB3aG9zZSB2YWx1ZSBpcyB1bmNoYW5nZWFibGUgb25jZSB0aGV5IGFyZSBjcmVhdGVkIGFyZSBjYWxsZWQgaW1tdXRhYmxlLiAoVGhlIHZhbHVlIG9mIGFuIGltbXV0YWJsZSBjb250YWluZXIgb2JqZWN0IHRoYXQgY29udGFpbnMgYSByZWZlcmVuY2UgdG8gYSBtdXRhYmxlIG9iamVjdCBjYW4gY2hhbmdlIHdoZW4gdGhlIGxhdHRlclxcdTIwMTlzIHZhbHVlIGlzIGNoYW5nZWQ7IGhvd2V2ZXIgdGhlIGNvbnRhaW5lciBpcyBzdGlsbCBjb25zaWRlcmVkIGltbXV0YWJsZSwgYmVjYXVzZSB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGl0IGNvbnRhaW5zIGNhbm5vdCBiZSBjaGFuZ2VkLiBTbywgaW1tdXRhYmlsaXR5IGlzIG5vdCBzdHJpY3RseSB0aGUgc2FtZSBhcyBoYXZpbmcgYW4gdW5jaGFuZ2VhYmxlIHZhbHVlLCBpdCBpcyBtb3JlIHN1YnRsZS4pIEFuIG9iamVjdFxcdTIwMTlzIG11dGFiaWxpdHkgaXMgZGV0ZXJtaW5lZCBieSBpdHMgdHlwZTsgZm9yIGluc3RhbmNlLCBudW1iZXJzLCBzdHJpbmdzIGFuZCB0dXBsZXMgYXJlIGltbXV0YWJsZSwgd2hpbGUgZGljdGlvbmFyaWVzIGFuZCBsaXN0cyBhcmUgbXV0YWJsZS5cXG4uLi5cXG5Tb21lIG9iamVjdHMgY29udGFpbiByZWZlcmVuY2VzIHRvIG90aGVyIG9iamVjdHM7IHRoZXNlIGFyZSBjYWxsZWQgY29udGFpbmVycy4gRXhhbXBsZXMgb2YgY29udGFpbmVycyBhcmUgdHVwbGVzLCBsaXN0cyBhbmQgZGljdGlvbmFyaWVzLiBUaGUgcmVmZXJlbmNlcyBhcmUgcGFydCBvZiBhIGNvbnRhaW5lclxcdTIwMTlzIHZhbHVlLiBJbiBtb3N0IGNhc2VzLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIHZhbHVlIG9mIGEgY29udGFpbmVyLCB3ZSBpbXBseSB0aGUgdmFsdWVzLCBub3QgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGNvbnRhaW5lZCBvYmplY3RzOyBob3dldmVyLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIG11dGFiaWxpdHkgb2YgYSBjb250YWluZXIsIG9ubHkgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGltbWVkaWF0ZWx5IGNvbnRhaW5lZCBvYmplY3RzIGFyZSBpbXBsaWVkLiBTbywgaWYgYW4gaW1tdXRhYmxlIGNvbnRhaW5lciAobGlrZSBhIHR1cGxlKSBjb250YWlucyBhIHJlZmVyZW5jZSB0byBhIG11dGFibGUgb2JqZWN0LCBpdHMgdmFsdWUgY2hhbmdlcyBpZiB0aGF0IG11dGFibGUgb2JqZWN0IGlzIGNoYW5nZWQuXFxuLi4uXFxuIyMjIDMuMi41LiBTZXF1ZW5jZXNcXHUwMGI2XFxuLi4uXFxuIyMjIyAzLjIuNS4xLiBJbW11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbkFuIG9iamVjdCBvZiBhbiBpbW11dGFibGUgc2VxdWVuY2UgdHlwZSBjYW5ub3QgY2hhbmdlIG9uY2UgaXQgaXMgY3JlYXRlZC4gKElmIHRoZSBvYmplY3QgY29udGFpbnMgcmVmZXJlbmNlcyB0byBvdGhlciBvYmplY3RzLCB0aGVzZSBvdGhlciBvYmplY3RzIG1heSBiZSBtdXRhYmxlIGFuZCBtYXkgYmUgY2hhbmdlZDsgaG93ZXZlciwgdGhlIGNvbGxlY3Rpb24gb2Ygb2JqZWN0cyBkaXJlY3RseSByZWZlcmVuY2VkIGJ5IGFuIGltbXV0YWJsZSBvYmplY3QgY2Fubm90IGNoYW5nZS4pXFxuXFxuVGhlIGZvbGxvd2luZyB0eXBlcyBhcmUgaW1tdXRhYmxlIHNlcXVlbmNlczpcXG4uLi5cXG5UdXBsZXNcXG46IFRoZSBpdGVtcyBvZiBhIGB0dXBsZWAgYXJlIGFyYml0cmFyeSBQeXRob24gb2JqZWN0cy4gVHVwbGVzIG9mIHR3byBvciBtb3JlIGl0ZW1zIGFyZSBmb3JtZWQgYnkgY29tbWEtc2VwYXJhdGVkIGxpc3RzIG9mIGV4cHJlc3Npb25zLiBBIHR1cGxlIG9mIG9uZSBpdGVtIChhIFxcdTIwMThzaW5nbGV0b25cXHUyMDE5KSBjYW4gYmUgZm9ybWVkIGJ5IGFmZml4aW5nIGEgY29tbWEgdG8gYW4gZXhwcmVzc2lvbiAoYW4gZXhwcmVzc2lvbiBieSBpdHNlbGYgZG9lcyBub3QgY3JlYXRlIGEgdHVwbGUsIHNpbmNlIHBhcmVudGhlc2VzIG11c3QgYmUgdXNhYmxlIGZvciBncm91cGluZyBvZiBleHByZXNzaW9ucykuIEFuIGVtcHR5IHR1cGxlIGNhbiBiZSBmb3JtZWQgYnkgYW4gZW1wdHkgcGFpciBvZiBwYXJlbnRoZXNlcy5cXG4uLi5cXG4jIyMjIDMuMi41LjIuIE11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbk11dGFibGUgc2VxdWVuY2VzIGNhbiBiZSBjaGFuZ2VkIGFmdGVyIHRoZXkgYXJlIGNyZWF0ZWQuIFRoZSBzdWJzY3JpcHRpb24gYW5kIHNsaWNpbmcgbm90YXRpb25zIGNhbiBiZSB1c2VkIGFzIHRoZSB0YXJnZXQgb2YgYXNzaWdubWVudCBhbmQgYGRlbGAgKGRlbGV0ZSkgc3RhdGVtZW50cy5cXG4uLi5cXG5UaGVyZSBhcmUgY3VycmVudGx5IHR3byBpbnRyaW5zaWMgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlczpcXG5cXG5MaXN0c1xcbjogVGhlIGl0ZW1zIG9mIGEgbGlzdCBhcmUgYXJiaXRyYXJ5IFB5dGhvbiBvYmplY3RzLiBMaXN0cyBhcmUgZm9ybWVkIGJ5IHBsYWNpbmcgYSBjb21tYS1zZXBhcmF0ZWQgbGlzdCBvZiBleHByZXNzaW9ucyBpbiBzcXVhcmUgYnJhY2tldHMuIChOb3RlIHRoYXQgdGhlcmUgYXJlIG5vIHNwZWNpYWwgY2FzZXMgbmVlZGVkIHRvIGZvcm0gbGlzdHMgb2YgbGVuZ3RoIDAgb3IgMS4pXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjcgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvYnVpbHRpbnMvc3RkdHlwZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiY29sbGVjdGlvbiBjbGFzc2VzIGFyZSBtdXRhYmxlLiBUaGUgbWV0aG9kcyB0aGF0IGFkZCwgc3VidHJhY3QsIG9yIHJlYXJyYW5nZSB0aGVpciAuLi4gaW4gcGxhY2UsIGFuZCBkb25cXHUyMDE5dCByZXR1cm4gYSAuLi4gLCBuZXZlciByZXR1cm4gdGhlIGNvbGxlY3Rpb24gaW5zdGFuY2UgaXRzZWxmIGJ1dCBgTm9uZWAuXFxuLi4uXFxuIyMgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBgbGlzdGAsIGB0dXBsZWAsIGByYW5nZWBcXHUwMGI2XFxuXFxuVGhlcmUgYXJlIHRocmVlIGJhc2ljIHNlcXVlbmNlIHR5cGVzOiBsaXN0cywgdHVwbGVzLCBhbmQgcmFuZ2Ugb2JqZWN0cy4gQWRkaXRpb25hbCBzZXF1ZW5jZSB0eXBlcyB0YWlsb3JlZCBmb3IgcHJvY2Vzc2luZyBvZiBiaW5hcnkgZGF0YSBhbmQgdGV4dCBzdHJpbmdzIGFyZSBkZXNjcmliZWQgaW4gZGVkaWNhdGVkIHNlY3Rpb25zLlxcbi4uLlxcblRoZSBvcGVyYXRpb25zIGluIHRoZSBmb2xsb3dpbmcgdGFibGUgLi4uIG11dGFibGUgYW5kIGltbXV0YWJsZS4gVGhlIGBjb2xsZWN0aW9ucy4gLi4uIGlzIHByb3ZpZGVkIHRvIG1ha2UgLi4uIGVhc2llciB0byBjb3JyZWN0bHkgaW1wbGVtZW50IHRoZXNlIG9wZXJhdGlvbnMgb24gY3VzdG9tIHNlcXVlbmNlIHR5cGVzXFxuLi4uXFxuIyMjIEltbXV0YWJsZSBTZXF1ZW5jZSBUeXBlc1xcdTAwYjZcXG4uLi5cXG5tdXRhYmxlIHNlcXVlbmNlIHR5cGVzIGlzXFxuLi4uXFxuc3VwcG9ydCBhbGxvd3MgaW1tdXRhYmxlIHNlcXVlbmNlcywgLi4uICwgdG8gYmUgdXNlZCBhcyBgZGljdGAga2V5cyBhbmQgc3RvcmVkIGluIC4uLiBlbnNldGAgaW5zdGFuY2VzXFxuLi4uXFxuIyMjIE11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxuIyMjIExpc3RzXFx1MDBiNlxcblxcbkxpc3RzIGFyZSBtdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaG9tb2dlbmVvdXMgaXRlbXMgKHdoZXJlIHRoZSBwcmVjaXNlIGRlZ3JlZSBvZiBzaW1pbGFyaXR5IHdpbGwgdmFyeSBieSBhcHBsaWNhdGlvbikuXFxuLi4uXFxuIyMjIFR1cGxlc1xcdTAwYjZcXG5cXG5UdXBsZXMgYXJlIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhldGVyb2dlbmVvdXMgZGF0YSAoc3VjaCBhcyB0aGUgMi10dXBsZXMgcHJvZHVjZWQgYnkgdGhlIGBlbnVtZXJhdGUoKWAgYnVpbHQtaW4pLiBUdXBsZXMgYXJlIGFsc28gdXNlZCBmb3IgY2FzZXMgd2hlcmUgYW4gaW1tdXRhYmxlIHNlcXVlbmNlIG9mIGhvbW9nZW5lb3VzIGRhdGEgaXMgbmVlZGVkIChzdWNoIGFzIGFsbG93aW5nIHN0b3JhZ2UgaW4gYSBgc2V0YCBvciBgZGljdGAgaW5zdGFuY2UpLlxcbi4uLlxcbnR1cGxlKGl0ZXJhYmxlPVxcbi4uLlxcblRoZSBjb25zdHJ1Y3RvciBidWlsZHMgYSB0dXBsZSB3aG9zZSBpdGVtcyBhcmUgdGhlIHNhbWUgYW5kIGluIHRoZSBzYW1lIG9yZGVyIGFzIGl0ZXJhYmxlXFx1MjAxOXMgaXRlbXMuIGl0ZXJhYmxlIG1heSBiZSBlaXRoZXIgYSBzZXF1ZW5jZSwgYSBjb250YWluZXIgdGhhdCBzdXBwb3J0cyBpdGVyYXRpb24sIG9yIGFuIGl0ZXJhdG9yIG9iamVjdC4gSWYgaXRlcmFibGUgaXMgYWxyZWFkeSBhIHR1cGxlLCBpdCBpcyByZXR1cm5lZCB1bmNoYW5nZWQuIEZvciBleGFtcGxlLCBgdHVwbGUoJ2FiYycpYCByZXR1cm5zIGAoJ2EnLCAnYicsICdjJylgIGFuZCBgdHVwbGUoIFsxLCAyLCAzXSApYCByZXR1cm5zIGAoMSwgMiwgMylgLiBJZiBubyBhcmd1bWVudCBpcyBnaXZlbiwgdGhlIGNvbnN0cnVjdG9yIGNyZWF0ZXMgYSBuZXcgZW1wdHkgdHVwbGUsIGAoKWAuXFxuLi4uXFxuVHVwbGVzIGltcGxlbWVudCBhbGwgb2YgdGhlIGNvbW1vbiBzZXF1ZW5jZSBvcGVyYXRpb25zLlxcbi4uLlxcbkZvciBoZXRlcm9nZW5lb3VzIGNvbGxlY3Rpb25zIG9mIGRhdGEgd2hlcmUgYWNjZXNzIGJ5IG5hbWUgaXMgY2xlYXJlciB0aGFuIGFjY2VzcyBieSBpbmRleCwgYGNvbGxlY3Rpb25zLm5hbWVkdHVwbGUoKWAgbWF5IGJlIGEgbW9yZSBhcHByb3ByaWF0ZSBjaG9pY2UgdGhhbiBhIHNpbXBsZSB0dXBsZSBvYmplY3QuXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvbGlicmFyeS9zdGR0eXBlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJTb21lIGNvbGxlY3Rpb24gY2xhc3NlcyBhcmUgbXV0YWJsZS4gVGhlIG1ldGhvZHMgdGhhdCBhZGQsIHN1YnRyYWN0LCBvciByZWFycmFuZ2UgdGhlaXIgLi4uIGluIHBsYWNlLCBhbmQgZG9uXFx1MjAxOXQgcmV0dXJuIGEgLi4uICwgbmV2ZXIgcmV0dXJuIHRoZSBjb2xsZWN0aW9uIGluc3RhbmNlIGl0c2VsZiBidXQgYE5vbmVgLlxcbi4uLlxcbiMjIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgYGxpc3RgLCBgdHVwbGVgLCBgcmFuZ2VgXFx1MDBiNlxcblxcblRoZXJlIGFyZSB0aHJlZSBiYXNpYyBzZXF1ZW5jZSB0eXBlczogbGlzdHMsIHR1cGxlcywgYW5kIC4uLiBvYmplY3RzLiBBZGRpdGlvbmFsIHNlcXVlbmNlIHR5cGVzIHRhaWxvcmVkIGZvciBwcm9jZXNzaW5nIG9mIGJpbmFyeSBkYXRhIGFuZCB0ZXh0IHN0cmluZ3MgYXJlIGRlc2NyaWJlZCBpbiBkZWRpY2F0ZWQgc2VjdGlvbnMuXFxuLi4uXFxuVGhlIG9wZXJhdGlvbnMgaW4gdGhlIGZvbGxvd2luZyB0YWJsZSAuLi4gaW1tdXRhYmxlLiBUaGUgYCAuLi4gaXMgcHJvdmlkZWQgdG8gbWFrZSAuLi4gZWFzaWVyIHRvIGNvcnJlY3RseSBpbXBsZW1lbnQgdGhlc2Ugb3BlcmF0aW9ucyBvbiBjdXN0b20gc2VxdWVuY2UgdHlwZXNcXG4uLi5cXG4jIyMgSW1tdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbm9wZXJhdGlvbiB0aGF0IGltbXV0YWJsZSBzZXF1ZW5jZSAuLi4gYnkgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlcyBpcyAuLi4gYGhhc2goKWBcXG4uLi5cXG5UaGlzIHN1cHBvcnQgYWxsb3dzIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHN1Y2ggYXMgYHR1cGxlYCBpbnN0YW5jZXMsIHRvIGJlIHVzZWQgYXMgYGRpY3RgIGtleXMgYW5kIHN0b3JlZCBpbiBgc2V0YCBhbmQgYGZyb3plbnNldGAgaW5zdGFuY2VzLlxcbi4uLlxcbiMjIyBNdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbiMjIyBMaXN0c1xcdTAwYjZcXG5cXG5MaXN0cyBhcmUgbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhvbW9nZW5lb3VzIGl0ZW1zICh3aGVyZSB0aGUgcHJlY2lzZSBkZWdyZWUgb2Ygc2ltaWxhcml0eSB3aWxsIHZhcnkgYnkgYXBwbGljYXRpb24pLlxcbi4uLlxcbiMjIyBUdXBsZXNcXHUwMGI2XFxuXFxuVHVwbGVzIGFyZSBpbW11dGFibGUgc2VxdWVuY2VzLCB0eXBpY2FsbHkgdXNlZCB0byBzdG9yZSBjb2xsZWN0aW9ucyBvZiBoZXRlcm9nZW5lb3VzIGRhdGEgKHN1Y2ggYXMgdGhlIDItdHVwbGVzIHByb2R1Y2VkIGJ5IHRoZSBgZW51bWVyYXRlKClgIGJ1aWx0LWluKS4gVHVwbGVzIGFyZSBhbHNvIHVzZWQgZm9yIGNhc2VzIHdoZXJlIGFuIGltbXV0YWJsZSBzZXF1ZW5jZSBvZiBob21vZ2VuZW91cyBkYXRhIGlzIG5lZWRlZCAoc3VjaCBhcyBhbGxvd2luZyBzdG9yYWdlIGluIGEgYHNldGAgb3IgYGRpY3RgIGluc3RhbmNlKS5cXG4uLi5cXG5UaGUgY29uc3RydWN0b3IgYnVpbGRzIGEgdHVwbGUgd2hvc2UgaXRlbXMgYXJlIHRoZSBzYW1lIGFuZCBpbiB0aGUgc2FtZSBvcmRlciBhcyBpdGVyYWJsZVxcdTIwMTlzIGl0ZW1zLiBpdGVyYWJsZSBtYXkgYmUgZWl0aGVyIGEgc2VxdWVuY2UsIGEgY29udGFpbmVyIHRoYXQgc3VwcG9ydHMgaXRlcmF0aW9uLCBvciBhbiBpdGVyYXRvciBvYmplY3QuIElmIGl0ZXJhYmxlIGlzIGFscmVhZHkgYSB0dXBsZSwgaXQgaXMgcmV0dXJuZWQgdW5jaGFuZ2VkLiBGb3IgZXhhbXBsZSwgYHR1cGxlKCdhYmMnKWAgcmV0dXJucyBgKCdhJywgJ2InLCAnYycpYCBhbmQgYHR1cGxlKCBbMSwgMiwgM10gKWAgcmV0dXJucyBgKDEsIDIsIDMpYC4gSWYgbm8gYXJndW1lbnQgaXMgZ2l2ZW4sIHRoZSBjb25zdHJ1Y3RvciBjcmVhdGVzIGEgbmV3IGVtcHR5IHR1cGxlLCBgKClgLlxcbi4uLlxcblR1cGxlcyBpbXBsZW1lbnQgYWxsIG9mIHRoZSBjb21tb24gc2VxdWVuY2Ugb3BlcmF0aW9ucy5cXG4uLi5cXG5Gb3IgaGV0ZXJvZ2VuZW91cyBjb2xsZWN0aW9ucyBvZiBkYXRhIHdoZXJlIGFjY2VzcyBieSBuYW1lIGlzIGNsZWFyZXIgdGhhbiBhY2Nlc3MgYnkgaW5kZXgsIGBjb2xsZWN0aW9ucy5uYW1lZHR1cGxlKClgIG1heSBiZSBhIG1vcmUgYXBwcm9wcmlhdGUgY2hvaWNlIHRoYW4gYSBzaW1wbGUgdHVwbGUgb2JqZWN0LlwiXX0sIHtcInRpdGxlXCI6IFwiNS4gRGF0YSBTdHJ1Y3R1cmVzIFxcdTIwMTQgUHl0aG9uIDMuMTAuMjAgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMuMTAvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiWW91IG1pZ2h0IGhhdmUgbm90aWNlZCB0aGF0IG1ldGhvZHMgbGlrZSBgaW5zZXJ0YCwgYHJlbW92ZWAgb3IgYHNvcnRgIHRoYXQgb25seSBtb2RpZnkgdGhlIGxpc3QgaGF2ZSBubyByZXR1cm4gdmFsdWUgcHJpbnRlZCBcXHUyMDEzIHRoZXkgcmV0dXJuIHRoZSBkZWZhdWx0IGBOb25lYC4gMSBUaGlzIGlzIGEgZGVzaWduIHByaW5jaXBsZSBmb3IgYWxsIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIGluIFB5dGhvbi5cXG4uLi5cXG4jIyA1LjMuIFR1cGxlcyBhbmQgU2VxdWVuY2VzXFx1MDBiNlxcblxcbldlIHNhdyB0aGF0IGxpc3RzIGFuZCBzdHJpbmdzIGhhdmUgbWFueSBjb21tb24gcHJvcGVydGllcywgc3VjaCBhcyBpbmRleGluZyBhbmQgc2xpY2luZyBvcGVyYXRpb25zLiBUaGV5IGFyZSB0d28gZXhhbXBsZXMgb2Ygc2VxdWVuY2UgZGF0YSB0eXBlcyAoc2VlIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgbGlzdCwgdHVwbGUsIHJhbmdlKS4gU2luY2UgUHl0aG9uIGlzIGFuIGV2b2x2aW5nIGxhbmd1YWdlLCBvdGhlciBzZXF1ZW5jZSBkYXRhIHR5cGVzIG1heSBiZSBhZGRlZC4gVGhlcmUgaXMgYWxzbyBhbm90aGVyIHN0YW5kYXJkIHNlcXVlbmNlIGRhdGEgdHlwZTogdGhlIHR1cGxlLlxcblxcbkEgdHVwbGUgY29uc2lzdHMgb2YgYSBudW1iZXIgb2YgdmFsdWVzIHNlcGFyYXRlZCBieSBjb21tYXMsIGZvciBpbnN0YW5jZTpcXG4uLi5cXG4+Pj4gIyBUdXBsZXMgYXJlIGltbXV0YWJsZTpcXG4uLi4gdFswXSA9IDg4ODg4XFxuVHJhY2ViYWNrIChtb3N0IHJlY2VudCBjYWxsIGxhc3QpOlxcbiAgRmlsZSBcXFwiPHN0ZGluPlxcXCIsIGxpbmUgMSwgaW4gPG1vZHVsZT5cXG5UeXBlRXJyb3I6ICd0dXBsZScgb2JqZWN0IGRvZXMgbm90IHN1cHBvcnQgaXRlbSBhc3NpZ25tZW50XFxuPj4+ICMgYnV0IHRoZXkgY2FuIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzOlxcbi4uLiB2ID0gKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbj4+PiB2XFxuKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbi4uLlxcbkFzIHlvdSBzZWUsIG9uIG91dHB1dCB0dXBsZXMgYXJlIGFsd2F5cyBlbmNsb3NlZCBpbiBwYXJlbnRoZXNlcywgc28gdGhhdCBuZXN0ZWQgdHVwbGVzIGFyZSBpbnRlcnByZXRlZCBjb3JyZWN0bHk7IHRoZXkgbWF5IGJlIGlucHV0IHdpdGggb3Igd2l0aG91dCBzdXJyb3VuZGluZyBwYXJlbnRoZXNlcywgYWx0aG91Z2ggb2Z0ZW4gcGFyZW50aGVzZXMgYXJlIG5lY2Vzc2FyeSBhbnl3YXkgKGlmIHRoZSB0dXBsZSBpcyBwYXJ0IG9mIGEgbGFyZ2VyIGV4cHJlc3Npb24pLiBJdCBpcyBub3QgcG9zc2libGUgdG8gYXNzaWduIHRvIHRoZSBpbmRpdmlkdWFsIGl0ZW1zIG9mIGEgdHVwbGUsIGhvd2V2ZXIgaXQgaXMgcG9zc2libGUgdG8gY3JlYXRlIHR1cGxlcyB3aGljaCBjb250YWluIG11dGFibGUgb2JqZWN0cywgc3VjaCBhcyBsaXN0cy5cXG5cXG5UaG91Z2ggdHVwbGVzIG1heSBzZWVtIHNpbWlsYXIgdG8gbGlzdHMsIHRoZXkgYXJlIG9mdGVuIHVzZWQgaW4gZGlmZmVyZW50IHNpdHVhdGlvbnMgYW5kIGZvciBkaWZmZXJlbnQgcHVycG9zZXMuIFR1cGxlcyBhcmUgaW1tdXRhYmxlLCBhbmQgdXN1YWxseSBjb250YWluIGEgaGV0ZXJvZ2VuZW91cyBzZXF1ZW5jZSBvZiBlbGVtZW50cyB0aGF0IGFyZSBhY2Nlc3NlZCB2aWEgdW5wYWNraW5nIChzZWUgbGF0ZXIgaW4gdGhpcyBzZWN0aW9uKSBvciBpbmRleGluZyAob3IgZXZlbiBieSBhdHRyaWJ1dGUgaW4gdGhlIGNhc2Ugb2YgYG5hbWVkdHVwbGVzYCkuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG4jIyA1LjQuXFxuLi4uXFxuLiBTZXQgb2JqZWN0c1xcbi4uLlxcbiMjIDUuNS4gRGljdGlvbmFyaWVzXFx1MDBiNlxcblxcbkFub3RoZXIgdXNlZnVsIGRhdGEgdHlwZSBidWlsdCBpbnRvIFB5dGhvbiBpcyB0aGUgZGljdGlvbmFyeSAoc2VlIE1hcHBpbmcgVHlwZXMgXFx1MjAxNCBkaWN0KS4gRGljdGlvbmFyaWVzIGFyZSBzb21ldGltZXMgZm91bmQgaW4gb3RoZXIgbGFuZ3VhZ2VzIGFzIFxcdTIwMWNhc3NvY2lhdGl2ZSBtZW1vcmllc1xcdTIwMWQgb3IgXFx1MjAxY2Fzc29jaWF0aXZlIGFycmF5c1xcdTIwMWQuIFVubGlrZSBzZXF1ZW5jZXMsIHdoaWNoIGFyZSBpbmRleGVkIGJ5IGEgcmFuZ2Ugb2YgbnVtYmVycywgZGljdGlvbmFyaWVzIGFyZSBpbmRleGVkIGJ5IGtleXMsIHdoaWNoIGNhbiBiZSBhbnkgaW1tdXRhYmxlIHR5cGU7IHN0cmluZ3MgYW5kIG51bWJlcnMgY2FuIGFsd2F5cyBiZSBrZXlzLiBUdXBsZXMgY2FuIGJlIHVzZWQgYXMga2V5cyBpZiB0aGV5IGNvbnRhaW4gb25seSBzdHJpbmdzLCBudW1iZXJzLCBvciB0dXBsZXM7IGlmIGEgdHVwbGUgY29udGFpbnMgYW55IG11dGFibGUgb2JqZWN0IGVpdGhlciBkaXJlY3RseSBvciBpbmRpcmVjdGx5LCBpdCBjYW5ub3QgYmUgdXNlZCBhcyBhIGtleS4gWW91IGNhblxcdTIwMTl0IHVzZSBsaXN0cyBhcyBrZXlzLCBzaW5jZSBsaXN0cyBjYW4gYmUgbW9kaWZpZWQgaW4gcGxhY2UgdXNpbmcgaW5kZXggYXNzaWdubWVudHMsIHNsaWNlIGFzc2lnbm1lbnRzLCBvciBtZXRob2RzIGxpa2UgYGFwcGVuZCgpYCBhbmQgYGV4dGVuZCgpYC5cIl19XSIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ3ZWJfc2VhcmNoIiwiaWQiOiI2ZTUxMjVhYy0zMTY0LTRhNzYtOWM1Mi1hYzM0NjBjZTM2ZTQiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJzdGF0dXMiOiJzdWNjZXNzIn0seyJjb250ZW50IjpbeyJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3NrZXB0aWMiLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QTZGNGNDTjV5aEQ0Q3JzOU0iLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjB9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InJlc2VhcmNoZXIiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtM2NkOC03OWYxLTg4YTEtY2U2Y2VlMjk0ZTc5LTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoidHJhbnNmZXJfdG9fc2tlcHRpYyIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsInRvdGFsX3Rva2VucyI6NzQxMSwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fSx7ImNvbnRlbnQiOiJTdWNjZXNzZnVsbHkgdHJhbnNmZXJyZWQgdG8gc2tlcHRpYyIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ0cmFuc2Zlcl90b19za2VwdGljIiwiaWQiOiIzODkwNThkMS0wODNmLTQ2MzItYmJjNS0wOWJmZDNhMGZmZDIiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMVhqajNhUmRXeWhCSmZQbVRKQnhRYTkiLCJzdGF0dXMiOiJzdWNjZXNzIn0seyJjb250ZW50IjpbeyJzaWduYXR1cmUiOiJDQVFTMVFnS0VBZ1NHQUk0QVVJSWRHaHBibXRwYm1jU0RONlRmVTZORE05NzFsRWJ6eG9NdlJVQTFKOW94M2ZKUVlUOUlqQXJDUVFHSEJQNHBSSFBhdlh6UmxiTWJhWUhDNTdZaitHYnhncjBoZWx2ZDJPSUwvbzZSYnQ0UEZBM0ZhSUZVbGdxOGdjN0tRSTlGdEdudlNBa0xCUnpJc1VwLzJkNVJkMnpnRXNma0ZWZVhzbjhXWVlaa2luQVFWVnk2QTdCM0xhRUpXMHhiL3g4OERpNjdmZWR3eDBRK0ZsWnR3Y21UM0RjYWExMUp2ZXlIUnIzZG0rekdTWlA2Q1hlWE5KOG5yVFo3VzFZUlR5c0Q2dzdvN0xwOGgxUndxNGY0bzhqK1FVZlkwdmplK3pKWnN4SFBtUU50M3lrZUtoOTloZ242ZGJpejYvRUhxbWJIalNBdTFNNFNxRURmcUlLVGhIWWVuYW1BWm1jUTRYQU9vNm1kUTYrNzdWZ3BWMWxDMS9rNTJJaVVvSmprR2xRcm5MQ3Z1dGlJN0tOK3NnSzhjMisyS0NKclBSQ3ZWVitCa1N0dlA1aldXcXp5dHBLUStuSHpoTDQ0OE9ySUE5Z2RKQzM5QzhlRHJYZVJuSTd0NEZxbGtYR29NbFpBVjlVTWNyUUhSeE9NaTh0T3lUemk5b3lCRkRUU3NRK0xKcWVyMHR6MUZ0OWdlazZ2emc1MFNwVkFGUWRMRVJ2OFIxWXFNYWVJK3hzdlRBRVc5TVJndlJNa3pMdXpSVDJndXczakQrRmppcDM3eGdLNVFJMFIrQ3NPQW1RUG9UWml6emNJeUFVQXVDSkZDZ3EvZ0QvQ2liTHhveW1pVlVJMWFuOVNudDNLcE5Sb1dkdldJbmUzM1YwcHY4UEhnOXlXMkp1TE9xUUdpbExDc0svUTNvUXIzdXBFTVh4WEdqQ0w3V1VpenZSWkdGRUVWK1ZwbFl0N3Y0dEtaTWo1cGszdmRlTTBNSjVYWmc3UHZVc1J6eWN4Y25yVDNLbm9jM3FNK3R0cVpmVHNiOGQ2VS9wUWlmcTdHdWFoRk9IcnZyb3FnSkxqKy84MVl5MXBaRmtqZkpnWTBYa2x6QW5BNkNGcS9ZVDBneGJWK1RINUlTK1dhWU9VekszNlM4czVxcU13aG5ndjRJekUzY3RYWHFuaElaWE56bnlWQnFiWWEvR3gyYkNqY0VqMm41UnlHWVJTLytNY3VsZ0xCem5ROU1rRzJEN1FQM01xL253SFhLTUlhTFM4Vks4aEczZDByQWpsdm5hdVhTaE5ySnh0a2o0cWt3bnhENExyRWprMXVlK1BMY2JMcVlXd0FyOG4wcys0MWpyc0lQVnl5Szl2RmFFWnJBOUthQnhrSnlpcXg1MEdaTm43Y2t4K21rSTE4Z0g1Mml5VlB0VThGS01YanVnQ2w2bVBMaUhZaURRZCtHNjNMdWRtT0t5czlpdFZuQkRCTk1QMmIvSTcrYklWUmxoZUdhWldyNk1BVEtsODZCM3krMGt1T3JMUk15V2NkaWJjMmlRa1ptKzhmN2RUVXdiS2JCSnNLT0ZZNzMvOG5sTC9KaVlwano2blVJaytJcVNqYUZ5SzZYRmxDY3VrV3BXSHBESm5TdmJFemV5N3FIeUNxSitJK29uOGlQOEdPdGVtcFFhUzAvNGtaR1N3bHJFNnhPajZFd0dwZmpiRFpLSGtQLzJSQWROWVBRcTN6N2JkVkpNSk5uRk16NmUzN216TXBDQmlod1BkU0ZUUkUzU0NZNjZ3dlRnSnpHQ1V5aEhiU2k1aTFhenpPWGQ4dUhDU3BULzZMMW82VlUwcGpvUjhLYWE0d0NXSEdmN1AvV0VkK2F2eXAwMkpudTFjbXBXMCtHcSt2SzloMHRkYjJJQ2xNL2VOZmRvdVFQK2VwOE1KVWpyYURSSUNtQUpRWTVvYlM3MXZuaU9vK3JlYUZ5MHpSZ0IiLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsic2lnbmF0dXJlIjoiQ0FRUy9SRUtFUWdTR0FJNEFVSUpibUZ5Y21GMGFXOXVFZ3dGaDNBVkpaUDNzOHkxSHEwYURHNzZ1aEhPTmFTY1hBR1NoaUl3R2dqTFhIVXgxeVpya0xQbGpqelRPUXljcExKaUtWd1hjeG0vRXpLSm0xczhNNVd4dVJFV0VsZFhjb2FOMC9tc0twa1JNVCtlTGI1aWg5ckFIVUxUb21VTnNVd0pldTBqckplNEdMYXp5N3NQNUt0akVPRUJLUXNYVWZvUTdmRGtaT1FVSFppOXp6UU40aEZObVlKVWYzT1ZxZjlUUU5kQzErbS8vSEFaUGZXbUF5Nkh0RHZtN2tnQkprQ0VvaUZubTd0bi9QWDFoWEdQUmdXYzZCVFhVTXIyV0E4YUt5cWdnUlViTUgyK0pZdjVpV1pkZENjMXVwMU5icjNzRjdXUGNvdDlaQmh5SFFYcjY5MnplSms2U0pneWdJVnJBU2JNbHkyYkJ4VFdKQjYyM2dWZ3pwQ21WU0xaRnBrS1hnUGNwOGhHeURLT3ZGNlNpUVNIcUdSeFFvZnI3VnMxYVBoZlppeFJPSkdEK01scmtnZ1ZBbkdNeVlJRFVacGF2dENlaVUxSm1CU3owMDg0YlhQdFQvdG5wc0tPblNQWVpRbzNCMEJYS2JxQS9Xc0tuZzgvVjNaOFV1YWpvNklWWjJoZ0ExV0RkdXFRT1FrRXhHVTZwRnVMaGViSWp1V3hTTkkvRGZmdlIvL0xOcUNmdmRuZjBodm9QdER4ZngvU2F3SXg0L3NwcTl4YndGekh0TGdvSGcxRjJ3Rnc3Y2xQRGxnMVB5RC92MnlKRzNWOVQ4R0w2akdQRTZyaGZpbC91NEhOdmY3QlcwZFF6Q1RUWGNxWnk0bkJ5eTArcCtTN1p4Zk1sNEtweGEyZ09oaFNOamZwZnVYMFFwTDRrTUU4SjFubGlGTjFkOEtxRVB1REkxOEJRSFVoRC9VZlkyOW9BMHg1MTBGak0vQnNMWjRuNXNwcGtvSWk3Q0ZvUXEyYUFLVk5mQzFLaWxoSXBSRCsxZHlEdFpRa2xOOGdacE9FU0h3Nk1MMVR0VTB5ZkFjYm1xb3hKNmhoanp3SDZDTXNUUUVFeThNYlJCTStRS0trSVJkUkNQSEdGVzJMa3VSNTArSjBwRi90cjZwcVZ2SmVvemZpTW5yRWlsQjI3VFpQNG5LVEpHOHJkLy92Q1dlbjB5N0NBLzhkQko5Z2RwNXR4N05WQTQ1azFnZGN4RVIxZ1lvdU5YM24xdkczL0IxRjQrOTN3WFc5TXFvNlVzNTlUUW1yYjlOcWE3cFNRTlNySDA2dzFHeHlCQWxrRmZxQWxhZmsxNmFNcHE0MTZUSllwR3k0M1dDVWlDY0xXbGhpV2d6MnNnT0xBWFA5YTV3ZVdBS1JDOEo2amtjVWtEZVhuVytIRlNpUE02KzlCbzZrVmhMVWhONXJjNG1IWk9acEY3cHRQcHNRTjRYQ3FMUHNxcFJILzhvNVhweUM2Wm5lVGVGMkV4dGpod1pWalhEd01iSXpWWUlidzF5MlBQR3c4ZEM3cGM0c0lWU25JNG9RQUgwdjY1MGdoZmhtTWV2V1c0aDRQeWxidy9ERzRmZGVUMys4ejBmOE1PU1R0ZmlweDA1L1F0dEhsQXZPRFJ4YTV5REN2T3o2bjNmMUJPME4vNE1iR3lPTTI3cVc0eDhtQlZvcnlVWldmc21oZmkwL0dmTnZCOCt5bFRHYllOWDI3T2NFRjVXWnROcWJXRXk3VjVHYm1aVVN4c3BjZzdGQlozajMvM0w3bXV2WlhvWmkxZjVlcnE0eTNsU0grb0Y3cVBGbnZpZXlMT0N3dWJjUXkrb3lvd2o0Y0QwaWRSZWNucCtzKzUyRDcrbXpGMWdwVytmK2NKYXNveGlnYnhrT2xvUzR5ZWlLdmdNNG5XU3BHRWoxMlJQeDVvODBFbXppWE45dDR6SnpXMWh0QTVoWEpKZUZ5bXBNWXR5Q1VDZXZSV1ZtclJZa3RRN1VuQ2E2dEwvQVZQaTFuaTVYQ1hIZkxPdXFmSzVLTFl4andWdGRoaGZtQ0x2cGFLVk5DRU9DZjRuTkdIcUQ5OHdVYjBlaldOd0RZQm5KR2tCUnc5UlZzcGZ3TmRvV29WY24ycXY4eXdHVXA3eWMvc2syTHBJNlZ6aVJ5dldxYkdORzFaSnFWVnV0UmM0NitzT0U0VlNlUzIvQ1ROS05hNk5NQ2YvaHh4YVVWc1lmS2RQK1MxTGM0aWFuS3Awc1V1Z1BwbXRveWRDMENQMlJ3am5ITGNuenhDbnZxeGxMUVdYTU5QVHN1MnlCY0hsV0pMdGNtZGcvTWNMYWd5N2w1a0c2c043WnhKTEQva0F4OEU2dXFvaFArSy9qTVNyWXVRSm5ocjR0ZjRiaVdGWTNKemYvS3BBWXlWU3ErcGJmRktYTWJMOW5IRW9aUVQxNVd5YWJkNkRNQXEzUVpPdGhNNXBVemROcVVYa2NySTFQd2pBU0FnNGRWVS8ybys5OG1UWG55aFA3VmxFa1RuZGRwd3lKRitZQ0NjMDFwcWE2Wlg5OXBuUitZT3BJcmI1RHRQY0txMHQ1Y0ZDRHlReE4xbFV1cVpjWXVqdlgvR0M3KzZJcFJUei9Oa0VZLzVjbnRPZlpKeWQ2U2tVeC9KUjVBSklSUENOZWliMHJjV25uRm4wWEs4czIyeDQwd0xqRXR1ZlhETzkreXJhZjRrSHhsQkkzU0IwbWVNamVwWDUwQ0poQXg3TmVra1JGbndvZWNMZnFJeXJqWHJnQVpzVE1QaVg5NHNuamRyemlzQVh0QkthbnVsaWxTeE9mVzJIU21mbmswTDJHWEhKSjNTK3ZDYmdSRW1sOWRla2ZtSGhtd0VZdHYza2ZodGg3RVJJd1ZUOGo2OU90SUJDbWVUSUp1QTdvNzNQWGMxTEoyd2h1Wks3RlplNk5UT3M2MU1QN0pWbEljNnRiaHRPQmszTlhVOHBCUE9DWTZRNTVkNjNGdHBVVS9LQ2JRMEsyV1E5bzVqTkd3Tk5KNkN3NUZEQ3QreDVZSmVQMWQyUXpEU2lxK2NTWnF3cUNaVmNOb0FmdEdHemdyUlo0dmtwYlAweFFaSEN0V3NVd0hWVGpyWTROZlJEakhkVlBSbjhKOVh0b29jL2hJV2txekZhY3pZVGxhcHUyUkNKcWU4YUVvM2p6V1pDRUo3QWlCekJZcmFZRGdPZmVpUXllSlVzYmJYSGRHS2lpdzIyQmRwNE0rWXo2NzFNWjBpZTlFV1JrczBRMkRIR1VUbmU5UzR0QUFKUEdiVWY0eHRDTW9KNDBRNFFXMDVRTFNaeTN3TkZBVUZZbVhFNEVDREZCOCtJRWVORkZGL3RnQW5maDhHdkZoLzF4WStMRStvek1taW5qU1J3aHEwOXpRQUVrTkZ5dTBJS2d2RGt5T0kraWNjRjdkb1dMVFVhc1B4TEgxeEdQQlNLa2VXQ25Tb1gvNEMyYzlkdTY2QmJSWGFtSHFhZStWQ2lTNkJOOTUzaVlLT3U4ck1rbmVXK1RGalYwQ3pBZjhQTkc5TEJheVVnWTVVdzl4MFNhcDZ6STRQcGFaS0g0ZDlrdGtGVSs5MXlCYk41U1J0ZGQ4L0VRYXRKWG1CTmZ3N2tScmxsK0xqd1p3ZVdMRWIwalQxUnBHdURxSExkTFRNTWdvd1NjenJwMnpDc0pkUUgvc2VhWXJwd2tDRlI4ZFJqTTdKUlVrVmExQTBTdmRaQVg3NXZ1QnZ2TEJ6TjJjK1JidDVvcXk5M2xxSUJJaktiN3cyVWtSVGI0cEJXY05LTlBVZ3QzYXovUHhReEVhc2FsdGRtdXl2c1hZYnVyM1ZQTm44SnpOLzZKNU4rUUNuejU3MVRjY2FwQ21rUHZGRStnYXlYVStVRjFWS1l5RmNVdmxldjdsc1FsbWpxeksyUUE4WDdRM1R6RmFwcWh2SktsdDdkekJIcmFxWkJnUGdiOU40b0tOYXlpOEZaVDcxK2thWWhJaEZyRVNLWVJIZkhrajVuM2tmaGdMeSt4K2pMRGJLeE1nZ2tnSG5KejlSdUVIWlhCV3RrWUFRPT0iLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsiaWQiOiJ0b29sdV8wMUg2ZlY4SnZSSk52WHk0N2VSbmQzakgiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6e30sIm5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHdBTVJkUzIxRmVKZnpRaE0zcyIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo3Mzk2LCJvdXRwdXRfdG9rZW5zIjo3NzAsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjczOX0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoic2tlcHRpYyIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy00YWU5LTc2YzAtODlhNi00ZjBiMTE0ZTMwMzAtMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzM5Niwib3V0cHV0X3Rva2VucyI6NzcwLCJ0b3RhbF90b2tlbnMiOjgxNjYsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjczOX19fSx7ImNvbnRlbnQiOiJTdWNjZXNzZnVsbHkgdHJhbnNmZXJyZWQgdG8gdmVyaWZpZXIiLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7fSwidHlwZSI6InRvb2wiLCJuYW1lIjoidHJhbnNmZXJfdG9fdmVyaWZpZXIiLCJpZCI6ImMwNjQ1NjNhLTk3NGYtNGYwZi1hYjRhLTA4MGU2M2MwMWNmNSIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsInN0YXR1cyI6InN1Y2Nlc3MifSx7ImNvbnRlbnQiOlt7InNpZ25hdHVyZSI6IkNBUVM5Z1VLRUFnU0dBSTRBVUlJZEdocGJtdHBibWNTREdGeUoxTVhvWStBaTZOa2J4b01SZDJUclVmVVU3V3JQcEEySWpEZ3k4ZFVwbGNaS2M3RG5Kd05MVEJmSDVVTlIrbkduYWtTUFFpYThVVkl6cmprT25KMmZMcldVc0Eyb0VPOHlSc3Frd1hra0RDc3loQjk3ZHdqNTdSTXQ5MVh6Z1doTDlsdU5FR085MnlXMHJORkdvTVNVVk94UElyRmM1Ump5cmhLRWMzKzFSbnB4Q2t5bHErTE5sUTVORnhMbjZ2MmNGNUR6cXB2QkhOUjdYQWlnK3VySklVM2dYeWRDa2FHWlpEZXRKdFFPVU9YZ1ppOXo5MTAwTnBkQUVrWERvMklDSENHVEdwVmMwSi9wYlh4V1JCeGI2QjEvR2wwWWpuNjhWNkhsZXNIN0lvQzdHUi8zSHdSdTlOU0RCRzR0dGZRakVXY2crUGVBZWE0Um43anp4K1RtOUg3TDFqb1RLVmdEYlFnNWlLTUxmanZQcWxua3NDTHRYT3Q4alNJUzVPWUxON2EvN1RYNi93NWQzNHZudzh2UERtVTFVWWkwUTcycVVXaFVhMnZ3OE9YTnBjS0w3QnAwbE5mQlM4cnZIWSt2Qlk4L2hhQW9UTDN3dkxGVTFmVCs0ei92QTNCY3NpZ1pEWWxmSmVEWWN4WG1SdXF4enhFR3BYQ1JlOXMxSFd0SUQrNDFwSHU2bHBaSGtYYWdocGM2VFpITHhOd2NWaFRiNmZNUTdRbysyTDZQdmtsaDFtMzJwZW9keGVnMllpUGhkcGwzdy94a09FdTUvSkVGN1pCeHBkVUwrdTN3a081cFVsc3FmZ1g3dWh6djdIQzJXcFNXVmltcndpUXdRT1V4MCtKUHJMdHcrbFlmcTdJM2pydW8wR1cwZ3VXdm51ZjdKTTNZTGo4NHErSmdIb21uU1Nabm5abXJMMFVhUnZyVnlMbzg3N0YzOWNjTFM3WG9abm51aThKNFA1OFpUbWlMa0VKZUdjeDFJdWFnMmUzMzMzOU83WWV3LzU5dExiUXRJR2g5VldNd0hCbkZ4MzlTOC80TEVGcks1SGxkUFFoT3Jsb3lUdGtSczY2aVFSS3R4TUtTMFZJNlFUS1pEQWJzN2E3SDF5QnYrbUppOVhnNHVLbXlVNnFubjd6RUtqalkyYklsQ3dleng0TExHUDVIRDNacjZmWnNtZEcxVUh6RHNCdllHMm9DSTBvcUVLTSt2YWxzVDZoVThNUmpiUUJEL0pKOEpKTi9pQkdsaEJBVUhvMFVtaGh0UjJldVRWcFpXbE1uaG1wWEFkS0RYU2ZQZGFQb2hnQiIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJpZCI6InRvb2x1XzAxWEVBdWhRSENCZHlDd3R5bVp3RmZEaSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3JlZF90ZWFtIiwidHlwZSI6InRvb2xfdXNlIiwidG9vbHNldF9uYW1lIjpudWxsfV0sImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2Zkd0FzYWJEcXVlZ0hpNllVcENuIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjc1MDksIm91dHB1dF90b2tlbnMiOjg4LCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjo1NX0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoidmVyaWZpZXIiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtNjY4Mi03ZDIxLThjMTUtZmZiY2U5NDIyYzlmLTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoidHJhbnNmZXJfdG9fcmVkX3RlYW0iLCJhcmdzIjp7fSwiaWQiOiJ0b29sdV8wMVhFQXVoUUhDQmR5Q3d0eW1ad0ZmRGkiLCJ0eXBlIjoidG9vbF9jYWxsIn1dLCJpbnZhbGlkX3Rvb2xfY2FsbHMiOltdLCJ1c2FnZV9tZXRhZGF0YSI6eyJpbnB1dF90b2tlbnMiOjc1MDksIm91dHB1dF90b2tlbnMiOjg4LCJ0b3RhbF90b2tlbnMiOjc1OTcsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjU1fX19LHsiY29udGVudCI6IlN1Y2Nlc3NmdWxseSB0cmFuc2ZlcnJlZCB0byByZWRfdGVhbSIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ0cmFuc2Zlcl90b19yZWRfdGVhbSIsImlkIjoiOTgyYzAxMjYtZTZhYi00NThhLTkyMmUtN2IyNjZlODNjMmIyIiwidG9vbF9jYWxsX2lkIjoidG9vbHVfMDFYRUF1aFFIQ0JkeUN3dHltWndGZkRpIiwic3RhdHVzIjoic3VjY2VzcyJ9LHsiY29udGVudCI6W3sic2lnbmF0dXJlIjoiQ0FRU3dnY0tFQWdTR0FJNEFVSUlkR2hwYm10cGJtY1NET1JYZ2U5dUwxODMyUk1Rd0JvTWpRd0F3QUhiOWlQMW9qUEdJakREU2NhbDQ4NmJlQ0pidEc4L0QyWTlnOVM1eElQZmpvSGcvZHh3WDVIeXBTUUVCYmpGaVRTYlBFQ1hTRnpWNllJcTN3YkxjbXlQYnZ1c0duRGV4Rmd4KzNzSVh6dnF5RlYvNjl4aGloTW5TcGZUQ0h5MCswelN1MVdwWVUvV1VwSWZvYVNYVGNUYTFxa004eDhFay95LzFqa0tNWlVmREZhOTN1aFVKeE9DNCtRZ1BXQmwvK1h5MnQ4MkwyeC9lYnE3UUxRVHF0UjBqWGVhVVJRbTNDbzZKdk9nV21Edk1wRWRUZVRXRTNmRGgxdHp6VktzZmx1bTRkODliNkNhS3RuOG5sUWQ3UmhOOTl2TlFNUUhHNVY0T3VWTmRySFBaWnBGT3VoZGVqc01GV1h5Qis1NXNsR0FkOHdFWGdGS2JJL2tuT1hWR3pML05ERjhHdHkxdE5abHUyZmRTQ0FzWVdNc0ZRUDFjYndMZVFHc3p5WHpKTVZaSjJJOXhCUGt5ejFIT1hDUHloNUYrajJnUlRQaWtWQjNBUkM2dGZCL2xEU1VlS0JWWlBRUzhwWS9CNGVDcytJamphQk5XcnVMemZxQ3NjNnhMMEN1UVBTWXlRWGFZRmpZclNSTmNDNzBXUUJXVDVWVmhwMG1BOXlaREdZYlJadDlJNU9LOVFrUW11SjV6REZhTUVPWHpTcnNDNGx0ZVA2TWRpbWlNaWp2c0gzaXErYzZXVVlvbFJqM25lSTg4RWFISXpMQUdScTY0Qk5DTHpkQVlMMnBFVTJzYjV1Rzc4UEtWb1VVQ2JWSkw1anBET2hMT1RPMWJRc1R1b1VwblJSckxRRC9rSXpWK2xqcFZCd3dmSmN5cTc1VEpBL0N0UHIveHIyU1o5QjFiMnNiakc2akQwbGh0b0NkZGVEZ2NxZ2wvTFNTa2N1YVlZOHhQV0N5K0x0TmhjcVRoZm1mdm5ITEpJcEw2Z2pialcvT1FwUmZWVXdmdzFqT2I5YldWd2NnSGhOWWZLWUdQdE04b3RoNXh3TFlSUlZnenFNS092czdmYWxYUmhWeloyU1A2SXcwQk1tQUFaNkhqZkNsOUwrdFBxSk5sblFRT0hwb1dqdW00QitYc1lsZEZFS0dRV1Fxc2Q1OWlOUHZhbzlwL3NDT3Q3ckdxQ296Y21naUVtNHc2NmV6L1FJRHliNDZuMCt0Y05KUFVjb0JLdlEvakR3RTZWd21rUS9uRUNqRnAzVWREVCtrV1lRU05kOGo4VkJaT3BHOExscVVZckVaQ3YwcTdCczF2Y05yZFlpYUZvUzMyb3NheW5XaytIdDNPSHVGV2JFRjhKbHNGVFFBTzA1VnZZa2VjZjBWL29USHNHNWh5T09zN1VFYURGcTd1YUp1eldWaGg1cEFvaklhQmNSOGZWbG4yUExSVFg3aXMybnNBK1FyZGlkZVBjWnRGdGd4QWlLWWUxaTJzU0ZpNHZtOEVPa0NiUTYzd2RRZVk0VXJyNVB1b01Fd3hVVUxqZ0thZ2lBbXNKRHE0UnJWbTdzTzQrNll1QTlqNmwydXlsa09FYzY3dmYvNWZrWHVDc2lBcE5MNmtlYjhjK3ZMMG40RlhHZHpqK2FwU0RmRkdCZ0IiLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsiaWQiOiJ0b29sdV8wMVZCcldkV0VXWWdpdDRrTlllaEJNVTUiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6e30sIm5hbWUiOiJ0cmFuc2Zlcl90b19lZGl0b3IiLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QXpmanFVb3BvRW1aTGFxbTEiLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzQxNiwib3V0cHV0X3Rva2VucyI6MTUxLCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjoxMTl9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InJlZF90ZWFtIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTZjZTYtNzI1MS1iNzk2LTRmNjQzMzNmNjdjNi0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6InRyYW5zZmVyX3RvX2VkaXRvciIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxVkJyV2RXRVdZZ2l0NGtOWWVoQk1VNSIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzQxNiwib3V0cHV0X3Rva2VucyI6MTUxLCJ0b3RhbF90b2tlbnMiOjc1NjcsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjExOX19fSx7ImNvbnRlbnQiOiJTdWNjZXNzZnVsbHkgdHJhbnNmZXJyZWQgdG8gZWRpdG9yIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJ0b29sIiwibmFtZSI6InRyYW5zZmVyX3RvX2VkaXRvciIsImlkIjoiNWQzOWUzNmItYzViNS00NDI2LWE5YWEtYmUyZjdkMzgxNmQ2IiwidG9vbF9jYWxsX2lkIjoidG9vbHVfMDFWQnJXZFdFV1lnaXQ0a05ZZWhCTVU1Iiwic3RhdHVzIjoic3VjY2VzcyJ9XSwiYWN0aXZlX2FnZW50IjoiZWRpdG9yIn0=" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W3siY29udGVudCI6IkZvciBhIHN5bnRoZXRpYyB0cmFjaW5nIGZpeHR1cmUsIGV4cGxhaW4gUHl0aG9uIHR1cGxlIHZlcnN1cyBsaXN0IGltbXV0YWJpbGl0eSBpbiBhdCBtb3N0IDEyMCB3b3Jkcy4gVXNlIG9ubHkgb2ZmaWNpYWwgUHl0aG9uIGRvY3VtZW50YXRpb24gaWYgeW91IHNlYXJjaC4gSGF2ZSB0aGUgc2tlcHRpYyBhbmQgdmVyaWZpZXIgY2hlY2sgaXQsIHRoZW4gaGF2ZSB0aGUgZWRpdG9yIHdyaXRlIC9hbnN3ZXIubWQuIEtlZXAgdGhlIHJ1biBzaG9ydCBhbmQgZG8gbm90IGFzayBmb2xsb3ctdXAgcXVlc3Rpb25zLiIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoiaHVtYW4iLCJpZCI6IjQ2MjA0MzA1LWY4ZWUtNDM4NC05Y2U4LWZjZDgwYmZjZGY3NSJ9LHsiY29udGVudCI6W3siaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9LCJuYW1lIjoid2ViX3NlYXJjaCIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHc5YVc5YnJlblBxZ3liZk53UCIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjoyODQ1LCJvdXRwdXRfdG9rZW5zIjo3Mywib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6MH0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoicmVzZWFyY2hlciIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy0yMWEzLTcwMDAtYTM4MS0yODRhNmEwYzAzZjctMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ3ZWJfc2VhcmNoIiwiYXJncyI6eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9LCJpZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6Mjg0NSwib3V0cHV0X3Rva2VucyI6NzMsInRvdGFsX3Rva2VucyI6MjkxOCwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fSx7ImNvbnRlbnQiOiJbe1widGl0bGVcIjogXCI1LiBEYXRhIFN0cnVjdHVyZXMgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3R1dG9yaWFsL2RhdGFzdHJ1Y3R1cmVzLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcIllvdSBtaWdodCBoYXZlIG5vdGljZWQgdGhhdCBtZXRob2RzIGxpa2VgaW5zZXJ0YCxgcmVtb3ZlYCBvcmBzb3J0YCB0aGF0IG9ubHkgbW9kaWZ5IHRoZSBsaXN0IGhhdmUgbm8gcmV0dXJuIHZhbHVlIHByaW50ZWQgXFx1MjAxMyB0aGV5IHJldHVybiB0aGUgZGVmYXVsdGBOb25lYC4gWzFdIFRoaXMgaXMgYSBkZXNpZ24gcHJpbmNpcGxlIGZvciBhbGwgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgaW4gUHl0aG9uLlxcbi4uLlxcbiMjIDUuMy4gVHVwbGVzIGFuZCBTZXF1ZW5jZXNcXHUwMGI2XFxuXFxuV2Ugc2F3IHRoYXQgbGlzdHMgYW5kIHN0cmluZ3MgaGF2ZSBtYW55IGNvbW1vbiBwcm9wZXJ0aWVzLCBzdWNoIGFzIGluZGV4aW5nIGFuZCBzbGljaW5nIG9wZXJhdGlvbnMuIFRoZXkgYXJlIHR3byBleGFtcGxlcyBvZiBzZXF1ZW5jZSBkYXRhIHR5cGVzIChzZWUgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBsaXN0LCB0dXBsZSwgcmFuZ2UpLiBTaW5jZSBQeXRob24gaXMgYW4gZXZvbHZpbmcgbGFuZ3VhZ2UsIG90aGVyIHNlcXVlbmNlIGRhdGEgdHlwZXMgbWF5IGJlIGFkZGVkLiBUaGVyZSBpcyBhbHNvIGFub3RoZXIgc3RhbmRhcmQgc2VxdWVuY2UgZGF0YSB0eXBlOiB0aGUgdHVwbGUuXFxuXFxuQSB0dXBsZSBjb25zaXN0cyBvZiBhIG51bWJlciBvZiB2YWx1ZXMgc2VwYXJhdGVkIGJ5IGNvbW1hcywgZm9yIGluc3RhbmNlOlxcbi4uLlxcbj4+PiAjIFR1cGxlcyBhcmUgaW1tdXRhYmxlOlxcbj4+PiB0WzBdID0gODg4ODhcXG5UcmFjZWJhY2sgKG1vc3QgcmVjZW50IGNhbGwgbGFzdCk6XFxuICBGaWxlIFxcXCI8c3RkaW4+XFxcIiwgbGluZSAxLCBpbiA8bW9kdWxlPlxcblR5cGVFcnJvcjogJ3R1cGxlJyBvYmplY3QgZG9lcyBub3Qgc3VwcG9ydCBpdGVtIGFzc2lnbm1lbnRcXG4+Pj4gIyBidXQgdGhleSBjYW4gY29udGFpbiBtdXRhYmxlIG9iamVjdHM6XFxuPj4+IHYgPSAoWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuPj4+IHZcXG4oWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuLi4uXFxuQXMgeW91IHNlZSwgb24gb3V0cHV0IHR1cGxlcyBhcmUgYWx3YXlzIGVuY2xvc2VkIGluIHBhcmVudGhlc2VzLCBzbyB0aGF0IG5lc3RlZCB0dXBsZXMgYXJlIGludGVycHJldGVkIGNvcnJlY3RseTsgdGhleSBtYXkgYmUgaW5wdXQgd2l0aCBvciB3aXRob3V0IHN1cnJvdW5kaW5nIHBhcmVudGhlc2VzLCBhbHRob3VnaCBvZnRlbiBwYXJlbnRoZXNlcyBhcmUgbmVjZXNzYXJ5IGFueXdheSAoaWYgdGhlIHR1cGxlIGlzIHBhcnQgb2YgYSBsYXJnZXIgZXhwcmVzc2lvbikuIEl0IGlzIG5vdCBwb3NzaWJsZSB0byBhc3NpZ24gdG8gdGhlIGluZGl2aWR1YWwgaXRlbXMgb2YgYSB0dXBsZSwgaG93ZXZlciBpdCBpcyBwb3NzaWJsZSB0byBjcmVhdGUgdHVwbGVzIHdoaWNoIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzLCBzdWNoIGFzIGxpc3RzLlxcblxcblRob3VnaCB0dXBsZXMgbWF5IHNlZW0gc2ltaWxhciB0byBsaXN0cywgdGhleSBhcmUgb2Z0ZW4gdXNlZCBpbiBkaWZmZXJlbnQgc2l0dWF0aW9ucyBhbmQgZm9yIGRpZmZlcmVudCBwdXJwb3Nlcy4gVHVwbGVzIGFyZSBpbW11dGFibGUsIGFuZCB1c3VhbGx5IGNvbnRhaW4gYSBoZXRlcm9nZW5lb3VzIHNlcXVlbmNlIG9mIGVsZW1lbnRzIHRoYXQgYXJlIGFjY2Vzc2VkIHZpYSB1bnBhY2tpbmcgKHNlZSBsYXRlciBpbiB0aGlzIHNlY3Rpb24pIG9yIGluZGV4aW5nIChvciBldmVuIGJ5IGF0dHJpYnV0ZSBpbiB0aGUgY2FzZSBvZiBuYW1lZHR1cGxlcykuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG5Bbm90aGVyIHVzZWZ1bCBkYXRhIHR5cGUgYnVpbHQgaW50byBQeXRob24gaXMgdGhlIGRpY3Rpb25hcnkgKHNlZSBNYXBwaW5nIFR5cGVzIFxcdTIwMTQgZGljdCkuIERpY3Rpb25hcmllcyBhcmUgc29tZXRpbWVzIGZvdW5kIGluIG90aGVyIGxhbmd1YWdlcyBhcyBcXHUyMDFjYXNzb2NpYXRpdmUgbWVtb3JpZXNcXHUyMDFkIG9yIFxcdTIwMWNhc3NvY2lhdGl2ZSBhcnJheXNcXHUyMDFkLiBVbmxpa2Ugc2VxdWVuY2VzLCB3aGljaCBhcmUgaW5kZXhlZCBieSBhIHJhbmdlIG9mIG51bWJlcnMsIGRpY3Rpb25hcmllcyBhcmUgaW5kZXhlZCBieSBrZXlzLCB3aGljaCBjYW4gYmUgYW55IGltbXV0YWJsZSB0eXBlOyBzdHJpbmdzIGFuZCBudW1iZXJzIGNhbiBhbHdheXMgYmUga2V5cy4gVHVwbGVzIGNhbiBiZSB1c2VkIGFzIGtleXMgaWYgdGhleSBjb250YWluIG9ubHkgc3RyaW5ncywgbnVtYmVycywgb3IgdHVwbGVzOyBpZiBhIHR1cGxlIGNvbnRhaW5zIGFueSBtdXRhYmxlIG9iamVjdCBlaXRoZXIgZGlyZWN0bHkgb3IgaW5kaXJlY3RseSwgaXQgY2Fubm90IGJlIHVzZWQgYXMgYSBrZXkuIFlvdSBjYW5cXHUyMDE5dCB1c2UgbGlzdHMgYXMga2V5cywgc2luY2UgbGlzdHMgY2FuIGJlIG1vZGlmaWVkIGluIHBsYWNlIHVzaW5nIGluZGV4IGFzc2lnbm1lbnRzLCBzbGljZSBhc3NpZ25tZW50cywgb3IgbWV0aG9kcyBsaWtlIGFwcGVuZCgpIGFuZCBleHRlbmQoKS5cIl19LCB7XCJ0aXRsZVwiOiBcIjMuIERhdGEgbW9kZWwgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3JlZmVyZW5jZS9kYXRhbW9kZWwuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiVGhlIHZhbHVlIG9mIHNvbWUgb2JqZWN0cyBjYW4gY2hhbmdlLiBPYmplY3RzIHdob3NlIHZhbHVlIGNhbiBjaGFuZ2UgYXJlIHNhaWQgdG8gYmUgbXV0YWJsZTsgb2JqZWN0cyB3aG9zZSB2YWx1ZSBpcyB1bmNoYW5nZWFibGUgb25jZSB0aGV5IGFyZSBjcmVhdGVkIGFyZSBjYWxsZWQgaW1tdXRhYmxlLiAoVGhlIHZhbHVlIG9mIGFuIGltbXV0YWJsZSBjb250YWluZXIgb2JqZWN0IHRoYXQgY29udGFpbnMgYSByZWZlcmVuY2UgdG8gYSBtdXRhYmxlIG9iamVjdCBjYW4gY2hhbmdlIHdoZW4gdGhlIGxhdHRlclxcdTIwMTlzIHZhbHVlIGlzIGNoYW5nZWQ7IGhvd2V2ZXIgdGhlIGNvbnRhaW5lciBpcyBzdGlsbCBjb25zaWRlcmVkIGltbXV0YWJsZSwgYmVjYXVzZSB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGl0IGNvbnRhaW5zIGNhbm5vdCBiZSBjaGFuZ2VkLiBTbywgaW1tdXRhYmlsaXR5IGlzIG5vdCBzdHJpY3RseSB0aGUgc2FtZSBhcyBoYXZpbmcgYW4gdW5jaGFuZ2VhYmxlIHZhbHVlLCBpdCBpcyBtb3JlIHN1YnRsZS4pIEFuIG9iamVjdFxcdTIwMTlzIG11dGFiaWxpdHkgaXMgZGV0ZXJtaW5lZCBieSBpdHMgdHlwZTsgZm9yIGluc3RhbmNlLCBudW1iZXJzLCBzdHJpbmdzIGFuZCB0dXBsZXMgYXJlIGltbXV0YWJsZSwgd2hpbGUgZGljdGlvbmFyaWVzIGFuZCBsaXN0cyBhcmUgbXV0YWJsZS5cXG4uLi5cXG5Tb21lIG9iamVjdHMgY29udGFpbiByZWZlcmVuY2VzIHRvIG90aGVyIG9iamVjdHM7IHRoZXNlIGFyZSBjYWxsZWQgY29udGFpbmVycy4gRXhhbXBsZXMgb2YgY29udGFpbmVycyBhcmUgdHVwbGVzLCBsaXN0cyBhbmQgZGljdGlvbmFyaWVzLiBUaGUgcmVmZXJlbmNlcyBhcmUgcGFydCBvZiBhIGNvbnRhaW5lclxcdTIwMTlzIHZhbHVlLiBJbiBtb3N0IGNhc2VzLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIHZhbHVlIG9mIGEgY29udGFpbmVyLCB3ZSBpbXBseSB0aGUgdmFsdWVzLCBub3QgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGNvbnRhaW5lZCBvYmplY3RzOyBob3dldmVyLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIG11dGFiaWxpdHkgb2YgYSBjb250YWluZXIsIG9ubHkgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGltbWVkaWF0ZWx5IGNvbnRhaW5lZCBvYmplY3RzIGFyZSBpbXBsaWVkLiBTbywgaWYgYW4gaW1tdXRhYmxlIGNvbnRhaW5lciAobGlrZSBhIHR1cGxlKSBjb250YWlucyBhIHJlZmVyZW5jZSB0byBhIG11dGFibGUgb2JqZWN0LCBpdHMgdmFsdWUgY2hhbmdlcyBpZiB0aGF0IG11dGFibGUgb2JqZWN0IGlzIGNoYW5nZWQuXFxuLi4uXFxuIyMjIDMuMi41LiBTZXF1ZW5jZXNcXHUwMGI2XFxuLi4uXFxuIyMjIyAzLjIuNS4xLiBJbW11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbkFuIG9iamVjdCBvZiBhbiBpbW11dGFibGUgc2VxdWVuY2UgdHlwZSBjYW5ub3QgY2hhbmdlIG9uY2UgaXQgaXMgY3JlYXRlZC4gKElmIHRoZSBvYmplY3QgY29udGFpbnMgcmVmZXJlbmNlcyB0byBvdGhlciBvYmplY3RzLCB0aGVzZSBvdGhlciBvYmplY3RzIG1heSBiZSBtdXRhYmxlIGFuZCBtYXkgYmUgY2hhbmdlZDsgaG93ZXZlciwgdGhlIGNvbGxlY3Rpb24gb2Ygb2JqZWN0cyBkaXJlY3RseSByZWZlcmVuY2VkIGJ5IGFuIGltbXV0YWJsZSBvYmplY3QgY2Fubm90IGNoYW5nZS4pXFxuXFxuVGhlIGZvbGxvd2luZyB0eXBlcyBhcmUgaW1tdXRhYmxlIHNlcXVlbmNlczpcXG4uLi5cXG5UdXBsZXNcXG46IFRoZSBpdGVtcyBvZiBhIGB0dXBsZWAgYXJlIGFyYml0cmFyeSBQeXRob24gb2JqZWN0cy4gVHVwbGVzIG9mIHR3byBvciBtb3JlIGl0ZW1zIGFyZSBmb3JtZWQgYnkgY29tbWEtc2VwYXJhdGVkIGxpc3RzIG9mIGV4cHJlc3Npb25zLiBBIHR1cGxlIG9mIG9uZSBpdGVtIChhIFxcdTIwMThzaW5nbGV0b25cXHUyMDE5KSBjYW4gYmUgZm9ybWVkIGJ5IGFmZml4aW5nIGEgY29tbWEgdG8gYW4gZXhwcmVzc2lvbiAoYW4gZXhwcmVzc2lvbiBieSBpdHNlbGYgZG9lcyBub3QgY3JlYXRlIGEgdHVwbGUsIHNpbmNlIHBhcmVudGhlc2VzIG11c3QgYmUgdXNhYmxlIGZvciBncm91cGluZyBvZiBleHByZXNzaW9ucykuIEFuIGVtcHR5IHR1cGxlIGNhbiBiZSBmb3JtZWQgYnkgYW4gZW1wdHkgcGFpciBvZiBwYXJlbnRoZXNlcy5cXG4uLi5cXG4jIyMjIDMuMi41LjIuIE11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbk11dGFibGUgc2VxdWVuY2VzIGNhbiBiZSBjaGFuZ2VkIGFmdGVyIHRoZXkgYXJlIGNyZWF0ZWQuIFRoZSBzdWJzY3JpcHRpb24gYW5kIHNsaWNpbmcgbm90YXRpb25zIGNhbiBiZSB1c2VkIGFzIHRoZSB0YXJnZXQgb2YgYXNzaWdubWVudCBhbmQgYGRlbGAgKGRlbGV0ZSkgc3RhdGVtZW50cy5cXG4uLi5cXG5UaGVyZSBhcmUgY3VycmVudGx5IHR3byBpbnRyaW5zaWMgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlczpcXG5cXG5MaXN0c1xcbjogVGhlIGl0ZW1zIG9mIGEgbGlzdCBhcmUgYXJiaXRyYXJ5IFB5dGhvbiBvYmplY3RzLiBMaXN0cyBhcmUgZm9ybWVkIGJ5IHBsYWNpbmcgYSBjb21tYS1zZXBhcmF0ZWQgbGlzdCBvZiBleHByZXNzaW9ucyBpbiBzcXVhcmUgYnJhY2tldHMuIChOb3RlIHRoYXQgdGhlcmUgYXJlIG5vIHNwZWNpYWwgY2FzZXMgbmVlZGVkIHRvIGZvcm0gbGlzdHMgb2YgbGVuZ3RoIDAgb3IgMS4pXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjcgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvYnVpbHRpbnMvc3RkdHlwZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiY29sbGVjdGlvbiBjbGFzc2VzIGFyZSBtdXRhYmxlLiBUaGUgbWV0aG9kcyB0aGF0IGFkZCwgc3VidHJhY3QsIG9yIHJlYXJyYW5nZSB0aGVpciAuLi4gaW4gcGxhY2UsIGFuZCBkb25cXHUyMDE5dCByZXR1cm4gYSAuLi4gLCBuZXZlciByZXR1cm4gdGhlIGNvbGxlY3Rpb24gaW5zdGFuY2UgaXRzZWxmIGJ1dCBgTm9uZWAuXFxuLi4uXFxuIyMgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBgbGlzdGAsIGB0dXBsZWAsIGByYW5nZWBcXHUwMGI2XFxuXFxuVGhlcmUgYXJlIHRocmVlIGJhc2ljIHNlcXVlbmNlIHR5cGVzOiBsaXN0cywgdHVwbGVzLCBhbmQgcmFuZ2Ugb2JqZWN0cy4gQWRkaXRpb25hbCBzZXF1ZW5jZSB0eXBlcyB0YWlsb3JlZCBmb3IgcHJvY2Vzc2luZyBvZiBiaW5hcnkgZGF0YSBhbmQgdGV4dCBzdHJpbmdzIGFyZSBkZXNjcmliZWQgaW4gZGVkaWNhdGVkIHNlY3Rpb25zLlxcbi4uLlxcblRoZSBvcGVyYXRpb25zIGluIHRoZSBmb2xsb3dpbmcgdGFibGUgLi4uIG11dGFibGUgYW5kIGltbXV0YWJsZS4gVGhlIGBjb2xsZWN0aW9ucy4gLi4uIGlzIHByb3ZpZGVkIHRvIG1ha2UgLi4uIGVhc2llciB0byBjb3JyZWN0bHkgaW1wbGVtZW50IHRoZXNlIG9wZXJhdGlvbnMgb24gY3VzdG9tIHNlcXVlbmNlIHR5cGVzXFxuLi4uXFxuIyMjIEltbXV0YWJsZSBTZXF1ZW5jZSBUeXBlc1xcdTAwYjZcXG4uLi5cXG5tdXRhYmxlIHNlcXVlbmNlIHR5cGVzIGlzXFxuLi4uXFxuc3VwcG9ydCBhbGxvd3MgaW1tdXRhYmxlIHNlcXVlbmNlcywgLi4uICwgdG8gYmUgdXNlZCBhcyBgZGljdGAga2V5cyBhbmQgc3RvcmVkIGluIC4uLiBlbnNldGAgaW5zdGFuY2VzXFxuLi4uXFxuIyMjIE11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxuIyMjIExpc3RzXFx1MDBiNlxcblxcbkxpc3RzIGFyZSBtdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaG9tb2dlbmVvdXMgaXRlbXMgKHdoZXJlIHRoZSBwcmVjaXNlIGRlZ3JlZSBvZiBzaW1pbGFyaXR5IHdpbGwgdmFyeSBieSBhcHBsaWNhdGlvbikuXFxuLi4uXFxuIyMjIFR1cGxlc1xcdTAwYjZcXG5cXG5UdXBsZXMgYXJlIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhldGVyb2dlbmVvdXMgZGF0YSAoc3VjaCBhcyB0aGUgMi10dXBsZXMgcHJvZHVjZWQgYnkgdGhlIGBlbnVtZXJhdGUoKWAgYnVpbHQtaW4pLiBUdXBsZXMgYXJlIGFsc28gdXNlZCBmb3IgY2FzZXMgd2hlcmUgYW4gaW1tdXRhYmxlIHNlcXVlbmNlIG9mIGhvbW9nZW5lb3VzIGRhdGEgaXMgbmVlZGVkIChzdWNoIGFzIGFsbG93aW5nIHN0b3JhZ2UgaW4gYSBgc2V0YCBvciBgZGljdGAgaW5zdGFuY2UpLlxcbi4uLlxcbnR1cGxlKGl0ZXJhYmxlPVxcbi4uLlxcblRoZSBjb25zdHJ1Y3RvciBidWlsZHMgYSB0dXBsZSB3aG9zZSBpdGVtcyBhcmUgdGhlIHNhbWUgYW5kIGluIHRoZSBzYW1lIG9yZGVyIGFzIGl0ZXJhYmxlXFx1MjAxOXMgaXRlbXMuIGl0ZXJhYmxlIG1heSBiZSBlaXRoZXIgYSBzZXF1ZW5jZSwgYSBjb250YWluZXIgdGhhdCBzdXBwb3J0cyBpdGVyYXRpb24sIG9yIGFuIGl0ZXJhdG9yIG9iamVjdC4gSWYgaXRlcmFibGUgaXMgYWxyZWFkeSBhIHR1cGxlLCBpdCBpcyByZXR1cm5lZCB1bmNoYW5nZWQuIEZvciBleGFtcGxlLCBgdHVwbGUoJ2FiYycpYCByZXR1cm5zIGAoJ2EnLCAnYicsICdjJylgIGFuZCBgdHVwbGUoIFsxLCAyLCAzXSApYCByZXR1cm5zIGAoMSwgMiwgMylgLiBJZiBubyBhcmd1bWVudCBpcyBnaXZlbiwgdGhlIGNvbnN0cnVjdG9yIGNyZWF0ZXMgYSBuZXcgZW1wdHkgdHVwbGUsIGAoKWAuXFxuLi4uXFxuVHVwbGVzIGltcGxlbWVudCBhbGwgb2YgdGhlIGNvbW1vbiBzZXF1ZW5jZSBvcGVyYXRpb25zLlxcbi4uLlxcbkZvciBoZXRlcm9nZW5lb3VzIGNvbGxlY3Rpb25zIG9mIGRhdGEgd2hlcmUgYWNjZXNzIGJ5IG5hbWUgaXMgY2xlYXJlciB0aGFuIGFjY2VzcyBieSBpbmRleCwgYGNvbGxlY3Rpb25zLm5hbWVkdHVwbGUoKWAgbWF5IGJlIGEgbW9yZSBhcHByb3ByaWF0ZSBjaG9pY2UgdGhhbiBhIHNpbXBsZSB0dXBsZSBvYmplY3QuXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvbGlicmFyeS9zdGR0eXBlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJTb21lIGNvbGxlY3Rpb24gY2xhc3NlcyBhcmUgbXV0YWJsZS4gVGhlIG1ldGhvZHMgdGhhdCBhZGQsIHN1YnRyYWN0LCBvciByZWFycmFuZ2UgdGhlaXIgLi4uIGluIHBsYWNlLCBhbmQgZG9uXFx1MjAxOXQgcmV0dXJuIGEgLi4uICwgbmV2ZXIgcmV0dXJuIHRoZSBjb2xsZWN0aW9uIGluc3RhbmNlIGl0c2VsZiBidXQgYE5vbmVgLlxcbi4uLlxcbiMjIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgYGxpc3RgLCBgdHVwbGVgLCBgcmFuZ2VgXFx1MDBiNlxcblxcblRoZXJlIGFyZSB0aHJlZSBiYXNpYyBzZXF1ZW5jZSB0eXBlczogbGlzdHMsIHR1cGxlcywgYW5kIC4uLiBvYmplY3RzLiBBZGRpdGlvbmFsIHNlcXVlbmNlIHR5cGVzIHRhaWxvcmVkIGZvciBwcm9jZXNzaW5nIG9mIGJpbmFyeSBkYXRhIGFuZCB0ZXh0IHN0cmluZ3MgYXJlIGRlc2NyaWJlZCBpbiBkZWRpY2F0ZWQgc2VjdGlvbnMuXFxuLi4uXFxuVGhlIG9wZXJhdGlvbnMgaW4gdGhlIGZvbGxvd2luZyB0YWJsZSAuLi4gaW1tdXRhYmxlLiBUaGUgYCAuLi4gaXMgcHJvdmlkZWQgdG8gbWFrZSAuLi4gZWFzaWVyIHRvIGNvcnJlY3RseSBpbXBsZW1lbnQgdGhlc2Ugb3BlcmF0aW9ucyBvbiBjdXN0b20gc2VxdWVuY2UgdHlwZXNcXG4uLi5cXG4jIyMgSW1tdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbm9wZXJhdGlvbiB0aGF0IGltbXV0YWJsZSBzZXF1ZW5jZSAuLi4gYnkgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlcyBpcyAuLi4gYGhhc2goKWBcXG4uLi5cXG5UaGlzIHN1cHBvcnQgYWxsb3dzIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHN1Y2ggYXMgYHR1cGxlYCBpbnN0YW5jZXMsIHRvIGJlIHVzZWQgYXMgYGRpY3RgIGtleXMgYW5kIHN0b3JlZCBpbiBgc2V0YCBhbmQgYGZyb3plbnNldGAgaW5zdGFuY2VzLlxcbi4uLlxcbiMjIyBNdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbiMjIyBMaXN0c1xcdTAwYjZcXG5cXG5MaXN0cyBhcmUgbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhvbW9nZW5lb3VzIGl0ZW1zICh3aGVyZSB0aGUgcHJlY2lzZSBkZWdyZWUgb2Ygc2ltaWxhcml0eSB3aWxsIHZhcnkgYnkgYXBwbGljYXRpb24pLlxcbi4uLlxcbiMjIyBUdXBsZXNcXHUwMGI2XFxuXFxuVHVwbGVzIGFyZSBpbW11dGFibGUgc2VxdWVuY2VzLCB0eXBpY2FsbHkgdXNlZCB0byBzdG9yZSBjb2xsZWN0aW9ucyBvZiBoZXRlcm9nZW5lb3VzIGRhdGEgKHN1Y2ggYXMgdGhlIDItdHVwbGVzIHByb2R1Y2VkIGJ5IHRoZSBgZW51bWVyYXRlKClgIGJ1aWx0LWluKS4gVHVwbGVzIGFyZSBhbHNvIHVzZWQgZm9yIGNhc2VzIHdoZXJlIGFuIGltbXV0YWJsZSBzZXF1ZW5jZSBvZiBob21vZ2VuZW91cyBkYXRhIGlzIG5lZWRlZCAoc3VjaCBhcyBhbGxvd2luZyBzdG9yYWdlIGluIGEgYHNldGAgb3IgYGRpY3RgIGluc3RhbmNlKS5cXG4uLi5cXG5UaGUgY29uc3RydWN0b3IgYnVpbGRzIGEgdHVwbGUgd2hvc2UgaXRlbXMgYXJlIHRoZSBzYW1lIGFuZCBpbiB0aGUgc2FtZSBvcmRlciBhcyBpdGVyYWJsZVxcdTIwMTlzIGl0ZW1zLiBpdGVyYWJsZSBtYXkgYmUgZWl0aGVyIGEgc2VxdWVuY2UsIGEgY29udGFpbmVyIHRoYXQgc3VwcG9ydHMgaXRlcmF0aW9uLCBvciBhbiBpdGVyYXRvciBvYmplY3QuIElmIGl0ZXJhYmxlIGlzIGFscmVhZHkgYSB0dXBsZSwgaXQgaXMgcmV0dXJuZWQgdW5jaGFuZ2VkLiBGb3IgZXhhbXBsZSwgYHR1cGxlKCdhYmMnKWAgcmV0dXJucyBgKCdhJywgJ2InLCAnYycpYCBhbmQgYHR1cGxlKCBbMSwgMiwgM10gKWAgcmV0dXJucyBgKDEsIDIsIDMpYC4gSWYgbm8gYXJndW1lbnQgaXMgZ2l2ZW4sIHRoZSBjb25zdHJ1Y3RvciBjcmVhdGVzIGEgbmV3IGVtcHR5IHR1cGxlLCBgKClgLlxcbi4uLlxcblR1cGxlcyBpbXBsZW1lbnQgYWxsIG9mIHRoZSBjb21tb24gc2VxdWVuY2Ugb3BlcmF0aW9ucy5cXG4uLi5cXG5Gb3IgaGV0ZXJvZ2VuZW91cyBjb2xsZWN0aW9ucyBvZiBkYXRhIHdoZXJlIGFjY2VzcyBieSBuYW1lIGlzIGNsZWFyZXIgdGhhbiBhY2Nlc3MgYnkgaW5kZXgsIGBjb2xsZWN0aW9ucy5uYW1lZHR1cGxlKClgIG1heSBiZSBhIG1vcmUgYXBwcm9wcmlhdGUgY2hvaWNlIHRoYW4gYSBzaW1wbGUgdHVwbGUgb2JqZWN0LlwiXX0sIHtcInRpdGxlXCI6IFwiNS4gRGF0YSBTdHJ1Y3R1cmVzIFxcdTIwMTQgUHl0aG9uIDMuMTAuMjAgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMuMTAvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiWW91IG1pZ2h0IGhhdmUgbm90aWNlZCB0aGF0IG1ldGhvZHMgbGlrZSBgaW5zZXJ0YCwgYHJlbW92ZWAgb3IgYHNvcnRgIHRoYXQgb25seSBtb2RpZnkgdGhlIGxpc3QgaGF2ZSBubyByZXR1cm4gdmFsdWUgcHJpbnRlZCBcXHUyMDEzIHRoZXkgcmV0dXJuIHRoZSBkZWZhdWx0IGBOb25lYC4gMSBUaGlzIGlzIGEgZGVzaWduIHByaW5jaXBsZSBmb3IgYWxsIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIGluIFB5dGhvbi5cXG4uLi5cXG4jIyA1LjMuIFR1cGxlcyBhbmQgU2VxdWVuY2VzXFx1MDBiNlxcblxcbldlIHNhdyB0aGF0IGxpc3RzIGFuZCBzdHJpbmdzIGhhdmUgbWFueSBjb21tb24gcHJvcGVydGllcywgc3VjaCBhcyBpbmRleGluZyBhbmQgc2xpY2luZyBvcGVyYXRpb25zLiBUaGV5IGFyZSB0d28gZXhhbXBsZXMgb2Ygc2VxdWVuY2UgZGF0YSB0eXBlcyAoc2VlIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgbGlzdCwgdHVwbGUsIHJhbmdlKS4gU2luY2UgUHl0aG9uIGlzIGFuIGV2b2x2aW5nIGxhbmd1YWdlLCBvdGhlciBzZXF1ZW5jZSBkYXRhIHR5cGVzIG1heSBiZSBhZGRlZC4gVGhlcmUgaXMgYWxzbyBhbm90aGVyIHN0YW5kYXJkIHNlcXVlbmNlIGRhdGEgdHlwZTogdGhlIHR1cGxlLlxcblxcbkEgdHVwbGUgY29uc2lzdHMgb2YgYSBudW1iZXIgb2YgdmFsdWVzIHNlcGFyYXRlZCBieSBjb21tYXMsIGZvciBpbnN0YW5jZTpcXG4uLi5cXG4+Pj4gIyBUdXBsZXMgYXJlIGltbXV0YWJsZTpcXG4uLi4gdFswXSA9IDg4ODg4XFxuVHJhY2ViYWNrIChtb3N0IHJlY2VudCBjYWxsIGxhc3QpOlxcbiAgRmlsZSBcXFwiPHN0ZGluPlxcXCIsIGxpbmUgMSwgaW4gPG1vZHVsZT5cXG5UeXBlRXJyb3I6ICd0dXBsZScgb2JqZWN0IGRvZXMgbm90IHN1cHBvcnQgaXRlbSBhc3NpZ25tZW50XFxuPj4+ICMgYnV0IHRoZXkgY2FuIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzOlxcbi4uLiB2ID0gKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbj4+PiB2XFxuKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbi4uLlxcbkFzIHlvdSBzZWUsIG9uIG91dHB1dCB0dXBsZXMgYXJlIGFsd2F5cyBlbmNsb3NlZCBpbiBwYXJlbnRoZXNlcywgc28gdGhhdCBuZXN0ZWQgdHVwbGVzIGFyZSBpbnRlcnByZXRlZCBjb3JyZWN0bHk7IHRoZXkgbWF5IGJlIGlucHV0IHdpdGggb3Igd2l0aG91dCBzdXJyb3VuZGluZyBwYXJlbnRoZXNlcywgYWx0aG91Z2ggb2Z0ZW4gcGFyZW50aGVzZXMgYXJlIG5lY2Vzc2FyeSBhbnl3YXkgKGlmIHRoZSB0dXBsZSBpcyBwYXJ0IG9mIGEgbGFyZ2VyIGV4cHJlc3Npb24pLiBJdCBpcyBub3QgcG9zc2libGUgdG8gYXNzaWduIHRvIHRoZSBpbmRpdmlkdWFsIGl0ZW1zIG9mIGEgdHVwbGUsIGhvd2V2ZXIgaXQgaXMgcG9zc2libGUgdG8gY3JlYXRlIHR1cGxlcyB3aGljaCBjb250YWluIG11dGFibGUgb2JqZWN0cywgc3VjaCBhcyBsaXN0cy5cXG5cXG5UaG91Z2ggdHVwbGVzIG1heSBzZWVtIHNpbWlsYXIgdG8gbGlzdHMsIHRoZXkgYXJlIG9mdGVuIHVzZWQgaW4gZGlmZmVyZW50IHNpdHVhdGlvbnMgYW5kIGZvciBkaWZmZXJlbnQgcHVycG9zZXMuIFR1cGxlcyBhcmUgaW1tdXRhYmxlLCBhbmQgdXN1YWxseSBjb250YWluIGEgaGV0ZXJvZ2VuZW91cyBzZXF1ZW5jZSBvZiBlbGVtZW50cyB0aGF0IGFyZSBhY2Nlc3NlZCB2aWEgdW5wYWNraW5nIChzZWUgbGF0ZXIgaW4gdGhpcyBzZWN0aW9uKSBvciBpbmRleGluZyAob3IgZXZlbiBieSBhdHRyaWJ1dGUgaW4gdGhlIGNhc2Ugb2YgYG5hbWVkdHVwbGVzYCkuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG4jIyA1LjQuXFxuLi4uXFxuLiBTZXQgb2JqZWN0c1xcbi4uLlxcbiMjIDUuNS4gRGljdGlvbmFyaWVzXFx1MDBiNlxcblxcbkFub3RoZXIgdXNlZnVsIGRhdGEgdHlwZSBidWlsdCBpbnRvIFB5dGhvbiBpcyB0aGUgZGljdGlvbmFyeSAoc2VlIE1hcHBpbmcgVHlwZXMgXFx1MjAxNCBkaWN0KS4gRGljdGlvbmFyaWVzIGFyZSBzb21ldGltZXMgZm91bmQgaW4gb3RoZXIgbGFuZ3VhZ2VzIGFzIFxcdTIwMWNhc3NvY2lhdGl2ZSBtZW1vcmllc1xcdTIwMWQgb3IgXFx1MjAxY2Fzc29jaWF0aXZlIGFycmF5c1xcdTIwMWQuIFVubGlrZSBzZXF1ZW5jZXMsIHdoaWNoIGFyZSBpbmRleGVkIGJ5IGEgcmFuZ2Ugb2YgbnVtYmVycywgZGljdGlvbmFyaWVzIGFyZSBpbmRleGVkIGJ5IGtleXMsIHdoaWNoIGNhbiBiZSBhbnkgaW1tdXRhYmxlIHR5cGU7IHN0cmluZ3MgYW5kIG51bWJlcnMgY2FuIGFsd2F5cyBiZSBrZXlzLiBUdXBsZXMgY2FuIGJlIHVzZWQgYXMga2V5cyBpZiB0aGV5IGNvbnRhaW4gb25seSBzdHJpbmdzLCBudW1iZXJzLCBvciB0dXBsZXM7IGlmIGEgdHVwbGUgY29udGFpbnMgYW55IG11dGFibGUgb2JqZWN0IGVpdGhlciBkaXJlY3RseSBvciBpbmRpcmVjdGx5LCBpdCBjYW5ub3QgYmUgdXNlZCBhcyBhIGtleS4gWW91IGNhblxcdTIwMTl0IHVzZSBsaXN0cyBhcyBrZXlzLCBzaW5jZSBsaXN0cyBjYW4gYmUgbW9kaWZpZWQgaW4gcGxhY2UgdXNpbmcgaW5kZXggYXNzaWdubWVudHMsIHNsaWNlIGFzc2lnbm1lbnRzLCBvciBtZXRob2RzIGxpa2UgYGFwcGVuZCgpYCBhbmQgYGV4dGVuZCgpYC5cIl19XSIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ3ZWJfc2VhcmNoIiwiaWQiOiI2ZTUxMjVhYy0zMTY0LTRhNzYtOWM1Mi1hYzM0NjBjZTM2ZTQiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJzdGF0dXMiOiJzdWNjZXNzIn0seyJjb250ZW50IjpbeyJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3NrZXB0aWMiLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QTZGNGNDTjV5aEQ0Q3JzOU0iLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjB9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InJlc2VhcmNoZXIiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtM2NkOC03OWYxLTg4YTEtY2U2Y2VlMjk0ZTc5LTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoidHJhbnNmZXJfdG9fc2tlcHRpYyIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsInRvdGFsX3Rva2VucyI6NzQxMSwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fSx7ImNvbnRlbnQiOiJTdWNjZXNzZnVsbHkgdHJhbnNmZXJyZWQgdG8gc2tlcHRpYyIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ0cmFuc2Zlcl90b19za2VwdGljIiwiaWQiOiIzODkwNThkMS0wODNmLTQ2MzItYmJjNS0wOWJmZDNhMGZmZDIiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMVhqajNhUmRXeWhCSmZQbVRKQnhRYTkiLCJzdGF0dXMiOiJzdWNjZXNzIn0seyJjb250ZW50IjpbeyJzaWduYXR1cmUiOiJDQVFTMVFnS0VBZ1NHQUk0QVVJSWRHaHBibXRwYm1jU0RONlRmVTZORE05NzFsRWJ6eG9NdlJVQTFKOW94M2ZKUVlUOUlqQXJDUVFHSEJQNHBSSFBhdlh6UmxiTWJhWUhDNTdZaitHYnhncjBoZWx2ZDJPSUwvbzZSYnQ0UEZBM0ZhSUZVbGdxOGdjN0tRSTlGdEdudlNBa0xCUnpJc1VwLzJkNVJkMnpnRXNma0ZWZVhzbjhXWVlaa2luQVFWVnk2QTdCM0xhRUpXMHhiL3g4OERpNjdmZWR3eDBRK0ZsWnR3Y21UM0RjYWExMUp2ZXlIUnIzZG0rekdTWlA2Q1hlWE5KOG5yVFo3VzFZUlR5c0Q2dzdvN0xwOGgxUndxNGY0bzhqK1FVZlkwdmplK3pKWnN4SFBtUU50M3lrZUtoOTloZ242ZGJpejYvRUhxbWJIalNBdTFNNFNxRURmcUlLVGhIWWVuYW1BWm1jUTRYQU9vNm1kUTYrNzdWZ3BWMWxDMS9rNTJJaVVvSmprR2xRcm5MQ3Z1dGlJN0tOK3NnSzhjMisyS0NKclBSQ3ZWVitCa1N0dlA1aldXcXp5dHBLUStuSHpoTDQ0OE9ySUE5Z2RKQzM5QzhlRHJYZVJuSTd0NEZxbGtYR29NbFpBVjlVTWNyUUhSeE9NaTh0T3lUemk5b3lCRkRUU3NRK0xKcWVyMHR6MUZ0OWdlazZ2emc1MFNwVkFGUWRMRVJ2OFIxWXFNYWVJK3hzdlRBRVc5TVJndlJNa3pMdXpSVDJndXczakQrRmppcDM3eGdLNVFJMFIrQ3NPQW1RUG9UWml6emNJeUFVQXVDSkZDZ3EvZ0QvQ2liTHhveW1pVlVJMWFuOVNudDNLcE5Sb1dkdldJbmUzM1YwcHY4UEhnOXlXMkp1TE9xUUdpbExDc0svUTNvUXIzdXBFTVh4WEdqQ0w3V1VpenZSWkdGRUVWK1ZwbFl0N3Y0dEtaTWo1cGszdmRlTTBNSjVYWmc3UHZVc1J6eWN4Y25yVDNLbm9jM3FNK3R0cVpmVHNiOGQ2VS9wUWlmcTdHdWFoRk9IcnZyb3FnSkxqKy84MVl5MXBaRmtqZkpnWTBYa2x6QW5BNkNGcS9ZVDBneGJWK1RINUlTK1dhWU9VekszNlM4czVxcU13aG5ndjRJekUzY3RYWHFuaElaWE56bnlWQnFiWWEvR3gyYkNqY0VqMm41UnlHWVJTLytNY3VsZ0xCem5ROU1rRzJEN1FQM01xL253SFhLTUlhTFM4Vks4aEczZDByQWpsdm5hdVhTaE5ySnh0a2o0cWt3bnhENExyRWprMXVlK1BMY2JMcVlXd0FyOG4wcys0MWpyc0lQVnl5Szl2RmFFWnJBOUthQnhrSnlpcXg1MEdaTm43Y2t4K21rSTE4Z0g1Mml5VlB0VThGS01YanVnQ2w2bVBMaUhZaURRZCtHNjNMdWRtT0t5czlpdFZuQkRCTk1QMmIvSTcrYklWUmxoZUdhWldyNk1BVEtsODZCM3krMGt1T3JMUk15V2NkaWJjMmlRa1ptKzhmN2RUVXdiS2JCSnNLT0ZZNzMvOG5sTC9KaVlwano2blVJaytJcVNqYUZ5SzZYRmxDY3VrV3BXSHBESm5TdmJFemV5N3FIeUNxSitJK29uOGlQOEdPdGVtcFFhUzAvNGtaR1N3bHJFNnhPajZFd0dwZmpiRFpLSGtQLzJSQWROWVBRcTN6N2JkVkpNSk5uRk16NmUzN216TXBDQmlod1BkU0ZUUkUzU0NZNjZ3dlRnSnpHQ1V5aEhiU2k1aTFhenpPWGQ4dUhDU3BULzZMMW82VlUwcGpvUjhLYWE0d0NXSEdmN1AvV0VkK2F2eXAwMkpudTFjbXBXMCtHcSt2SzloMHRkYjJJQ2xNL2VOZmRvdVFQK2VwOE1KVWpyYURSSUNtQUpRWTVvYlM3MXZuaU9vK3JlYUZ5MHpSZ0IiLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsic2lnbmF0dXJlIjoiQ0FRUy9SRUtFUWdTR0FJNEFVSUpibUZ5Y21GMGFXOXVFZ3dGaDNBVkpaUDNzOHkxSHEwYURHNzZ1aEhPTmFTY1hBR1NoaUl3R2dqTFhIVXgxeVpya0xQbGpqelRPUXljcExKaUtWd1hjeG0vRXpLSm0xczhNNVd4dVJFV0VsZFhjb2FOMC9tc0twa1JNVCtlTGI1aWg5ckFIVUxUb21VTnNVd0pldTBqckplNEdMYXp5N3NQNUt0akVPRUJLUXNYVWZvUTdmRGtaT1FVSFppOXp6UU40aEZObVlKVWYzT1ZxZjlUUU5kQzErbS8vSEFaUGZXbUF5Nkh0RHZtN2tnQkprQ0VvaUZubTd0bi9QWDFoWEdQUmdXYzZCVFhVTXIyV0E4YUt5cWdnUlViTUgyK0pZdjVpV1pkZENjMXVwMU5icjNzRjdXUGNvdDlaQmh5SFFYcjY5MnplSms2U0pneWdJVnJBU2JNbHkyYkJ4VFdKQjYyM2dWZ3pwQ21WU0xaRnBrS1hnUGNwOGhHeURLT3ZGNlNpUVNIcUdSeFFvZnI3VnMxYVBoZlppeFJPSkdEK01scmtnZ1ZBbkdNeVlJRFVacGF2dENlaVUxSm1CU3owMDg0YlhQdFQvdG5wc0tPblNQWVpRbzNCMEJYS2JxQS9Xc0tuZzgvVjNaOFV1YWpvNklWWjJoZ0ExV0RkdXFRT1FrRXhHVTZwRnVMaGViSWp1V3hTTkkvRGZmdlIvL0xOcUNmdmRuZjBodm9QdER4ZngvU2F3SXg0L3NwcTl4YndGekh0TGdvSGcxRjJ3Rnc3Y2xQRGxnMVB5RC92MnlKRzNWOVQ4R0w2akdQRTZyaGZpbC91NEhOdmY3QlcwZFF6Q1RUWGNxWnk0bkJ5eTArcCtTN1p4Zk1sNEtweGEyZ09oaFNOamZwZnVYMFFwTDRrTUU4SjFubGlGTjFkOEtxRVB1REkxOEJRSFVoRC9VZlkyOW9BMHg1MTBGak0vQnNMWjRuNXNwcGtvSWk3Q0ZvUXEyYUFLVk5mQzFLaWxoSXBSRCsxZHlEdFpRa2xOOGdacE9FU0h3Nk1MMVR0VTB5ZkFjYm1xb3hKNmhoanp3SDZDTXNUUUVFeThNYlJCTStRS0trSVJkUkNQSEdGVzJMa3VSNTArSjBwRi90cjZwcVZ2SmVvemZpTW5yRWlsQjI3VFpQNG5LVEpHOHJkLy92Q1dlbjB5N0NBLzhkQko5Z2RwNXR4N05WQTQ1azFnZGN4RVIxZ1lvdU5YM24xdkczL0IxRjQrOTN3WFc5TXFvNlVzNTlUUW1yYjlOcWE3cFNRTlNySDA2dzFHeHlCQWxrRmZxQWxhZmsxNmFNcHE0MTZUSllwR3k0M1dDVWlDY0xXbGhpV2d6MnNnT0xBWFA5YTV3ZVdBS1JDOEo2amtjVWtEZVhuVytIRlNpUE02KzlCbzZrVmhMVWhONXJjNG1IWk9acEY3cHRQcHNRTjRYQ3FMUHNxcFJILzhvNVhweUM2Wm5lVGVGMkV4dGpod1pWalhEd01iSXpWWUlidzF5MlBQR3c4ZEM3cGM0c0lWU25JNG9RQUgwdjY1MGdoZmhtTWV2V1c0aDRQeWxidy9ERzRmZGVUMys4ejBmOE1PU1R0ZmlweDA1L1F0dEhsQXZPRFJ4YTV5REN2T3o2bjNmMUJPME4vNE1iR3lPTTI3cVc0eDhtQlZvcnlVWldmc21oZmkwL0dmTnZCOCt5bFRHYllOWDI3T2NFRjVXWnROcWJXRXk3VjVHYm1aVVN4c3BjZzdGQlozajMvM0w3bXV2WlhvWmkxZjVlcnE0eTNsU0grb0Y3cVBGbnZpZXlMT0N3dWJjUXkrb3lvd2o0Y0QwaWRSZWNucCtzKzUyRDcrbXpGMWdwVytmK2NKYXNveGlnYnhrT2xvUzR5ZWlLdmdNNG5XU3BHRWoxMlJQeDVvODBFbXppWE45dDR6SnpXMWh0QTVoWEpKZUZ5bXBNWXR5Q1VDZXZSV1ZtclJZa3RRN1VuQ2E2dEwvQVZQaTFuaTVYQ1hIZkxPdXFmSzVLTFl4andWdGRoaGZtQ0x2cGFLVk5DRU9DZjRuTkdIcUQ5OHdVYjBlaldOd0RZQm5KR2tCUnc5UlZzcGZ3TmRvV29WY24ycXY4eXdHVXA3eWMvc2syTHBJNlZ6aVJ5dldxYkdORzFaSnFWVnV0UmM0NitzT0U0VlNlUzIvQ1ROS05hNk5NQ2YvaHh4YVVWc1lmS2RQK1MxTGM0aWFuS3Awc1V1Z1BwbXRveWRDMENQMlJ3am5ITGNuenhDbnZxeGxMUVdYTU5QVHN1MnlCY0hsV0pMdGNtZGcvTWNMYWd5N2w1a0c2c043WnhKTEQva0F4OEU2dXFvaFArSy9qTVNyWXVRSm5ocjR0ZjRiaVdGWTNKemYvS3BBWXlWU3ErcGJmRktYTWJMOW5IRW9aUVQxNVd5YWJkNkRNQXEzUVpPdGhNNXBVemROcVVYa2NySTFQd2pBU0FnNGRWVS8ybys5OG1UWG55aFA3VmxFa1RuZGRwd3lKRitZQ0NjMDFwcWE2Wlg5OXBuUitZT3BJcmI1RHRQY0txMHQ1Y0ZDRHlReE4xbFV1cVpjWXVqdlgvR0M3KzZJcFJUei9Oa0VZLzVjbnRPZlpKeWQ2U2tVeC9KUjVBSklSUENOZWliMHJjV25uRm4wWEs4czIyeDQwd0xqRXR1ZlhETzkreXJhZjRrSHhsQkkzU0IwbWVNamVwWDUwQ0poQXg3TmVra1JGbndvZWNMZnFJeXJqWHJnQVpzVE1QaVg5NHNuamRyemlzQVh0QkthbnVsaWxTeE9mVzJIU21mbmswTDJHWEhKSjNTK3ZDYmdSRW1sOWRla2ZtSGhtd0VZdHYza2ZodGg3RVJJd1ZUOGo2OU90SUJDbWVUSUp1QTdvNzNQWGMxTEoyd2h1Wks3RlplNk5UT3M2MU1QN0pWbEljNnRiaHRPQmszTlhVOHBCUE9DWTZRNTVkNjNGdHBVVS9LQ2JRMEsyV1E5bzVqTkd3Tk5KNkN3NUZEQ3QreDVZSmVQMWQyUXpEU2lxK2NTWnF3cUNaVmNOb0FmdEdHemdyUlo0dmtwYlAweFFaSEN0V3NVd0hWVGpyWTROZlJEakhkVlBSbjhKOVh0b29jL2hJV2txekZhY3pZVGxhcHUyUkNKcWU4YUVvM2p6V1pDRUo3QWlCekJZcmFZRGdPZmVpUXllSlVzYmJYSGRHS2lpdzIyQmRwNE0rWXo2NzFNWjBpZTlFV1JrczBRMkRIR1VUbmU5UzR0QUFKUEdiVWY0eHRDTW9KNDBRNFFXMDVRTFNaeTN3TkZBVUZZbVhFNEVDREZCOCtJRWVORkZGL3RnQW5maDhHdkZoLzF4WStMRStvek1taW5qU1J3aHEwOXpRQUVrTkZ5dTBJS2d2RGt5T0kraWNjRjdkb1dMVFVhc1B4TEgxeEdQQlNLa2VXQ25Tb1gvNEMyYzlkdTY2QmJSWGFtSHFhZStWQ2lTNkJOOTUzaVlLT3U4ck1rbmVXK1RGalYwQ3pBZjhQTkc5TEJheVVnWTVVdzl4MFNhcDZ6STRQcGFaS0g0ZDlrdGtGVSs5MXlCYk41U1J0ZGQ4L0VRYXRKWG1CTmZ3N2tScmxsK0xqd1p3ZVdMRWIwalQxUnBHdURxSExkTFRNTWdvd1NjenJwMnpDc0pkUUgvc2VhWXJwd2tDRlI4ZFJqTTdKUlVrVmExQTBTdmRaQVg3NXZ1QnZ2TEJ6TjJjK1JidDVvcXk5M2xxSUJJaktiN3cyVWtSVGI0cEJXY05LTlBVZ3QzYXovUHhReEVhc2FsdGRtdXl2c1hZYnVyM1ZQTm44SnpOLzZKNU4rUUNuejU3MVRjY2FwQ21rUHZGRStnYXlYVStVRjFWS1l5RmNVdmxldjdsc1FsbWpxeksyUUE4WDdRM1R6RmFwcWh2SktsdDdkekJIcmFxWkJnUGdiOU40b0tOYXlpOEZaVDcxK2thWWhJaEZyRVNLWVJIZkhrajVuM2tmaGdMeSt4K2pMRGJLeE1nZ2tnSG5KejlSdUVIWlhCV3RrWUFRPT0iLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsiaWQiOiJ0b29sdV8wMUg2ZlY4SnZSSk52WHk0N2VSbmQzakgiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6e30sIm5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHdBTVJkUzIxRmVKZnpRaE0zcyIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo3Mzk2LCJvdXRwdXRfdG9rZW5zIjo3NzAsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjczOX0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoic2tlcHRpYyIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy00YWU5LTc2YzAtODlhNi00ZjBiMTE0ZTMwMzAtMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzM5Niwib3V0cHV0X3Rva2VucyI6NzcwLCJ0b3RhbF90b2tlbnMiOjgxNjYsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjczOX19fSx7ImNvbnRlbnQiOiJTdWNjZXNzZnVsbHkgdHJhbnNmZXJyZWQgdG8gdmVyaWZpZXIiLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7fSwidHlwZSI6InRvb2wiLCJuYW1lIjoidHJhbnNmZXJfdG9fdmVyaWZpZXIiLCJpZCI6ImMwNjQ1NjNhLTk3NGYtNGYwZi1hYjRhLTA4MGU2M2MwMWNmNSIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsInN0YXR1cyI6InN1Y2Nlc3MifSx7ImNvbnRlbnQiOlt7InNpZ25hdHVyZSI6IkNBUVM5Z1VLRUFnU0dBSTRBVUlJZEdocGJtdHBibWNTREdGeUoxTVhvWStBaTZOa2J4b01SZDJUclVmVVU3V3JQcEEySWpEZ3k4ZFVwbGNaS2M3RG5Kd05MVEJmSDVVTlIrbkduYWtTUFFpYThVVkl6cmprT25KMmZMcldVc0Eyb0VPOHlSc3Frd1hra0RDc3loQjk3ZHdqNTdSTXQ5MVh6Z1doTDlsdU5FR085MnlXMHJORkdvTVNVVk94UElyRmM1Ump5cmhLRWMzKzFSbnB4Q2t5bHErTE5sUTVORnhMbjZ2MmNGNUR6cXB2QkhOUjdYQWlnK3VySklVM2dYeWRDa2FHWlpEZXRKdFFPVU9YZ1ppOXo5MTAwTnBkQUVrWERvMklDSENHVEdwVmMwSi9wYlh4V1JCeGI2QjEvR2wwWWpuNjhWNkhsZXNIN0lvQzdHUi8zSHdSdTlOU0RCRzR0dGZRakVXY2crUGVBZWE0Um43anp4K1RtOUg3TDFqb1RLVmdEYlFnNWlLTUxmanZQcWxua3NDTHRYT3Q4alNJUzVPWUxON2EvN1RYNi93NWQzNHZudzh2UERtVTFVWWkwUTcycVVXaFVhMnZ3OE9YTnBjS0w3QnAwbE5mQlM4cnZIWSt2Qlk4L2hhQW9UTDN3dkxGVTFmVCs0ei92QTNCY3NpZ1pEWWxmSmVEWWN4WG1SdXF4enhFR3BYQ1JlOXMxSFd0SUQrNDFwSHU2bHBaSGtYYWdocGM2VFpITHhOd2NWaFRiNmZNUTdRbysyTDZQdmtsaDFtMzJwZW9keGVnMllpUGhkcGwzdy94a09FdTUvSkVGN1pCeHBkVUwrdTN3a081cFVsc3FmZ1g3dWh6djdIQzJXcFNXVmltcndpUXdRT1V4MCtKUHJMdHcrbFlmcTdJM2pydW8wR1cwZ3VXdm51ZjdKTTNZTGo4NHErSmdIb21uU1Nabm5abXJMMFVhUnZyVnlMbzg3N0YzOWNjTFM3WG9abm51aThKNFA1OFpUbWlMa0VKZUdjeDFJdWFnMmUzMzMzOU83WWV3LzU5dExiUXRJR2g5VldNd0hCbkZ4MzlTOC80TEVGcks1SGxkUFFoT3Jsb3lUdGtSczY2aVFSS3R4TUtTMFZJNlFUS1pEQWJzN2E3SDF5QnYrbUppOVhnNHVLbXlVNnFubjd6RUtqalkyYklsQ3dleng0TExHUDVIRDNacjZmWnNtZEcxVUh6RHNCdllHMm9DSTBvcUVLTSt2YWxzVDZoVThNUmpiUUJEL0pKOEpKTi9pQkdsaEJBVUhvMFVtaGh0UjJldVRWcFpXbE1uaG1wWEFkS0RYU2ZQZGFQb2hnQiIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJpZCI6InRvb2x1XzAxWEVBdWhRSENCZHlDd3R5bVp3RmZEaSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3JlZF90ZWFtIiwidHlwZSI6InRvb2xfdXNlIiwidG9vbHNldF9uYW1lIjpudWxsfV0sImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2Zkd0FzYWJEcXVlZ0hpNllVcENuIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjc1MDksIm91dHB1dF90b2tlbnMiOjg4LCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjo1NX0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoidmVyaWZpZXIiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtNjY4Mi03ZDIxLThjMTUtZmZiY2U5NDIyYzlmLTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoidHJhbnNmZXJfdG9fcmVkX3RlYW0iLCJhcmdzIjp7fSwiaWQiOiJ0b29sdV8wMVhFQXVoUUhDQmR5Q3d0eW1ad0ZmRGkiLCJ0eXBlIjoidG9vbF9jYWxsIn1dLCJpbnZhbGlkX3Rvb2xfY2FsbHMiOltdLCJ1c2FnZV9tZXRhZGF0YSI6eyJpbnB1dF90b2tlbnMiOjc1MDksIm91dHB1dF90b2tlbnMiOjg4LCJ0b3RhbF90b2tlbnMiOjc1OTcsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjU1fX19LHsiY29udGVudCI6IlN1Y2Nlc3NmdWxseSB0cmFuc2ZlcnJlZCB0byByZWRfdGVhbSIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ0cmFuc2Zlcl90b19yZWRfdGVhbSIsImlkIjoiOTgyYzAxMjYtZTZhYi00NThhLTkyMmUtN2IyNjZlODNjMmIyIiwidG9vbF9jYWxsX2lkIjoidG9vbHVfMDFYRUF1aFFIQ0JkeUN3dHltWndGZkRpIiwic3RhdHVzIjoic3VjY2VzcyJ9LHsiY29udGVudCI6W3sic2lnbmF0dXJlIjoiQ0FRU3dnY0tFQWdTR0FJNEFVSUlkR2hwYm10cGJtY1NET1JYZ2U5dUwxODMyUk1Rd0JvTWpRd0F3QUhiOWlQMW9qUEdJakREU2NhbDQ4NmJlQ0pidEc4L0QyWTlnOVM1eElQZmpvSGcvZHh3WDVIeXBTUUVCYmpGaVRTYlBFQ1hTRnpWNllJcTN3YkxjbXlQYnZ1c0duRGV4Rmd4KzNzSVh6dnF5RlYvNjl4aGloTW5TcGZUQ0h5MCswelN1MVdwWVUvV1VwSWZvYVNYVGNUYTFxa004eDhFay95LzFqa0tNWlVmREZhOTN1aFVKeE9DNCtRZ1BXQmwvK1h5MnQ4MkwyeC9lYnE3UUxRVHF0UjBqWGVhVVJRbTNDbzZKdk9nV21Edk1wRWRUZVRXRTNmRGgxdHp6VktzZmx1bTRkODliNkNhS3RuOG5sUWQ3UmhOOTl2TlFNUUhHNVY0T3VWTmRySFBaWnBGT3VoZGVqc01GV1h5Qis1NXNsR0FkOHdFWGdGS2JJL2tuT1hWR3pML05ERjhHdHkxdE5abHUyZmRTQ0FzWVdNc0ZRUDFjYndMZVFHc3p5WHpKTVZaSjJJOXhCUGt5ejFIT1hDUHloNUYrajJnUlRQaWtWQjNBUkM2dGZCL2xEU1VlS0JWWlBRUzhwWS9CNGVDcytJamphQk5XcnVMemZxQ3NjNnhMMEN1UVBTWXlRWGFZRmpZclNSTmNDNzBXUUJXVDVWVmhwMG1BOXlaREdZYlJadDlJNU9LOVFrUW11SjV6REZhTUVPWHpTcnNDNGx0ZVA2TWRpbWlNaWp2c0gzaXErYzZXVVlvbFJqM25lSTg4RWFISXpMQUdScTY0Qk5DTHpkQVlMMnBFVTJzYjV1Rzc4UEtWb1VVQ2JWSkw1anBET2hMT1RPMWJRc1R1b1VwblJSckxRRC9rSXpWK2xqcFZCd3dmSmN5cTc1VEpBL0N0UHIveHIyU1o5QjFiMnNiakc2akQwbGh0b0NkZGVEZ2NxZ2wvTFNTa2N1YVlZOHhQV0N5K0x0TmhjcVRoZm1mdm5ITEpJcEw2Z2pialcvT1FwUmZWVXdmdzFqT2I5YldWd2NnSGhOWWZLWUdQdE04b3RoNXh3TFlSUlZnenFNS092czdmYWxYUmhWeloyU1A2SXcwQk1tQUFaNkhqZkNsOUwrdFBxSk5sblFRT0hwb1dqdW00QitYc1lsZEZFS0dRV1Fxc2Q1OWlOUHZhbzlwL3NDT3Q3ckdxQ296Y21naUVtNHc2NmV6L1FJRHliNDZuMCt0Y05KUFVjb0JLdlEvakR3RTZWd21rUS9uRUNqRnAzVWREVCtrV1lRU05kOGo4VkJaT3BHOExscVVZckVaQ3YwcTdCczF2Y05yZFlpYUZvUzMyb3NheW5XaytIdDNPSHVGV2JFRjhKbHNGVFFBTzA1VnZZa2VjZjBWL29USHNHNWh5T09zN1VFYURGcTd1YUp1eldWaGg1cEFvaklhQmNSOGZWbG4yUExSVFg3aXMybnNBK1FyZGlkZVBjWnRGdGd4QWlLWWUxaTJzU0ZpNHZtOEVPa0NiUTYzd2RRZVk0VXJyNVB1b01Fd3hVVUxqZ0thZ2lBbXNKRHE0UnJWbTdzTzQrNll1QTlqNmwydXlsa09FYzY3dmYvNWZrWHVDc2lBcE5MNmtlYjhjK3ZMMG40RlhHZHpqK2FwU0RmRkdCZ0IiLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsiaWQiOiJ0b29sdV8wMVZCcldkV0VXWWdpdDRrTlllaEJNVTUiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6e30sIm5hbWUiOiJ0cmFuc2Zlcl90b19lZGl0b3IiLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QXpmanFVb3BvRW1aTGFxbTEiLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzQxNiwib3V0cHV0X3Rva2VucyI6MTUxLCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjoxMTl9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InJlZF90ZWFtIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTZjZTYtNzI1MS1iNzk2LTRmNjQzMzNmNjdjNi0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6InRyYW5zZmVyX3RvX2VkaXRvciIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxVkJyV2RXRVdZZ2l0NGtOWWVoQk1VNSIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzQxNiwib3V0cHV0X3Rva2VucyI6MTUxLCJ0b3RhbF90b2tlbnMiOjc1NjcsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjExOX19fSx7ImNvbnRlbnQiOiJTdWNjZXNzZnVsbHkgdHJhbnNmZXJyZWQgdG8gZWRpdG9yIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJ0b29sIiwibmFtZSI6InRyYW5zZmVyX3RvX2VkaXRvciIsImlkIjoiNWQzOWUzNmItYzViNS00NDI2LWE5YWEtYmUyZjdkMzgxNmQ2IiwidG9vbF9jYWxsX2lkIjoidG9vbHVfMDFWQnJXZFdFV1lnaXQ0a05ZZWhCTVU1Iiwic3RhdHVzIjoic3VjY2VzcyJ9LHsiY29udGVudCI6W3sic2lnbmF0dXJlIjoiQ0FRUzVnWUtFQWdTR0FJNEFVSUlkR2hwYm10cGJtY1NESTRRSWI3TDJ3NWFXM2NNL1JvTUxpMndFeERrcWh1eEpEaVlJakFxR3hNNFdVd2lxNHVaSHJpQ2J1QzRrVndhN2lnZFVMb29jWmJpNWZldktiZndxSFNickxOVDUxTkdYNFhzTHRzcWd3WS90YWJIamxsenF1N25SV3Y4K0Z6NFI2bjlqYTdDcGoyNFp1eEZ1S2trUU9CbUE0VGYxYXBrM1JQcWNFUStqK3dlZHUwbE8rcmRFQ3lkM2xYaGZnRVJGRnFjTTZhdWN4ZWw0UmJ3NW9BSFh1OStYZVR0emwrTjh0QWVaOWlHWjJYVHRVNU9iOGFQVDFLdVgrdXA4a1E0VlVYa0JUZmQyZjVKUDlSeFpFcTl3U1lhWnN5SGR2MG4rMVU0NkFPc3ZLWm1wOVFYQndvMmY0TzkwalA4ZWdpcmVtOUViWmJtYVlPSjhXSzdDdEtSVmJZMnk0Kzl4UlVKMDRuc2RJejVGNUZGS0V4eE5PckI0bWMvTkU4M2FiM1o2NGlXek93cW9VR0FHUVNrYUgwZDVjRXVtWm80ZlpaQWhQeXVKZVFEeHVsTUNld243RUdDbnBKdy9uemkwd295RWFRTDRZN2g5OVA3YVh4RnIyOHA1S0N5SkVKVERlZmVJcFZsQXdNQ0Fqeit4QzY5QUFMbXd5VFErY0ZFTStLQWdlSVNwaExWeThBUHIyRlNoYXhCTnJYUWd6MXM5czJkaExaUU9CaXd6aHhtRWdkSzcrOEphcWhCMGVFRWQvMVJWQXVJWWRlekE1SEJyd0U5RHlRaUNsM1VPc2JTbk5FVmUrZ3JIcDVwM3RtTUJxNjhnTFY4Mm9BekZZWmwrQ2V3eDcxdjVkUEZPdktQRUhVenBRWU9WTHJuQjFFWlJZclp6NzJuMndmU1Z4Qk96cFRjbkJ4VjZIZWRDUHlJektYWk05K3lFZVRKaThRdHZWZ1oxdyt2MGhtQ0ZHcG5tWk43bVhsZ3FMMXM2TVhWeWRwUE9LUGxNM05hN0NkSG5QRllpd3Iyekx2a2tjSU03Q3NrL0MzK1pNQVdUSytDOUIxdmJHeFBsdEUzT3Y4a085OFBnU1htMUFKdUViWHkrcjNhM0QzUWxaSjNkRUtlMkxOZEFCeXJQTWhTY0tVOEdsZkgvOU50Z3JodGFrZ2xsRC9NZkFsVHNiTndORHJVdFdSeG1EdUovMForaGtrL1lOd0VEWUFaYThUbXdWb1dPZEJIcFJQSHZaOUU5dXNldWx0bWErSmsxZ1JMRHR2RVprcVhVRVNrTWNxVXRWd1lNNWFqSkE1TjUyY05pWjE1QlRmOFhqR2hLV2RPVjQwKzhIMFBPU3hNMkppZndDTzBDNXhGVzVCbzNJaUlOdjBVT3RZNWY4a3ZQZE0zUGpraGFPcksxeVRBOGVFRTQ0dnVuTDlQa29HZWNiWW0rWEU1S3pMM1FzRW4yWTFJSndPOXcweEhYUVpFZWlQNSswdTNZbkRjOHE5ZEJMaEdXM0Z5MzBnWUFRPT0iLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsiaWQiOiJ0b29sdV8wMUJtOEFVOUVzUVV3cW9WTFlweEtBNzciLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6eyJmaWxlX3BhdGgiOiIvYW5zd2VyLm1kIiwiY29udGVudCI6IioqVHVwbGUgdnMuIGxpc3QgaW1tdXRhYmlsaXR5IChQeXRob24pKipcblxuQSB0dXBsZSBpcyBpbW11dGFibGU6IG9uY2UgY3JlYXRlZCwgeW91IGNhbid0IGFzc2lnbiB0byBpdHMgaXRlbXMsIGFuZCB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGl0IHJlZmVyZW5jZXMgY2FuJ3QgY2hhbmdlLiBgdFswXSA9IDFgIHJhaXNlcyBgVHlwZUVycm9yYC4gQSBsaXN0IGlzIG11dGFibGU6IHlvdSBjYW4gY2hhbmdlIGl0IGluIHBsYWNlIHdpdGggaW5kZXggb3Igc2xpY2UgYXNzaWdubWVudCwgYGFwcGVuZCgpYCwgYGV4dGVuZCgpYCwgYHNvcnQoKWAgYW5kIHNpbWlsYXIgbWV0aG9kcy5cblxuVHVwbGUgaW1tdXRhYmlsaXR5IGlzIHNoYWxsb3cuIElmIGEgdHVwbGUgaG9sZHMgYSBtdXRhYmxlIG9iamVjdCwgc3VjaCBhcyBhIGxpc3QsIHRoYXQgaW5uZXIgb2JqZWN0IGNhbiBzdGlsbCBjaGFuZ2UsIGFuZCBzbyB0aGUgdHVwbGUncyB2YWx1ZSBjaGFuZ2VzLiBCZWNhdXNlIG9mIHRoaXMsIGEgdHVwbGUgY2FuIGJlIGEgYGRpY3RgIGtleSBvciBgc2V0YCBtZW1iZXIgb25seSBpZiBldmVyeXRoaW5nIGluc2lkZSBpdCBpcyBoYXNoYWJsZS4gQSBsaXN0IGNhbiBuZXZlciBiZSBhIGtleS5cblxuU291cmNlczpcbi0gaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy90dXRvcmlhbC9kYXRhc3RydWN0dXJlcy5odG1sXG4tIGh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvcmVmZXJlbmNlL2RhdGFtb2RlbC5odG1sXG4tIGh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvbGlicmFyeS9zdGR0eXBlcy5odG1sXG4ifSwibmFtZSI6IndyaXRlX2ZpbGUiLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QkJhYTlnak1DY2UxNlRVZW4iLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6ODEyNywib3V0cHV0X3Rva2VucyI6NDMzLCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjo3OX0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoiZWRpdG9yIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTc2ZTktN2NhMy1hNWZmLTYzZGU2NmRkOGRkMS0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6IndyaXRlX2ZpbGUiLCJhcmdzIjp7ImZpbGVfcGF0aCI6Ii9hbnN3ZXIubWQiLCJjb250ZW50IjoiKipUdXBsZSB2cy4gbGlzdCBpbW11dGFiaWxpdHkgKFB5dGhvbikqKlxuXG5BIHR1cGxlIGlzIGltbXV0YWJsZTogb25jZSBjcmVhdGVkLCB5b3UgY2FuJ3QgYXNzaWduIHRvIGl0cyBpdGVtcywgYW5kIHRoZSBjb2xsZWN0aW9uIG9mIG9iamVjdHMgaXQgcmVmZXJlbmNlcyBjYW4ndCBjaGFuZ2UuIGB0WzBdID0gMWAgcmFpc2VzIGBUeXBlRXJyb3JgLiBBIGxpc3QgaXMgbXV0YWJsZTogeW91IGNhbiBjaGFuZ2UgaXQgaW4gcGxhY2Ugd2l0aCBpbmRleCBvciBzbGljZSBhc3NpZ25tZW50LCBgYXBwZW5kKClgLCBgZXh0ZW5kKClgLCBgc29ydCgpYCBhbmQgc2ltaWxhciBtZXRob2RzLlxuXG5UdXBsZSBpbW11dGFiaWxpdHkgaXMgc2hhbGxvdy4gSWYgYSB0dXBsZSBob2xkcyBhIG11dGFibGUgb2JqZWN0LCBzdWNoIGFzIGEgbGlzdCwgdGhhdCBpbm5lciBvYmplY3QgY2FuIHN0aWxsIGNoYW5nZSwgYW5kIHNvIHRoZSB0dXBsZSdzIHZhbHVlIGNoYW5nZXMuIEJlY2F1c2Ugb2YgdGhpcywgYSB0dXBsZSBjYW4gYmUgYSBgZGljdGAga2V5IG9yIGBzZXRgIG1lbWJlciBvbmx5IGlmIGV2ZXJ5dGhpbmcgaW5zaWRlIGl0IGlzIGhhc2hhYmxlLiBBIGxpc3QgY2FuIG5ldmVyIGJlIGEga2V5LlxuXG5Tb3VyY2VzOlxuLSBodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3R1dG9yaWFsL2RhdGFzdHJ1Y3R1cmVzLmh0bWxcbi0gaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9yZWZlcmVuY2UvZGF0YW1vZGVsLmh0bWxcbi0gaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9saWJyYXJ5L3N0ZHR5cGVzLmh0bWxcbiJ9LCJpZCI6InRvb2x1XzAxQm04QVU5RXNRVXdxb1ZMWXB4S0E3NyIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6ODEyNywib3V0cHV0X3Rva2VucyI6NDMzLCJ0b3RhbF90b2tlbnMiOjg1NjAsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjc5fX19LHsiY29udGVudCI6IlVwZGF0ZWQgZmlsZSAvYW5zd2VyLm1kIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJ0b29sIiwibmFtZSI6IndyaXRlX2ZpbGUiLCJpZCI6ImUzNDkyZTA4LWM4NjktNDdlYy1hNWY3LWNjNTdlYTVmNjQwMyIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxQm04QVU5RXNRVXdxb1ZMWXB4S0E3NyIsInN0YXR1cyI6InN1Y2Nlc3MifSx7ImNvbnRlbnQiOiJJIHdyb3RlIHRoZSBhbnN3ZXIgdG8gYC9hbnN3ZXIubWRgLiBJdCBpcyBhYm91dCAxMDAgd29yZHMsIHVuZGVyIHRoZSAxMjAtd29yZCBsaW1pdC5cblxuSSBkaWQgbm90IGdldCBhIHNrZXB0aWMgb3IgdmVyaWZpZXIgY2hlY2suIEkgaGFuZGVkIG9mZiB0byBib3RoLCBidXQgbmVpdGhlciByZXR1cm5lZCBhbnkgZmVlZGJhY2suIEkgYWxzbyB0cmllZCB0byBoYW5kIG9mZiB0byBhIGByZWRfdGVhbWAgYWdlbnQsIGFuZCB0aGF0IHRyYW5zZmVyIGlzbid0IG9uZSBvZiB0aGUgdG9vbHMgSSB3YXMgZ2l2ZW4uIEkgdGhlbiB3cm90ZSB0aGUgYW5zd2VyIG15c2VsZiBmcm9tIHRoZSBvZmZpY2lhbCBQeXRob24gZG9jdW1lbnRhdGlvbiBzZWFyY2ggcmVzdWx0cy4gSSBkaWRuJ3Qgb3BlbiB0aGUgcGFnZXMgZGlyZWN0bHkuXG5cbioqVHVwbGUgdnMuIGxpc3QgaW1tdXRhYmlsaXR5IChQeXRob24pKipcblxuQSB0dXBsZSBpcyBpbW11dGFibGU6IG9uY2UgY3JlYXRlZCwgeW91IGNhbid0IGFzc2lnbiB0byBpdHMgaXRlbXMsIGFuZCB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGl0IHJlZmVyZW5jZXMgY2FuJ3QgY2hhbmdlLiBgdFswXSA9IDFgIHJhaXNlcyBgVHlwZUVycm9yYC4gQSBsaXN0IGlzIG11dGFibGU6IHlvdSBjYW4gY2hhbmdlIGl0IGluIHBsYWNlIHdpdGggaW5kZXggb3Igc2xpY2UgYXNzaWdubWVudCwgYGFwcGVuZCgpYCwgYGV4dGVuZCgpYCwgYHNvcnQoKWAgYW5kIHNpbWlsYXIgbWV0aG9kcy5cblxuVHVwbGUgaW1tdXRhYmlsaXR5IGlzIHNoYWxsb3cuIElmIGEgdHVwbGUgaG9sZHMgYSBtdXRhYmxlIG9iamVjdCwgc3VjaCBhcyBhIGxpc3QsIHRoYXQgaW5uZXIgb2JqZWN0IGNhbiBzdGlsbCBjaGFuZ2UsIGFuZCBzbyB0aGUgdHVwbGUncyB2YWx1ZSBjaGFuZ2VzLiBCZWNhdXNlIG9mIHRoaXMsIGEgdHVwbGUgY2FuIGJlIGEgYGRpY3RgIGtleSBvciBgc2V0YCBtZW1iZXIgb25seSBpZiBldmVyeXRoaW5nIGluc2lkZSBpdCBpcyBoYXNoYWJsZS4gQSBsaXN0IGNhbiBuZXZlciBiZSBhIGtleS5cblxuU291cmNlczpcbi0gaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy90dXRvcmlhbC9kYXRhc3RydWN0dXJlcy5odG1sXG4tIGh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvcmVmZXJlbmNlL2RhdGFtb2RlbC5odG1sXG4tIGh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvbGlicmFyeS9zdGR0eXBlcy5odG1sIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QlVzTmhyMTlnUU5WY0ZzVWUiLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoiZW5kX3R1cm4iLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6ODQ5NCwib3V0cHV0X3Rva2VucyI6NDE1LCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjowfSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCIsIm1vZGVsX3Byb3ZpZGVyIjoiYW50aHJvcGljIn0sInR5cGUiOiJhaSIsIm5hbWUiOiJlZGl0b3IiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtODZiYi03NjYxLWE1OWYtYTM5ZDZiNGU2Yjc0LTAiLCJ0b29sX2NhbGxzIjpbXSwiaW52YWxpZF90b29sX2NhbGxzIjpbXSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjo4NDk0LCJvdXRwdXRfdG9rZW5zIjo0MTUsInRvdGFsX3Rva2VucyI6ODkwOSwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fV19" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "4809a44f637a7046", + "parentSpanId": "41a84418436b8795", + "name": "editor", + "kind": 1, + "startTimeUnixNano": "1790968035043306240", + "endTimeUnixNano": "1790968042918324992", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "editor" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "5" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "editor" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:editor\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"editor\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "editor:42b8f95e-d208-6d0d-c59e-cc043f552134" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "editor" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "editor:42b8f95e-d208-6d0d-c59e-cc043f552134" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "seq:step:1" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W3siY29udGVudCI6IkZvciBhIHN5bnRoZXRpYyB0cmFjaW5nIGZpeHR1cmUsIGV4cGxhaW4gUHl0aG9uIHR1cGxlIHZlcnN1cyBsaXN0IGltbXV0YWJpbGl0eSBpbiBhdCBtb3N0IDEyMCB3b3Jkcy4gVXNlIG9ubHkgb2ZmaWNpYWwgUHl0aG9uIGRvY3VtZW50YXRpb24gaWYgeW91IHNlYXJjaC4gSGF2ZSB0aGUgc2tlcHRpYyBhbmQgdmVyaWZpZXIgY2hlY2sgaXQsIHRoZW4gaGF2ZSB0aGUgZWRpdG9yIHdyaXRlIC9hbnN3ZXIubWQuIEtlZXAgdGhlIHJ1biBzaG9ydCBhbmQgZG8gbm90IGFzayBmb2xsb3ctdXAgcXVlc3Rpb25zLiIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoiaHVtYW4iLCJpZCI6IjQ2MjA0MzA1LWY4ZWUtNDM4NC05Y2U4LWZjZDgwYmZjZGY3NSJ9LHsiY29udGVudCI6W3siaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9LCJuYW1lIjoid2ViX3NlYXJjaCIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHc5YVc5YnJlblBxZ3liZk53UCIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjoyODQ1LCJvdXRwdXRfdG9rZW5zIjo3Mywib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6MH0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoicmVzZWFyY2hlciIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy0yMWEzLTcwMDAtYTM4MS0yODRhNmEwYzAzZjctMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ3ZWJfc2VhcmNoIiwiYXJncyI6eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9LCJpZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6Mjg0NSwib3V0cHV0X3Rva2VucyI6NzMsInRvdGFsX3Rva2VucyI6MjkxOCwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fSx7ImNvbnRlbnQiOiJbe1widGl0bGVcIjogXCI1LiBEYXRhIFN0cnVjdHVyZXMgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3R1dG9yaWFsL2RhdGFzdHJ1Y3R1cmVzLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcIllvdSBtaWdodCBoYXZlIG5vdGljZWQgdGhhdCBtZXRob2RzIGxpa2VgaW5zZXJ0YCxgcmVtb3ZlYCBvcmBzb3J0YCB0aGF0IG9ubHkgbW9kaWZ5IHRoZSBsaXN0IGhhdmUgbm8gcmV0dXJuIHZhbHVlIHByaW50ZWQgXFx1MjAxMyB0aGV5IHJldHVybiB0aGUgZGVmYXVsdGBOb25lYC4gWzFdIFRoaXMgaXMgYSBkZXNpZ24gcHJpbmNpcGxlIGZvciBhbGwgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgaW4gUHl0aG9uLlxcbi4uLlxcbiMjIDUuMy4gVHVwbGVzIGFuZCBTZXF1ZW5jZXNcXHUwMGI2XFxuXFxuV2Ugc2F3IHRoYXQgbGlzdHMgYW5kIHN0cmluZ3MgaGF2ZSBtYW55IGNvbW1vbiBwcm9wZXJ0aWVzLCBzdWNoIGFzIGluZGV4aW5nIGFuZCBzbGljaW5nIG9wZXJhdGlvbnMuIFRoZXkgYXJlIHR3byBleGFtcGxlcyBvZiBzZXF1ZW5jZSBkYXRhIHR5cGVzIChzZWUgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBsaXN0LCB0dXBsZSwgcmFuZ2UpLiBTaW5jZSBQeXRob24gaXMgYW4gZXZvbHZpbmcgbGFuZ3VhZ2UsIG90aGVyIHNlcXVlbmNlIGRhdGEgdHlwZXMgbWF5IGJlIGFkZGVkLiBUaGVyZSBpcyBhbHNvIGFub3RoZXIgc3RhbmRhcmQgc2VxdWVuY2UgZGF0YSB0eXBlOiB0aGUgdHVwbGUuXFxuXFxuQSB0dXBsZSBjb25zaXN0cyBvZiBhIG51bWJlciBvZiB2YWx1ZXMgc2VwYXJhdGVkIGJ5IGNvbW1hcywgZm9yIGluc3RhbmNlOlxcbi4uLlxcbj4+PiAjIFR1cGxlcyBhcmUgaW1tdXRhYmxlOlxcbj4+PiB0WzBdID0gODg4ODhcXG5UcmFjZWJhY2sgKG1vc3QgcmVjZW50IGNhbGwgbGFzdCk6XFxuICBGaWxlIFxcXCI8c3RkaW4+XFxcIiwgbGluZSAxLCBpbiA8bW9kdWxlPlxcblR5cGVFcnJvcjogJ3R1cGxlJyBvYmplY3QgZG9lcyBub3Qgc3VwcG9ydCBpdGVtIGFzc2lnbm1lbnRcXG4+Pj4gIyBidXQgdGhleSBjYW4gY29udGFpbiBtdXRhYmxlIG9iamVjdHM6XFxuPj4+IHYgPSAoWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuPj4+IHZcXG4oWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuLi4uXFxuQXMgeW91IHNlZSwgb24gb3V0cHV0IHR1cGxlcyBhcmUgYWx3YXlzIGVuY2xvc2VkIGluIHBhcmVudGhlc2VzLCBzbyB0aGF0IG5lc3RlZCB0dXBsZXMgYXJlIGludGVycHJldGVkIGNvcnJlY3RseTsgdGhleSBtYXkgYmUgaW5wdXQgd2l0aCBvciB3aXRob3V0IHN1cnJvdW5kaW5nIHBhcmVudGhlc2VzLCBhbHRob3VnaCBvZnRlbiBwYXJlbnRoZXNlcyBhcmUgbmVjZXNzYXJ5IGFueXdheSAoaWYgdGhlIHR1cGxlIGlzIHBhcnQgb2YgYSBsYXJnZXIgZXhwcmVzc2lvbikuIEl0IGlzIG5vdCBwb3NzaWJsZSB0byBhc3NpZ24gdG8gdGhlIGluZGl2aWR1YWwgaXRlbXMgb2YgYSB0dXBsZSwgaG93ZXZlciBpdCBpcyBwb3NzaWJsZSB0byBjcmVhdGUgdHVwbGVzIHdoaWNoIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzLCBzdWNoIGFzIGxpc3RzLlxcblxcblRob3VnaCB0dXBsZXMgbWF5IHNlZW0gc2ltaWxhciB0byBsaXN0cywgdGhleSBhcmUgb2Z0ZW4gdXNlZCBpbiBkaWZmZXJlbnQgc2l0dWF0aW9ucyBhbmQgZm9yIGRpZmZlcmVudCBwdXJwb3Nlcy4gVHVwbGVzIGFyZSBpbW11dGFibGUsIGFuZCB1c3VhbGx5IGNvbnRhaW4gYSBoZXRlcm9nZW5lb3VzIHNlcXVlbmNlIG9mIGVsZW1lbnRzIHRoYXQgYXJlIGFjY2Vzc2VkIHZpYSB1bnBhY2tpbmcgKHNlZSBsYXRlciBpbiB0aGlzIHNlY3Rpb24pIG9yIGluZGV4aW5nIChvciBldmVuIGJ5IGF0dHJpYnV0ZSBpbiB0aGUgY2FzZSBvZiBuYW1lZHR1cGxlcykuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG5Bbm90aGVyIHVzZWZ1bCBkYXRhIHR5cGUgYnVpbHQgaW50byBQeXRob24gaXMgdGhlIGRpY3Rpb25hcnkgKHNlZSBNYXBwaW5nIFR5cGVzIFxcdTIwMTQgZGljdCkuIERpY3Rpb25hcmllcyBhcmUgc29tZXRpbWVzIGZvdW5kIGluIG90aGVyIGxhbmd1YWdlcyBhcyBcXHUyMDFjYXNzb2NpYXRpdmUgbWVtb3JpZXNcXHUyMDFkIG9yIFxcdTIwMWNhc3NvY2lhdGl2ZSBhcnJheXNcXHUyMDFkLiBVbmxpa2Ugc2VxdWVuY2VzLCB3aGljaCBhcmUgaW5kZXhlZCBieSBhIHJhbmdlIG9mIG51bWJlcnMsIGRpY3Rpb25hcmllcyBhcmUgaW5kZXhlZCBieSBrZXlzLCB3aGljaCBjYW4gYmUgYW55IGltbXV0YWJsZSB0eXBlOyBzdHJpbmdzIGFuZCBudW1iZXJzIGNhbiBhbHdheXMgYmUga2V5cy4gVHVwbGVzIGNhbiBiZSB1c2VkIGFzIGtleXMgaWYgdGhleSBjb250YWluIG9ubHkgc3RyaW5ncywgbnVtYmVycywgb3IgdHVwbGVzOyBpZiBhIHR1cGxlIGNvbnRhaW5zIGFueSBtdXRhYmxlIG9iamVjdCBlaXRoZXIgZGlyZWN0bHkgb3IgaW5kaXJlY3RseSwgaXQgY2Fubm90IGJlIHVzZWQgYXMgYSBrZXkuIFlvdSBjYW5cXHUyMDE5dCB1c2UgbGlzdHMgYXMga2V5cywgc2luY2UgbGlzdHMgY2FuIGJlIG1vZGlmaWVkIGluIHBsYWNlIHVzaW5nIGluZGV4IGFzc2lnbm1lbnRzLCBzbGljZSBhc3NpZ25tZW50cywgb3IgbWV0aG9kcyBsaWtlIGFwcGVuZCgpIGFuZCBleHRlbmQoKS5cIl19LCB7XCJ0aXRsZVwiOiBcIjMuIERhdGEgbW9kZWwgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3JlZmVyZW5jZS9kYXRhbW9kZWwuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiVGhlIHZhbHVlIG9mIHNvbWUgb2JqZWN0cyBjYW4gY2hhbmdlLiBPYmplY3RzIHdob3NlIHZhbHVlIGNhbiBjaGFuZ2UgYXJlIHNhaWQgdG8gYmUgbXV0YWJsZTsgb2JqZWN0cyB3aG9zZSB2YWx1ZSBpcyB1bmNoYW5nZWFibGUgb25jZSB0aGV5IGFyZSBjcmVhdGVkIGFyZSBjYWxsZWQgaW1tdXRhYmxlLiAoVGhlIHZhbHVlIG9mIGFuIGltbXV0YWJsZSBjb250YWluZXIgb2JqZWN0IHRoYXQgY29udGFpbnMgYSByZWZlcmVuY2UgdG8gYSBtdXRhYmxlIG9iamVjdCBjYW4gY2hhbmdlIHdoZW4gdGhlIGxhdHRlclxcdTIwMTlzIHZhbHVlIGlzIGNoYW5nZWQ7IGhvd2V2ZXIgdGhlIGNvbnRhaW5lciBpcyBzdGlsbCBjb25zaWRlcmVkIGltbXV0YWJsZSwgYmVjYXVzZSB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGl0IGNvbnRhaW5zIGNhbm5vdCBiZSBjaGFuZ2VkLiBTbywgaW1tdXRhYmlsaXR5IGlzIG5vdCBzdHJpY3RseSB0aGUgc2FtZSBhcyBoYXZpbmcgYW4gdW5jaGFuZ2VhYmxlIHZhbHVlLCBpdCBpcyBtb3JlIHN1YnRsZS4pIEFuIG9iamVjdFxcdTIwMTlzIG11dGFiaWxpdHkgaXMgZGV0ZXJtaW5lZCBieSBpdHMgdHlwZTsgZm9yIGluc3RhbmNlLCBudW1iZXJzLCBzdHJpbmdzIGFuZCB0dXBsZXMgYXJlIGltbXV0YWJsZSwgd2hpbGUgZGljdGlvbmFyaWVzIGFuZCBsaXN0cyBhcmUgbXV0YWJsZS5cXG4uLi5cXG5Tb21lIG9iamVjdHMgY29udGFpbiByZWZlcmVuY2VzIHRvIG90aGVyIG9iamVjdHM7IHRoZXNlIGFyZSBjYWxsZWQgY29udGFpbmVycy4gRXhhbXBsZXMgb2YgY29udGFpbmVycyBhcmUgdHVwbGVzLCBsaXN0cyBhbmQgZGljdGlvbmFyaWVzLiBUaGUgcmVmZXJlbmNlcyBhcmUgcGFydCBvZiBhIGNvbnRhaW5lclxcdTIwMTlzIHZhbHVlLiBJbiBtb3N0IGNhc2VzLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIHZhbHVlIG9mIGEgY29udGFpbmVyLCB3ZSBpbXBseSB0aGUgdmFsdWVzLCBub3QgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGNvbnRhaW5lZCBvYmplY3RzOyBob3dldmVyLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIG11dGFiaWxpdHkgb2YgYSBjb250YWluZXIsIG9ubHkgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGltbWVkaWF0ZWx5IGNvbnRhaW5lZCBvYmplY3RzIGFyZSBpbXBsaWVkLiBTbywgaWYgYW4gaW1tdXRhYmxlIGNvbnRhaW5lciAobGlrZSBhIHR1cGxlKSBjb250YWlucyBhIHJlZmVyZW5jZSB0byBhIG11dGFibGUgb2JqZWN0LCBpdHMgdmFsdWUgY2hhbmdlcyBpZiB0aGF0IG11dGFibGUgb2JqZWN0IGlzIGNoYW5nZWQuXFxuLi4uXFxuIyMjIDMuMi41LiBTZXF1ZW5jZXNcXHUwMGI2XFxuLi4uXFxuIyMjIyAzLjIuNS4xLiBJbW11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbkFuIG9iamVjdCBvZiBhbiBpbW11dGFibGUgc2VxdWVuY2UgdHlwZSBjYW5ub3QgY2hhbmdlIG9uY2UgaXQgaXMgY3JlYXRlZC4gKElmIHRoZSBvYmplY3QgY29udGFpbnMgcmVmZXJlbmNlcyB0byBvdGhlciBvYmplY3RzLCB0aGVzZSBvdGhlciBvYmplY3RzIG1heSBiZSBtdXRhYmxlIGFuZCBtYXkgYmUgY2hhbmdlZDsgaG93ZXZlciwgdGhlIGNvbGxlY3Rpb24gb2Ygb2JqZWN0cyBkaXJlY3RseSByZWZlcmVuY2VkIGJ5IGFuIGltbXV0YWJsZSBvYmplY3QgY2Fubm90IGNoYW5nZS4pXFxuXFxuVGhlIGZvbGxvd2luZyB0eXBlcyBhcmUgaW1tdXRhYmxlIHNlcXVlbmNlczpcXG4uLi5cXG5UdXBsZXNcXG46IFRoZSBpdGVtcyBvZiBhIGB0dXBsZWAgYXJlIGFyYml0cmFyeSBQeXRob24gb2JqZWN0cy4gVHVwbGVzIG9mIHR3byBvciBtb3JlIGl0ZW1zIGFyZSBmb3JtZWQgYnkgY29tbWEtc2VwYXJhdGVkIGxpc3RzIG9mIGV4cHJlc3Npb25zLiBBIHR1cGxlIG9mIG9uZSBpdGVtIChhIFxcdTIwMThzaW5nbGV0b25cXHUyMDE5KSBjYW4gYmUgZm9ybWVkIGJ5IGFmZml4aW5nIGEgY29tbWEgdG8gYW4gZXhwcmVzc2lvbiAoYW4gZXhwcmVzc2lvbiBieSBpdHNlbGYgZG9lcyBub3QgY3JlYXRlIGEgdHVwbGUsIHNpbmNlIHBhcmVudGhlc2VzIG11c3QgYmUgdXNhYmxlIGZvciBncm91cGluZyBvZiBleHByZXNzaW9ucykuIEFuIGVtcHR5IHR1cGxlIGNhbiBiZSBmb3JtZWQgYnkgYW4gZW1wdHkgcGFpciBvZiBwYXJlbnRoZXNlcy5cXG4uLi5cXG4jIyMjIDMuMi41LjIuIE11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbk11dGFibGUgc2VxdWVuY2VzIGNhbiBiZSBjaGFuZ2VkIGFmdGVyIHRoZXkgYXJlIGNyZWF0ZWQuIFRoZSBzdWJzY3JpcHRpb24gYW5kIHNsaWNpbmcgbm90YXRpb25zIGNhbiBiZSB1c2VkIGFzIHRoZSB0YXJnZXQgb2YgYXNzaWdubWVudCBhbmQgYGRlbGAgKGRlbGV0ZSkgc3RhdGVtZW50cy5cXG4uLi5cXG5UaGVyZSBhcmUgY3VycmVudGx5IHR3byBpbnRyaW5zaWMgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlczpcXG5cXG5MaXN0c1xcbjogVGhlIGl0ZW1zIG9mIGEgbGlzdCBhcmUgYXJiaXRyYXJ5IFB5dGhvbiBvYmplY3RzLiBMaXN0cyBhcmUgZm9ybWVkIGJ5IHBsYWNpbmcgYSBjb21tYS1zZXBhcmF0ZWQgbGlzdCBvZiBleHByZXNzaW9ucyBpbiBzcXVhcmUgYnJhY2tldHMuIChOb3RlIHRoYXQgdGhlcmUgYXJlIG5vIHNwZWNpYWwgY2FzZXMgbmVlZGVkIHRvIGZvcm0gbGlzdHMgb2YgbGVuZ3RoIDAgb3IgMS4pXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjcgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvYnVpbHRpbnMvc3RkdHlwZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiY29sbGVjdGlvbiBjbGFzc2VzIGFyZSBtdXRhYmxlLiBUaGUgbWV0aG9kcyB0aGF0IGFkZCwgc3VidHJhY3QsIG9yIHJlYXJyYW5nZSB0aGVpciAuLi4gaW4gcGxhY2UsIGFuZCBkb25cXHUyMDE5dCByZXR1cm4gYSAuLi4gLCBuZXZlciByZXR1cm4gdGhlIGNvbGxlY3Rpb24gaW5zdGFuY2UgaXRzZWxmIGJ1dCBgTm9uZWAuXFxuLi4uXFxuIyMgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBgbGlzdGAsIGB0dXBsZWAsIGByYW5nZWBcXHUwMGI2XFxuXFxuVGhlcmUgYXJlIHRocmVlIGJhc2ljIHNlcXVlbmNlIHR5cGVzOiBsaXN0cywgdHVwbGVzLCBhbmQgcmFuZ2Ugb2JqZWN0cy4gQWRkaXRpb25hbCBzZXF1ZW5jZSB0eXBlcyB0YWlsb3JlZCBmb3IgcHJvY2Vzc2luZyBvZiBiaW5hcnkgZGF0YSBhbmQgdGV4dCBzdHJpbmdzIGFyZSBkZXNjcmliZWQgaW4gZGVkaWNhdGVkIHNlY3Rpb25zLlxcbi4uLlxcblRoZSBvcGVyYXRpb25zIGluIHRoZSBmb2xsb3dpbmcgdGFibGUgLi4uIG11dGFibGUgYW5kIGltbXV0YWJsZS4gVGhlIGBjb2xsZWN0aW9ucy4gLi4uIGlzIHByb3ZpZGVkIHRvIG1ha2UgLi4uIGVhc2llciB0byBjb3JyZWN0bHkgaW1wbGVtZW50IHRoZXNlIG9wZXJhdGlvbnMgb24gY3VzdG9tIHNlcXVlbmNlIHR5cGVzXFxuLi4uXFxuIyMjIEltbXV0YWJsZSBTZXF1ZW5jZSBUeXBlc1xcdTAwYjZcXG4uLi5cXG5tdXRhYmxlIHNlcXVlbmNlIHR5cGVzIGlzXFxuLi4uXFxuc3VwcG9ydCBhbGxvd3MgaW1tdXRhYmxlIHNlcXVlbmNlcywgLi4uICwgdG8gYmUgdXNlZCBhcyBgZGljdGAga2V5cyBhbmQgc3RvcmVkIGluIC4uLiBlbnNldGAgaW5zdGFuY2VzXFxuLi4uXFxuIyMjIE11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxuIyMjIExpc3RzXFx1MDBiNlxcblxcbkxpc3RzIGFyZSBtdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaG9tb2dlbmVvdXMgaXRlbXMgKHdoZXJlIHRoZSBwcmVjaXNlIGRlZ3JlZSBvZiBzaW1pbGFyaXR5IHdpbGwgdmFyeSBieSBhcHBsaWNhdGlvbikuXFxuLi4uXFxuIyMjIFR1cGxlc1xcdTAwYjZcXG5cXG5UdXBsZXMgYXJlIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhldGVyb2dlbmVvdXMgZGF0YSAoc3VjaCBhcyB0aGUgMi10dXBsZXMgcHJvZHVjZWQgYnkgdGhlIGBlbnVtZXJhdGUoKWAgYnVpbHQtaW4pLiBUdXBsZXMgYXJlIGFsc28gdXNlZCBmb3IgY2FzZXMgd2hlcmUgYW4gaW1tdXRhYmxlIHNlcXVlbmNlIG9mIGhvbW9nZW5lb3VzIGRhdGEgaXMgbmVlZGVkIChzdWNoIGFzIGFsbG93aW5nIHN0b3JhZ2UgaW4gYSBgc2V0YCBvciBgZGljdGAgaW5zdGFuY2UpLlxcbi4uLlxcbnR1cGxlKGl0ZXJhYmxlPVxcbi4uLlxcblRoZSBjb25zdHJ1Y3RvciBidWlsZHMgYSB0dXBsZSB3aG9zZSBpdGVtcyBhcmUgdGhlIHNhbWUgYW5kIGluIHRoZSBzYW1lIG9yZGVyIGFzIGl0ZXJhYmxlXFx1MjAxOXMgaXRlbXMuIGl0ZXJhYmxlIG1heSBiZSBlaXRoZXIgYSBzZXF1ZW5jZSwgYSBjb250YWluZXIgdGhhdCBzdXBwb3J0cyBpdGVyYXRpb24sIG9yIGFuIGl0ZXJhdG9yIG9iamVjdC4gSWYgaXRlcmFibGUgaXMgYWxyZWFkeSBhIHR1cGxlLCBpdCBpcyByZXR1cm5lZCB1bmNoYW5nZWQuIEZvciBleGFtcGxlLCBgdHVwbGUoJ2FiYycpYCByZXR1cm5zIGAoJ2EnLCAnYicsICdjJylgIGFuZCBgdHVwbGUoIFsxLCAyLCAzXSApYCByZXR1cm5zIGAoMSwgMiwgMylgLiBJZiBubyBhcmd1bWVudCBpcyBnaXZlbiwgdGhlIGNvbnN0cnVjdG9yIGNyZWF0ZXMgYSBuZXcgZW1wdHkgdHVwbGUsIGAoKWAuXFxuLi4uXFxuVHVwbGVzIGltcGxlbWVudCBhbGwgb2YgdGhlIGNvbW1vbiBzZXF1ZW5jZSBvcGVyYXRpb25zLlxcbi4uLlxcbkZvciBoZXRlcm9nZW5lb3VzIGNvbGxlY3Rpb25zIG9mIGRhdGEgd2hlcmUgYWNjZXNzIGJ5IG5hbWUgaXMgY2xlYXJlciB0aGFuIGFjY2VzcyBieSBpbmRleCwgYGNvbGxlY3Rpb25zLm5hbWVkdHVwbGUoKWAgbWF5IGJlIGEgbW9yZSBhcHByb3ByaWF0ZSBjaG9pY2UgdGhhbiBhIHNpbXBsZSB0dXBsZSBvYmplY3QuXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvbGlicmFyeS9zdGR0eXBlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJTb21lIGNvbGxlY3Rpb24gY2xhc3NlcyBhcmUgbXV0YWJsZS4gVGhlIG1ldGhvZHMgdGhhdCBhZGQsIHN1YnRyYWN0LCBvciByZWFycmFuZ2UgdGhlaXIgLi4uIGluIHBsYWNlLCBhbmQgZG9uXFx1MjAxOXQgcmV0dXJuIGEgLi4uICwgbmV2ZXIgcmV0dXJuIHRoZSBjb2xsZWN0aW9uIGluc3RhbmNlIGl0c2VsZiBidXQgYE5vbmVgLlxcbi4uLlxcbiMjIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgYGxpc3RgLCBgdHVwbGVgLCBgcmFuZ2VgXFx1MDBiNlxcblxcblRoZXJlIGFyZSB0aHJlZSBiYXNpYyBzZXF1ZW5jZSB0eXBlczogbGlzdHMsIHR1cGxlcywgYW5kIC4uLiBvYmplY3RzLiBBZGRpdGlvbmFsIHNlcXVlbmNlIHR5cGVzIHRhaWxvcmVkIGZvciBwcm9jZXNzaW5nIG9mIGJpbmFyeSBkYXRhIGFuZCB0ZXh0IHN0cmluZ3MgYXJlIGRlc2NyaWJlZCBpbiBkZWRpY2F0ZWQgc2VjdGlvbnMuXFxuLi4uXFxuVGhlIG9wZXJhdGlvbnMgaW4gdGhlIGZvbGxvd2luZyB0YWJsZSAuLi4gaW1tdXRhYmxlLiBUaGUgYCAuLi4gaXMgcHJvdmlkZWQgdG8gbWFrZSAuLi4gZWFzaWVyIHRvIGNvcnJlY3RseSBpbXBsZW1lbnQgdGhlc2Ugb3BlcmF0aW9ucyBvbiBjdXN0b20gc2VxdWVuY2UgdHlwZXNcXG4uLi5cXG4jIyMgSW1tdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbm9wZXJhdGlvbiB0aGF0IGltbXV0YWJsZSBzZXF1ZW5jZSAuLi4gYnkgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlcyBpcyAuLi4gYGhhc2goKWBcXG4uLi5cXG5UaGlzIHN1cHBvcnQgYWxsb3dzIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHN1Y2ggYXMgYHR1cGxlYCBpbnN0YW5jZXMsIHRvIGJlIHVzZWQgYXMgYGRpY3RgIGtleXMgYW5kIHN0b3JlZCBpbiBgc2V0YCBhbmQgYGZyb3plbnNldGAgaW5zdGFuY2VzLlxcbi4uLlxcbiMjIyBNdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbiMjIyBMaXN0c1xcdTAwYjZcXG5cXG5MaXN0cyBhcmUgbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhvbW9nZW5lb3VzIGl0ZW1zICh3aGVyZSB0aGUgcHJlY2lzZSBkZWdyZWUgb2Ygc2ltaWxhcml0eSB3aWxsIHZhcnkgYnkgYXBwbGljYXRpb24pLlxcbi4uLlxcbiMjIyBUdXBsZXNcXHUwMGI2XFxuXFxuVHVwbGVzIGFyZSBpbW11dGFibGUgc2VxdWVuY2VzLCB0eXBpY2FsbHkgdXNlZCB0byBzdG9yZSBjb2xsZWN0aW9ucyBvZiBoZXRlcm9nZW5lb3VzIGRhdGEgKHN1Y2ggYXMgdGhlIDItdHVwbGVzIHByb2R1Y2VkIGJ5IHRoZSBgZW51bWVyYXRlKClgIGJ1aWx0LWluKS4gVHVwbGVzIGFyZSBhbHNvIHVzZWQgZm9yIGNhc2VzIHdoZXJlIGFuIGltbXV0YWJsZSBzZXF1ZW5jZSBvZiBob21vZ2VuZW91cyBkYXRhIGlzIG5lZWRlZCAoc3VjaCBhcyBhbGxvd2luZyBzdG9yYWdlIGluIGEgYHNldGAgb3IgYGRpY3RgIGluc3RhbmNlKS5cXG4uLi5cXG5UaGUgY29uc3RydWN0b3IgYnVpbGRzIGEgdHVwbGUgd2hvc2UgaXRlbXMgYXJlIHRoZSBzYW1lIGFuZCBpbiB0aGUgc2FtZSBvcmRlciBhcyBpdGVyYWJsZVxcdTIwMTlzIGl0ZW1zLiBpdGVyYWJsZSBtYXkgYmUgZWl0aGVyIGEgc2VxdWVuY2UsIGEgY29udGFpbmVyIHRoYXQgc3VwcG9ydHMgaXRlcmF0aW9uLCBvciBhbiBpdGVyYXRvciBvYmplY3QuIElmIGl0ZXJhYmxlIGlzIGFscmVhZHkgYSB0dXBsZSwgaXQgaXMgcmV0dXJuZWQgdW5jaGFuZ2VkLiBGb3IgZXhhbXBsZSwgYHR1cGxlKCdhYmMnKWAgcmV0dXJucyBgKCdhJywgJ2InLCAnYycpYCBhbmQgYHR1cGxlKCBbMSwgMiwgM10gKWAgcmV0dXJucyBgKDEsIDIsIDMpYC4gSWYgbm8gYXJndW1lbnQgaXMgZ2l2ZW4sIHRoZSBjb25zdHJ1Y3RvciBjcmVhdGVzIGEgbmV3IGVtcHR5IHR1cGxlLCBgKClgLlxcbi4uLlxcblR1cGxlcyBpbXBsZW1lbnQgYWxsIG9mIHRoZSBjb21tb24gc2VxdWVuY2Ugb3BlcmF0aW9ucy5cXG4uLi5cXG5Gb3IgaGV0ZXJvZ2VuZW91cyBjb2xsZWN0aW9ucyBvZiBkYXRhIHdoZXJlIGFjY2VzcyBieSBuYW1lIGlzIGNsZWFyZXIgdGhhbiBhY2Nlc3MgYnkgaW5kZXgsIGBjb2xsZWN0aW9ucy5uYW1lZHR1cGxlKClgIG1heSBiZSBhIG1vcmUgYXBwcm9wcmlhdGUgY2hvaWNlIHRoYW4gYSBzaW1wbGUgdHVwbGUgb2JqZWN0LlwiXX0sIHtcInRpdGxlXCI6IFwiNS4gRGF0YSBTdHJ1Y3R1cmVzIFxcdTIwMTQgUHl0aG9uIDMuMTAuMjAgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMuMTAvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiWW91IG1pZ2h0IGhhdmUgbm90aWNlZCB0aGF0IG1ldGhvZHMgbGlrZSBgaW5zZXJ0YCwgYHJlbW92ZWAgb3IgYHNvcnRgIHRoYXQgb25seSBtb2RpZnkgdGhlIGxpc3QgaGF2ZSBubyByZXR1cm4gdmFsdWUgcHJpbnRlZCBcXHUyMDEzIHRoZXkgcmV0dXJuIHRoZSBkZWZhdWx0IGBOb25lYC4gMSBUaGlzIGlzIGEgZGVzaWduIHByaW5jaXBsZSBmb3IgYWxsIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIGluIFB5dGhvbi5cXG4uLi5cXG4jIyA1LjMuIFR1cGxlcyBhbmQgU2VxdWVuY2VzXFx1MDBiNlxcblxcbldlIHNhdyB0aGF0IGxpc3RzIGFuZCBzdHJpbmdzIGhhdmUgbWFueSBjb21tb24gcHJvcGVydGllcywgc3VjaCBhcyBpbmRleGluZyBhbmQgc2xpY2luZyBvcGVyYXRpb25zLiBUaGV5IGFyZSB0d28gZXhhbXBsZXMgb2Ygc2VxdWVuY2UgZGF0YSB0eXBlcyAoc2VlIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgbGlzdCwgdHVwbGUsIHJhbmdlKS4gU2luY2UgUHl0aG9uIGlzIGFuIGV2b2x2aW5nIGxhbmd1YWdlLCBvdGhlciBzZXF1ZW5jZSBkYXRhIHR5cGVzIG1heSBiZSBhZGRlZC4gVGhlcmUgaXMgYWxzbyBhbm90aGVyIHN0YW5kYXJkIHNlcXVlbmNlIGRhdGEgdHlwZTogdGhlIHR1cGxlLlxcblxcbkEgdHVwbGUgY29uc2lzdHMgb2YgYSBudW1iZXIgb2YgdmFsdWVzIHNlcGFyYXRlZCBieSBjb21tYXMsIGZvciBpbnN0YW5jZTpcXG4uLi5cXG4+Pj4gIyBUdXBsZXMgYXJlIGltbXV0YWJsZTpcXG4uLi4gdFswXSA9IDg4ODg4XFxuVHJhY2ViYWNrIChtb3N0IHJlY2VudCBjYWxsIGxhc3QpOlxcbiAgRmlsZSBcXFwiPHN0ZGluPlxcXCIsIGxpbmUgMSwgaW4gPG1vZHVsZT5cXG5UeXBlRXJyb3I6ICd0dXBsZScgb2JqZWN0IGRvZXMgbm90IHN1cHBvcnQgaXRlbSBhc3NpZ25tZW50XFxuPj4+ICMgYnV0IHRoZXkgY2FuIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzOlxcbi4uLiB2ID0gKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbj4+PiB2XFxuKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbi4uLlxcbkFzIHlvdSBzZWUsIG9uIG91dHB1dCB0dXBsZXMgYXJlIGFsd2F5cyBlbmNsb3NlZCBpbiBwYXJlbnRoZXNlcywgc28gdGhhdCBuZXN0ZWQgdHVwbGVzIGFyZSBpbnRlcnByZXRlZCBjb3JyZWN0bHk7IHRoZXkgbWF5IGJlIGlucHV0IHdpdGggb3Igd2l0aG91dCBzdXJyb3VuZGluZyBwYXJlbnRoZXNlcywgYWx0aG91Z2ggb2Z0ZW4gcGFyZW50aGVzZXMgYXJlIG5lY2Vzc2FyeSBhbnl3YXkgKGlmIHRoZSB0dXBsZSBpcyBwYXJ0IG9mIGEgbGFyZ2VyIGV4cHJlc3Npb24pLiBJdCBpcyBub3QgcG9zc2libGUgdG8gYXNzaWduIHRvIHRoZSBpbmRpdmlkdWFsIGl0ZW1zIG9mIGEgdHVwbGUsIGhvd2V2ZXIgaXQgaXMgcG9zc2libGUgdG8gY3JlYXRlIHR1cGxlcyB3aGljaCBjb250YWluIG11dGFibGUgb2JqZWN0cywgc3VjaCBhcyBsaXN0cy5cXG5cXG5UaG91Z2ggdHVwbGVzIG1heSBzZWVtIHNpbWlsYXIgdG8gbGlzdHMsIHRoZXkgYXJlIG9mdGVuIHVzZWQgaW4gZGlmZmVyZW50IHNpdHVhdGlvbnMgYW5kIGZvciBkaWZmZXJlbnQgcHVycG9zZXMuIFR1cGxlcyBhcmUgaW1tdXRhYmxlLCBhbmQgdXN1YWxseSBjb250YWluIGEgaGV0ZXJvZ2VuZW91cyBzZXF1ZW5jZSBvZiBlbGVtZW50cyB0aGF0IGFyZSBhY2Nlc3NlZCB2aWEgdW5wYWNraW5nIChzZWUgbGF0ZXIgaW4gdGhpcyBzZWN0aW9uKSBvciBpbmRleGluZyAob3IgZXZlbiBieSBhdHRyaWJ1dGUgaW4gdGhlIGNhc2Ugb2YgYG5hbWVkdHVwbGVzYCkuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG4jIyA1LjQuXFxuLi4uXFxuLiBTZXQgb2JqZWN0c1xcbi4uLlxcbiMjIDUuNS4gRGljdGlvbmFyaWVzXFx1MDBiNlxcblxcbkFub3RoZXIgdXNlZnVsIGRhdGEgdHlwZSBidWlsdCBpbnRvIFB5dGhvbiBpcyB0aGUgZGljdGlvbmFyeSAoc2VlIE1hcHBpbmcgVHlwZXMgXFx1MjAxNCBkaWN0KS4gRGljdGlvbmFyaWVzIGFyZSBzb21ldGltZXMgZm91bmQgaW4gb3RoZXIgbGFuZ3VhZ2VzIGFzIFxcdTIwMWNhc3NvY2lhdGl2ZSBtZW1vcmllc1xcdTIwMWQgb3IgXFx1MjAxY2Fzc29jaWF0aXZlIGFycmF5c1xcdTIwMWQuIFVubGlrZSBzZXF1ZW5jZXMsIHdoaWNoIGFyZSBpbmRleGVkIGJ5IGEgcmFuZ2Ugb2YgbnVtYmVycywgZGljdGlvbmFyaWVzIGFyZSBpbmRleGVkIGJ5IGtleXMsIHdoaWNoIGNhbiBiZSBhbnkgaW1tdXRhYmxlIHR5cGU7IHN0cmluZ3MgYW5kIG51bWJlcnMgY2FuIGFsd2F5cyBiZSBrZXlzLiBUdXBsZXMgY2FuIGJlIHVzZWQgYXMga2V5cyBpZiB0aGV5IGNvbnRhaW4gb25seSBzdHJpbmdzLCBudW1iZXJzLCBvciB0dXBsZXM7IGlmIGEgdHVwbGUgY29udGFpbnMgYW55IG11dGFibGUgb2JqZWN0IGVpdGhlciBkaXJlY3RseSBvciBpbmRpcmVjdGx5LCBpdCBjYW5ub3QgYmUgdXNlZCBhcyBhIGtleS4gWW91IGNhblxcdTIwMTl0IHVzZSBsaXN0cyBhcyBrZXlzLCBzaW5jZSBsaXN0cyBjYW4gYmUgbW9kaWZpZWQgaW4gcGxhY2UgdXNpbmcgaW5kZXggYXNzaWdubWVudHMsIHNsaWNlIGFzc2lnbm1lbnRzLCBvciBtZXRob2RzIGxpa2UgYGFwcGVuZCgpYCBhbmQgYGV4dGVuZCgpYC5cIl19XSIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ3ZWJfc2VhcmNoIiwiaWQiOiI2ZTUxMjVhYy0zMTY0LTRhNzYtOWM1Mi1hYzM0NjBjZTM2ZTQiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJzdGF0dXMiOiJzdWNjZXNzIn0seyJjb250ZW50IjpbeyJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3NrZXB0aWMiLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QTZGNGNDTjV5aEQ0Q3JzOU0iLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjB9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InJlc2VhcmNoZXIiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtM2NkOC03OWYxLTg4YTEtY2U2Y2VlMjk0ZTc5LTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoidHJhbnNmZXJfdG9fc2tlcHRpYyIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsInRvdGFsX3Rva2VucyI6NzQxMSwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fSx7ImNvbnRlbnQiOiJTdWNjZXNzZnVsbHkgdHJhbnNmZXJyZWQgdG8gc2tlcHRpYyIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ0cmFuc2Zlcl90b19za2VwdGljIiwiaWQiOiIzODkwNThkMS0wODNmLTQ2MzItYmJjNS0wOWJmZDNhMGZmZDIiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMVhqajNhUmRXeWhCSmZQbVRKQnhRYTkiLCJzdGF0dXMiOiJzdWNjZXNzIn0seyJjb250ZW50IjpbeyJzaWduYXR1cmUiOiJDQVFTMVFnS0VBZ1NHQUk0QVVJSWRHaHBibXRwYm1jU0RONlRmVTZORE05NzFsRWJ6eG9NdlJVQTFKOW94M2ZKUVlUOUlqQXJDUVFHSEJQNHBSSFBhdlh6UmxiTWJhWUhDNTdZaitHYnhncjBoZWx2ZDJPSUwvbzZSYnQ0UEZBM0ZhSUZVbGdxOGdjN0tRSTlGdEdudlNBa0xCUnpJc1VwLzJkNVJkMnpnRXNma0ZWZVhzbjhXWVlaa2luQVFWVnk2QTdCM0xhRUpXMHhiL3g4OERpNjdmZWR3eDBRK0ZsWnR3Y21UM0RjYWExMUp2ZXlIUnIzZG0rekdTWlA2Q1hlWE5KOG5yVFo3VzFZUlR5c0Q2dzdvN0xwOGgxUndxNGY0bzhqK1FVZlkwdmplK3pKWnN4SFBtUU50M3lrZUtoOTloZ242ZGJpejYvRUhxbWJIalNBdTFNNFNxRURmcUlLVGhIWWVuYW1BWm1jUTRYQU9vNm1kUTYrNzdWZ3BWMWxDMS9rNTJJaVVvSmprR2xRcm5MQ3Z1dGlJN0tOK3NnSzhjMisyS0NKclBSQ3ZWVitCa1N0dlA1aldXcXp5dHBLUStuSHpoTDQ0OE9ySUE5Z2RKQzM5QzhlRHJYZVJuSTd0NEZxbGtYR29NbFpBVjlVTWNyUUhSeE9NaTh0T3lUemk5b3lCRkRUU3NRK0xKcWVyMHR6MUZ0OWdlazZ2emc1MFNwVkFGUWRMRVJ2OFIxWXFNYWVJK3hzdlRBRVc5TVJndlJNa3pMdXpSVDJndXczakQrRmppcDM3eGdLNVFJMFIrQ3NPQW1RUG9UWml6emNJeUFVQXVDSkZDZ3EvZ0QvQ2liTHhveW1pVlVJMWFuOVNudDNLcE5Sb1dkdldJbmUzM1YwcHY4UEhnOXlXMkp1TE9xUUdpbExDc0svUTNvUXIzdXBFTVh4WEdqQ0w3V1VpenZSWkdGRUVWK1ZwbFl0N3Y0dEtaTWo1cGszdmRlTTBNSjVYWmc3UHZVc1J6eWN4Y25yVDNLbm9jM3FNK3R0cVpmVHNiOGQ2VS9wUWlmcTdHdWFoRk9IcnZyb3FnSkxqKy84MVl5MXBaRmtqZkpnWTBYa2x6QW5BNkNGcS9ZVDBneGJWK1RINUlTK1dhWU9VekszNlM4czVxcU13aG5ndjRJekUzY3RYWHFuaElaWE56bnlWQnFiWWEvR3gyYkNqY0VqMm41UnlHWVJTLytNY3VsZ0xCem5ROU1rRzJEN1FQM01xL253SFhLTUlhTFM4Vks4aEczZDByQWpsdm5hdVhTaE5ySnh0a2o0cWt3bnhENExyRWprMXVlK1BMY2JMcVlXd0FyOG4wcys0MWpyc0lQVnl5Szl2RmFFWnJBOUthQnhrSnlpcXg1MEdaTm43Y2t4K21rSTE4Z0g1Mml5VlB0VThGS01YanVnQ2w2bVBMaUhZaURRZCtHNjNMdWRtT0t5czlpdFZuQkRCTk1QMmIvSTcrYklWUmxoZUdhWldyNk1BVEtsODZCM3krMGt1T3JMUk15V2NkaWJjMmlRa1ptKzhmN2RUVXdiS2JCSnNLT0ZZNzMvOG5sTC9KaVlwano2blVJaytJcVNqYUZ5SzZYRmxDY3VrV3BXSHBESm5TdmJFemV5N3FIeUNxSitJK29uOGlQOEdPdGVtcFFhUzAvNGtaR1N3bHJFNnhPajZFd0dwZmpiRFpLSGtQLzJSQWROWVBRcTN6N2JkVkpNSk5uRk16NmUzN216TXBDQmlod1BkU0ZUUkUzU0NZNjZ3dlRnSnpHQ1V5aEhiU2k1aTFhenpPWGQ4dUhDU3BULzZMMW82VlUwcGpvUjhLYWE0d0NXSEdmN1AvV0VkK2F2eXAwMkpudTFjbXBXMCtHcSt2SzloMHRkYjJJQ2xNL2VOZmRvdVFQK2VwOE1KVWpyYURSSUNtQUpRWTVvYlM3MXZuaU9vK3JlYUZ5MHpSZ0IiLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsic2lnbmF0dXJlIjoiQ0FRUy9SRUtFUWdTR0FJNEFVSUpibUZ5Y21GMGFXOXVFZ3dGaDNBVkpaUDNzOHkxSHEwYURHNzZ1aEhPTmFTY1hBR1NoaUl3R2dqTFhIVXgxeVpya0xQbGpqelRPUXljcExKaUtWd1hjeG0vRXpLSm0xczhNNVd4dVJFV0VsZFhjb2FOMC9tc0twa1JNVCtlTGI1aWg5ckFIVUxUb21VTnNVd0pldTBqckplNEdMYXp5N3NQNUt0akVPRUJLUXNYVWZvUTdmRGtaT1FVSFppOXp6UU40aEZObVlKVWYzT1ZxZjlUUU5kQzErbS8vSEFaUGZXbUF5Nkh0RHZtN2tnQkprQ0VvaUZubTd0bi9QWDFoWEdQUmdXYzZCVFhVTXIyV0E4YUt5cWdnUlViTUgyK0pZdjVpV1pkZENjMXVwMU5icjNzRjdXUGNvdDlaQmh5SFFYcjY5MnplSms2U0pneWdJVnJBU2JNbHkyYkJ4VFdKQjYyM2dWZ3pwQ21WU0xaRnBrS1hnUGNwOGhHeURLT3ZGNlNpUVNIcUdSeFFvZnI3VnMxYVBoZlppeFJPSkdEK01scmtnZ1ZBbkdNeVlJRFVacGF2dENlaVUxSm1CU3owMDg0YlhQdFQvdG5wc0tPblNQWVpRbzNCMEJYS2JxQS9Xc0tuZzgvVjNaOFV1YWpvNklWWjJoZ0ExV0RkdXFRT1FrRXhHVTZwRnVMaGViSWp1V3hTTkkvRGZmdlIvL0xOcUNmdmRuZjBodm9QdER4ZngvU2F3SXg0L3NwcTl4YndGekh0TGdvSGcxRjJ3Rnc3Y2xQRGxnMVB5RC92MnlKRzNWOVQ4R0w2akdQRTZyaGZpbC91NEhOdmY3QlcwZFF6Q1RUWGNxWnk0bkJ5eTArcCtTN1p4Zk1sNEtweGEyZ09oaFNOamZwZnVYMFFwTDRrTUU4SjFubGlGTjFkOEtxRVB1REkxOEJRSFVoRC9VZlkyOW9BMHg1MTBGak0vQnNMWjRuNXNwcGtvSWk3Q0ZvUXEyYUFLVk5mQzFLaWxoSXBSRCsxZHlEdFpRa2xOOGdacE9FU0h3Nk1MMVR0VTB5ZkFjYm1xb3hKNmhoanp3SDZDTXNUUUVFeThNYlJCTStRS0trSVJkUkNQSEdGVzJMa3VSNTArSjBwRi90cjZwcVZ2SmVvemZpTW5yRWlsQjI3VFpQNG5LVEpHOHJkLy92Q1dlbjB5N0NBLzhkQko5Z2RwNXR4N05WQTQ1azFnZGN4RVIxZ1lvdU5YM24xdkczL0IxRjQrOTN3WFc5TXFvNlVzNTlUUW1yYjlOcWE3cFNRTlNySDA2dzFHeHlCQWxrRmZxQWxhZmsxNmFNcHE0MTZUSllwR3k0M1dDVWlDY0xXbGhpV2d6MnNnT0xBWFA5YTV3ZVdBS1JDOEo2amtjVWtEZVhuVytIRlNpUE02KzlCbzZrVmhMVWhONXJjNG1IWk9acEY3cHRQcHNRTjRYQ3FMUHNxcFJILzhvNVhweUM2Wm5lVGVGMkV4dGpod1pWalhEd01iSXpWWUlidzF5MlBQR3c4ZEM3cGM0c0lWU25JNG9RQUgwdjY1MGdoZmhtTWV2V1c0aDRQeWxidy9ERzRmZGVUMys4ejBmOE1PU1R0ZmlweDA1L1F0dEhsQXZPRFJ4YTV5REN2T3o2bjNmMUJPME4vNE1iR3lPTTI3cVc0eDhtQlZvcnlVWldmc21oZmkwL0dmTnZCOCt5bFRHYllOWDI3T2NFRjVXWnROcWJXRXk3VjVHYm1aVVN4c3BjZzdGQlozajMvM0w3bXV2WlhvWmkxZjVlcnE0eTNsU0grb0Y3cVBGbnZpZXlMT0N3dWJjUXkrb3lvd2o0Y0QwaWRSZWNucCtzKzUyRDcrbXpGMWdwVytmK2NKYXNveGlnYnhrT2xvUzR5ZWlLdmdNNG5XU3BHRWoxMlJQeDVvODBFbXppWE45dDR6SnpXMWh0QTVoWEpKZUZ5bXBNWXR5Q1VDZXZSV1ZtclJZa3RRN1VuQ2E2dEwvQVZQaTFuaTVYQ1hIZkxPdXFmSzVLTFl4andWdGRoaGZtQ0x2cGFLVk5DRU9DZjRuTkdIcUQ5OHdVYjBlaldOd0RZQm5KR2tCUnc5UlZzcGZ3TmRvV29WY24ycXY4eXdHVXA3eWMvc2syTHBJNlZ6aVJ5dldxYkdORzFaSnFWVnV0UmM0NitzT0U0VlNlUzIvQ1ROS05hNk5NQ2YvaHh4YVVWc1lmS2RQK1MxTGM0aWFuS3Awc1V1Z1BwbXRveWRDMENQMlJ3am5ITGNuenhDbnZxeGxMUVdYTU5QVHN1MnlCY0hsV0pMdGNtZGcvTWNMYWd5N2w1a0c2c043WnhKTEQva0F4OEU2dXFvaFArSy9qTVNyWXVRSm5ocjR0ZjRiaVdGWTNKemYvS3BBWXlWU3ErcGJmRktYTWJMOW5IRW9aUVQxNVd5YWJkNkRNQXEzUVpPdGhNNXBVemROcVVYa2NySTFQd2pBU0FnNGRWVS8ybys5OG1UWG55aFA3VmxFa1RuZGRwd3lKRitZQ0NjMDFwcWE2Wlg5OXBuUitZT3BJcmI1RHRQY0txMHQ1Y0ZDRHlReE4xbFV1cVpjWXVqdlgvR0M3KzZJcFJUei9Oa0VZLzVjbnRPZlpKeWQ2U2tVeC9KUjVBSklSUENOZWliMHJjV25uRm4wWEs4czIyeDQwd0xqRXR1ZlhETzkreXJhZjRrSHhsQkkzU0IwbWVNamVwWDUwQ0poQXg3TmVra1JGbndvZWNMZnFJeXJqWHJnQVpzVE1QaVg5NHNuamRyemlzQVh0QkthbnVsaWxTeE9mVzJIU21mbmswTDJHWEhKSjNTK3ZDYmdSRW1sOWRla2ZtSGhtd0VZdHYza2ZodGg3RVJJd1ZUOGo2OU90SUJDbWVUSUp1QTdvNzNQWGMxTEoyd2h1Wks3RlplNk5UT3M2MU1QN0pWbEljNnRiaHRPQmszTlhVOHBCUE9DWTZRNTVkNjNGdHBVVS9LQ2JRMEsyV1E5bzVqTkd3Tk5KNkN3NUZEQ3QreDVZSmVQMWQyUXpEU2lxK2NTWnF3cUNaVmNOb0FmdEdHemdyUlo0dmtwYlAweFFaSEN0V3NVd0hWVGpyWTROZlJEakhkVlBSbjhKOVh0b29jL2hJV2txekZhY3pZVGxhcHUyUkNKcWU4YUVvM2p6V1pDRUo3QWlCekJZcmFZRGdPZmVpUXllSlVzYmJYSGRHS2lpdzIyQmRwNE0rWXo2NzFNWjBpZTlFV1JrczBRMkRIR1VUbmU5UzR0QUFKUEdiVWY0eHRDTW9KNDBRNFFXMDVRTFNaeTN3TkZBVUZZbVhFNEVDREZCOCtJRWVORkZGL3RnQW5maDhHdkZoLzF4WStMRStvek1taW5qU1J3aHEwOXpRQUVrTkZ5dTBJS2d2RGt5T0kraWNjRjdkb1dMVFVhc1B4TEgxeEdQQlNLa2VXQ25Tb1gvNEMyYzlkdTY2QmJSWGFtSHFhZStWQ2lTNkJOOTUzaVlLT3U4ck1rbmVXK1RGalYwQ3pBZjhQTkc5TEJheVVnWTVVdzl4MFNhcDZ6STRQcGFaS0g0ZDlrdGtGVSs5MXlCYk41U1J0ZGQ4L0VRYXRKWG1CTmZ3N2tScmxsK0xqd1p3ZVdMRWIwalQxUnBHdURxSExkTFRNTWdvd1NjenJwMnpDc0pkUUgvc2VhWXJwd2tDRlI4ZFJqTTdKUlVrVmExQTBTdmRaQVg3NXZ1QnZ2TEJ6TjJjK1JidDVvcXk5M2xxSUJJaktiN3cyVWtSVGI0cEJXY05LTlBVZ3QzYXovUHhReEVhc2FsdGRtdXl2c1hZYnVyM1ZQTm44SnpOLzZKNU4rUUNuejU3MVRjY2FwQ21rUHZGRStnYXlYVStVRjFWS1l5RmNVdmxldjdsc1FsbWpxeksyUUE4WDdRM1R6RmFwcWh2SktsdDdkekJIcmFxWkJnUGdiOU40b0tOYXlpOEZaVDcxK2thWWhJaEZyRVNLWVJIZkhrajVuM2tmaGdMeSt4K2pMRGJLeE1nZ2tnSG5KejlSdUVIWlhCV3RrWUFRPT0iLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsiaWQiOiJ0b29sdV8wMUg2ZlY4SnZSSk52WHk0N2VSbmQzakgiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6e30sIm5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHdBTVJkUzIxRmVKZnpRaE0zcyIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo3Mzk2LCJvdXRwdXRfdG9rZW5zIjo3NzAsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjczOX0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoic2tlcHRpYyIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy00YWU5LTc2YzAtODlhNi00ZjBiMTE0ZTMwMzAtMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzM5Niwib3V0cHV0X3Rva2VucyI6NzcwLCJ0b3RhbF90b2tlbnMiOjgxNjYsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjczOX19fSx7ImNvbnRlbnQiOiJTdWNjZXNzZnVsbHkgdHJhbnNmZXJyZWQgdG8gdmVyaWZpZXIiLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7fSwidHlwZSI6InRvb2wiLCJuYW1lIjoidHJhbnNmZXJfdG9fdmVyaWZpZXIiLCJpZCI6ImMwNjQ1NjNhLTk3NGYtNGYwZi1hYjRhLTA4MGU2M2MwMWNmNSIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsInN0YXR1cyI6InN1Y2Nlc3MifSx7ImNvbnRlbnQiOlt7InNpZ25hdHVyZSI6IkNBUVM5Z1VLRUFnU0dBSTRBVUlJZEdocGJtdHBibWNTREdGeUoxTVhvWStBaTZOa2J4b01SZDJUclVmVVU3V3JQcEEySWpEZ3k4ZFVwbGNaS2M3RG5Kd05MVEJmSDVVTlIrbkduYWtTUFFpYThVVkl6cmprT25KMmZMcldVc0Eyb0VPOHlSc3Frd1hra0RDc3loQjk3ZHdqNTdSTXQ5MVh6Z1doTDlsdU5FR085MnlXMHJORkdvTVNVVk94UElyRmM1Ump5cmhLRWMzKzFSbnB4Q2t5bHErTE5sUTVORnhMbjZ2MmNGNUR6cXB2QkhOUjdYQWlnK3VySklVM2dYeWRDa2FHWlpEZXRKdFFPVU9YZ1ppOXo5MTAwTnBkQUVrWERvMklDSENHVEdwVmMwSi9wYlh4V1JCeGI2QjEvR2wwWWpuNjhWNkhsZXNIN0lvQzdHUi8zSHdSdTlOU0RCRzR0dGZRakVXY2crUGVBZWE0Um43anp4K1RtOUg3TDFqb1RLVmdEYlFnNWlLTUxmanZQcWxua3NDTHRYT3Q4alNJUzVPWUxON2EvN1RYNi93NWQzNHZudzh2UERtVTFVWWkwUTcycVVXaFVhMnZ3OE9YTnBjS0w3QnAwbE5mQlM4cnZIWSt2Qlk4L2hhQW9UTDN3dkxGVTFmVCs0ei92QTNCY3NpZ1pEWWxmSmVEWWN4WG1SdXF4enhFR3BYQ1JlOXMxSFd0SUQrNDFwSHU2bHBaSGtYYWdocGM2VFpITHhOd2NWaFRiNmZNUTdRbysyTDZQdmtsaDFtMzJwZW9keGVnMllpUGhkcGwzdy94a09FdTUvSkVGN1pCeHBkVUwrdTN3a081cFVsc3FmZ1g3dWh6djdIQzJXcFNXVmltcndpUXdRT1V4MCtKUHJMdHcrbFlmcTdJM2pydW8wR1cwZ3VXdm51ZjdKTTNZTGo4NHErSmdIb21uU1Nabm5abXJMMFVhUnZyVnlMbzg3N0YzOWNjTFM3WG9abm51aThKNFA1OFpUbWlMa0VKZUdjeDFJdWFnMmUzMzMzOU83WWV3LzU5dExiUXRJR2g5VldNd0hCbkZ4MzlTOC80TEVGcks1SGxkUFFoT3Jsb3lUdGtSczY2aVFSS3R4TUtTMFZJNlFUS1pEQWJzN2E3SDF5QnYrbUppOVhnNHVLbXlVNnFubjd6RUtqalkyYklsQ3dleng0TExHUDVIRDNacjZmWnNtZEcxVUh6RHNCdllHMm9DSTBvcUVLTSt2YWxzVDZoVThNUmpiUUJEL0pKOEpKTi9pQkdsaEJBVUhvMFVtaGh0UjJldVRWcFpXbE1uaG1wWEFkS0RYU2ZQZGFQb2hnQiIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJpZCI6InRvb2x1XzAxWEVBdWhRSENCZHlDd3R5bVp3RmZEaSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3JlZF90ZWFtIiwidHlwZSI6InRvb2xfdXNlIiwidG9vbHNldF9uYW1lIjpudWxsfV0sImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2Zkd0FzYWJEcXVlZ0hpNllVcENuIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjc1MDksIm91dHB1dF90b2tlbnMiOjg4LCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjo1NX0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoidmVyaWZpZXIiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtNjY4Mi03ZDIxLThjMTUtZmZiY2U5NDIyYzlmLTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoidHJhbnNmZXJfdG9fcmVkX3RlYW0iLCJhcmdzIjp7fSwiaWQiOiJ0b29sdV8wMVhFQXVoUUhDQmR5Q3d0eW1ad0ZmRGkiLCJ0eXBlIjoidG9vbF9jYWxsIn1dLCJpbnZhbGlkX3Rvb2xfY2FsbHMiOltdLCJ1c2FnZV9tZXRhZGF0YSI6eyJpbnB1dF90b2tlbnMiOjc1MDksIm91dHB1dF90b2tlbnMiOjg4LCJ0b3RhbF90b2tlbnMiOjc1OTcsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjU1fX19LHsiY29udGVudCI6IlN1Y2Nlc3NmdWxseSB0cmFuc2ZlcnJlZCB0byByZWRfdGVhbSIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ0cmFuc2Zlcl90b19yZWRfdGVhbSIsImlkIjoiOTgyYzAxMjYtZTZhYi00NThhLTkyMmUtN2IyNjZlODNjMmIyIiwidG9vbF9jYWxsX2lkIjoidG9vbHVfMDFYRUF1aFFIQ0JkeUN3dHltWndGZkRpIiwic3RhdHVzIjoic3VjY2VzcyJ9LHsiY29udGVudCI6W3sic2lnbmF0dXJlIjoiQ0FRU3dnY0tFQWdTR0FJNEFVSUlkR2hwYm10cGJtY1NET1JYZ2U5dUwxODMyUk1Rd0JvTWpRd0F3QUhiOWlQMW9qUEdJakREU2NhbDQ4NmJlQ0pidEc4L0QyWTlnOVM1eElQZmpvSGcvZHh3WDVIeXBTUUVCYmpGaVRTYlBFQ1hTRnpWNllJcTN3YkxjbXlQYnZ1c0duRGV4Rmd4KzNzSVh6dnF5RlYvNjl4aGloTW5TcGZUQ0h5MCswelN1MVdwWVUvV1VwSWZvYVNYVGNUYTFxa004eDhFay95LzFqa0tNWlVmREZhOTN1aFVKeE9DNCtRZ1BXQmwvK1h5MnQ4MkwyeC9lYnE3UUxRVHF0UjBqWGVhVVJRbTNDbzZKdk9nV21Edk1wRWRUZVRXRTNmRGgxdHp6VktzZmx1bTRkODliNkNhS3RuOG5sUWQ3UmhOOTl2TlFNUUhHNVY0T3VWTmRySFBaWnBGT3VoZGVqc01GV1h5Qis1NXNsR0FkOHdFWGdGS2JJL2tuT1hWR3pML05ERjhHdHkxdE5abHUyZmRTQ0FzWVdNc0ZRUDFjYndMZVFHc3p5WHpKTVZaSjJJOXhCUGt5ejFIT1hDUHloNUYrajJnUlRQaWtWQjNBUkM2dGZCL2xEU1VlS0JWWlBRUzhwWS9CNGVDcytJamphQk5XcnVMemZxQ3NjNnhMMEN1UVBTWXlRWGFZRmpZclNSTmNDNzBXUUJXVDVWVmhwMG1BOXlaREdZYlJadDlJNU9LOVFrUW11SjV6REZhTUVPWHpTcnNDNGx0ZVA2TWRpbWlNaWp2c0gzaXErYzZXVVlvbFJqM25lSTg4RWFISXpMQUdScTY0Qk5DTHpkQVlMMnBFVTJzYjV1Rzc4UEtWb1VVQ2JWSkw1anBET2hMT1RPMWJRc1R1b1VwblJSckxRRC9rSXpWK2xqcFZCd3dmSmN5cTc1VEpBL0N0UHIveHIyU1o5QjFiMnNiakc2akQwbGh0b0NkZGVEZ2NxZ2wvTFNTa2N1YVlZOHhQV0N5K0x0TmhjcVRoZm1mdm5ITEpJcEw2Z2pialcvT1FwUmZWVXdmdzFqT2I5YldWd2NnSGhOWWZLWUdQdE04b3RoNXh3TFlSUlZnenFNS092czdmYWxYUmhWeloyU1A2SXcwQk1tQUFaNkhqZkNsOUwrdFBxSk5sblFRT0hwb1dqdW00QitYc1lsZEZFS0dRV1Fxc2Q1OWlOUHZhbzlwL3NDT3Q3ckdxQ296Y21naUVtNHc2NmV6L1FJRHliNDZuMCt0Y05KUFVjb0JLdlEvakR3RTZWd21rUS9uRUNqRnAzVWREVCtrV1lRU05kOGo4VkJaT3BHOExscVVZckVaQ3YwcTdCczF2Y05yZFlpYUZvUzMyb3NheW5XaytIdDNPSHVGV2JFRjhKbHNGVFFBTzA1VnZZa2VjZjBWL29USHNHNWh5T09zN1VFYURGcTd1YUp1eldWaGg1cEFvaklhQmNSOGZWbG4yUExSVFg3aXMybnNBK1FyZGlkZVBjWnRGdGd4QWlLWWUxaTJzU0ZpNHZtOEVPa0NiUTYzd2RRZVk0VXJyNVB1b01Fd3hVVUxqZ0thZ2lBbXNKRHE0UnJWbTdzTzQrNll1QTlqNmwydXlsa09FYzY3dmYvNWZrWHVDc2lBcE5MNmtlYjhjK3ZMMG40RlhHZHpqK2FwU0RmRkdCZ0IiLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsiaWQiOiJ0b29sdV8wMVZCcldkV0VXWWdpdDRrTlllaEJNVTUiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6e30sIm5hbWUiOiJ0cmFuc2Zlcl90b19lZGl0b3IiLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QXpmanFVb3BvRW1aTGFxbTEiLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzQxNiwib3V0cHV0X3Rva2VucyI6MTUxLCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjoxMTl9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InJlZF90ZWFtIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTZjZTYtNzI1MS1iNzk2LTRmNjQzMzNmNjdjNi0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6InRyYW5zZmVyX3RvX2VkaXRvciIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxVkJyV2RXRVdZZ2l0NGtOWWVoQk1VNSIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzQxNiwib3V0cHV0X3Rva2VucyI6MTUxLCJ0b3RhbF90b2tlbnMiOjc1NjcsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjExOX19fSx7ImNvbnRlbnQiOiJTdWNjZXNzZnVsbHkgdHJhbnNmZXJyZWQgdG8gZWRpdG9yIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJ0b29sIiwibmFtZSI6InRyYW5zZmVyX3RvX2VkaXRvciIsImlkIjoiNWQzOWUzNmItYzViNS00NDI2LWE5YWEtYmUyZjdkMzgxNmQ2IiwidG9vbF9jYWxsX2lkIjoidG9vbHVfMDFWQnJXZFdFV1lnaXQ0a05ZZWhCTVU1Iiwic3RhdHVzIjoic3VjY2VzcyJ9XSwiYWN0aXZlX2FnZW50IjoiZWRpdG9yIn0=" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W3siY29udGVudCI6IkZvciBhIHN5bnRoZXRpYyB0cmFjaW5nIGZpeHR1cmUsIGV4cGxhaW4gUHl0aG9uIHR1cGxlIHZlcnN1cyBsaXN0IGltbXV0YWJpbGl0eSBpbiBhdCBtb3N0IDEyMCB3b3Jkcy4gVXNlIG9ubHkgb2ZmaWNpYWwgUHl0aG9uIGRvY3VtZW50YXRpb24gaWYgeW91IHNlYXJjaC4gSGF2ZSB0aGUgc2tlcHRpYyBhbmQgdmVyaWZpZXIgY2hlY2sgaXQsIHRoZW4gaGF2ZSB0aGUgZWRpdG9yIHdyaXRlIC9hbnN3ZXIubWQuIEtlZXAgdGhlIHJ1biBzaG9ydCBhbmQgZG8gbm90IGFzayBmb2xsb3ctdXAgcXVlc3Rpb25zLiIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoiaHVtYW4iLCJpZCI6IjQ2MjA0MzA1LWY4ZWUtNDM4NC05Y2U4LWZjZDgwYmZjZGY3NSJ9LHsiY29udGVudCI6W3siaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9LCJuYW1lIjoid2ViX3NlYXJjaCIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHc5YVc5YnJlblBxZ3liZk53UCIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjoyODQ1LCJvdXRwdXRfdG9rZW5zIjo3Mywib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6MH0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoicmVzZWFyY2hlciIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy0yMWEzLTcwMDAtYTM4MS0yODRhNmEwYzAzZjctMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ3ZWJfc2VhcmNoIiwiYXJncyI6eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9LCJpZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6Mjg0NSwib3V0cHV0X3Rva2VucyI6NzMsInRvdGFsX3Rva2VucyI6MjkxOCwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fSx7ImNvbnRlbnQiOiJbe1widGl0bGVcIjogXCI1LiBEYXRhIFN0cnVjdHVyZXMgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3R1dG9yaWFsL2RhdGFzdHJ1Y3R1cmVzLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcIllvdSBtaWdodCBoYXZlIG5vdGljZWQgdGhhdCBtZXRob2RzIGxpa2VgaW5zZXJ0YCxgcmVtb3ZlYCBvcmBzb3J0YCB0aGF0IG9ubHkgbW9kaWZ5IHRoZSBsaXN0IGhhdmUgbm8gcmV0dXJuIHZhbHVlIHByaW50ZWQgXFx1MjAxMyB0aGV5IHJldHVybiB0aGUgZGVmYXVsdGBOb25lYC4gWzFdIFRoaXMgaXMgYSBkZXNpZ24gcHJpbmNpcGxlIGZvciBhbGwgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgaW4gUHl0aG9uLlxcbi4uLlxcbiMjIDUuMy4gVHVwbGVzIGFuZCBTZXF1ZW5jZXNcXHUwMGI2XFxuXFxuV2Ugc2F3IHRoYXQgbGlzdHMgYW5kIHN0cmluZ3MgaGF2ZSBtYW55IGNvbW1vbiBwcm9wZXJ0aWVzLCBzdWNoIGFzIGluZGV4aW5nIGFuZCBzbGljaW5nIG9wZXJhdGlvbnMuIFRoZXkgYXJlIHR3byBleGFtcGxlcyBvZiBzZXF1ZW5jZSBkYXRhIHR5cGVzIChzZWUgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBsaXN0LCB0dXBsZSwgcmFuZ2UpLiBTaW5jZSBQeXRob24gaXMgYW4gZXZvbHZpbmcgbGFuZ3VhZ2UsIG90aGVyIHNlcXVlbmNlIGRhdGEgdHlwZXMgbWF5IGJlIGFkZGVkLiBUaGVyZSBpcyBhbHNvIGFub3RoZXIgc3RhbmRhcmQgc2VxdWVuY2UgZGF0YSB0eXBlOiB0aGUgdHVwbGUuXFxuXFxuQSB0dXBsZSBjb25zaXN0cyBvZiBhIG51bWJlciBvZiB2YWx1ZXMgc2VwYXJhdGVkIGJ5IGNvbW1hcywgZm9yIGluc3RhbmNlOlxcbi4uLlxcbj4+PiAjIFR1cGxlcyBhcmUgaW1tdXRhYmxlOlxcbj4+PiB0WzBdID0gODg4ODhcXG5UcmFjZWJhY2sgKG1vc3QgcmVjZW50IGNhbGwgbGFzdCk6XFxuICBGaWxlIFxcXCI8c3RkaW4+XFxcIiwgbGluZSAxLCBpbiA8bW9kdWxlPlxcblR5cGVFcnJvcjogJ3R1cGxlJyBvYmplY3QgZG9lcyBub3Qgc3VwcG9ydCBpdGVtIGFzc2lnbm1lbnRcXG4+Pj4gIyBidXQgdGhleSBjYW4gY29udGFpbiBtdXRhYmxlIG9iamVjdHM6XFxuPj4+IHYgPSAoWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuPj4+IHZcXG4oWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuLi4uXFxuQXMgeW91IHNlZSwgb24gb3V0cHV0IHR1cGxlcyBhcmUgYWx3YXlzIGVuY2xvc2VkIGluIHBhcmVudGhlc2VzLCBzbyB0aGF0IG5lc3RlZCB0dXBsZXMgYXJlIGludGVycHJldGVkIGNvcnJlY3RseTsgdGhleSBtYXkgYmUgaW5wdXQgd2l0aCBvciB3aXRob3V0IHN1cnJvdW5kaW5nIHBhcmVudGhlc2VzLCBhbHRob3VnaCBvZnRlbiBwYXJlbnRoZXNlcyBhcmUgbmVjZXNzYXJ5IGFueXdheSAoaWYgdGhlIHR1cGxlIGlzIHBhcnQgb2YgYSBsYXJnZXIgZXhwcmVzc2lvbikuIEl0IGlzIG5vdCBwb3NzaWJsZSB0byBhc3NpZ24gdG8gdGhlIGluZGl2aWR1YWwgaXRlbXMgb2YgYSB0dXBsZSwgaG93ZXZlciBpdCBpcyBwb3NzaWJsZSB0byBjcmVhdGUgdHVwbGVzIHdoaWNoIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzLCBzdWNoIGFzIGxpc3RzLlxcblxcblRob3VnaCB0dXBsZXMgbWF5IHNlZW0gc2ltaWxhciB0byBsaXN0cywgdGhleSBhcmUgb2Z0ZW4gdXNlZCBpbiBkaWZmZXJlbnQgc2l0dWF0aW9ucyBhbmQgZm9yIGRpZmZlcmVudCBwdXJwb3Nlcy4gVHVwbGVzIGFyZSBpbW11dGFibGUsIGFuZCB1c3VhbGx5IGNvbnRhaW4gYSBoZXRlcm9nZW5lb3VzIHNlcXVlbmNlIG9mIGVsZW1lbnRzIHRoYXQgYXJlIGFjY2Vzc2VkIHZpYSB1bnBhY2tpbmcgKHNlZSBsYXRlciBpbiB0aGlzIHNlY3Rpb24pIG9yIGluZGV4aW5nIChvciBldmVuIGJ5IGF0dHJpYnV0ZSBpbiB0aGUgY2FzZSBvZiBuYW1lZHR1cGxlcykuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG5Bbm90aGVyIHVzZWZ1bCBkYXRhIHR5cGUgYnVpbHQgaW50byBQeXRob24gaXMgdGhlIGRpY3Rpb25hcnkgKHNlZSBNYXBwaW5nIFR5cGVzIFxcdTIwMTQgZGljdCkuIERpY3Rpb25hcmllcyBhcmUgc29tZXRpbWVzIGZvdW5kIGluIG90aGVyIGxhbmd1YWdlcyBhcyBcXHUyMDFjYXNzb2NpYXRpdmUgbWVtb3JpZXNcXHUyMDFkIG9yIFxcdTIwMWNhc3NvY2lhdGl2ZSBhcnJheXNcXHUyMDFkLiBVbmxpa2Ugc2VxdWVuY2VzLCB3aGljaCBhcmUgaW5kZXhlZCBieSBhIHJhbmdlIG9mIG51bWJlcnMsIGRpY3Rpb25hcmllcyBhcmUgaW5kZXhlZCBieSBrZXlzLCB3aGljaCBjYW4gYmUgYW55IGltbXV0YWJsZSB0eXBlOyBzdHJpbmdzIGFuZCBudW1iZXJzIGNhbiBhbHdheXMgYmUga2V5cy4gVHVwbGVzIGNhbiBiZSB1c2VkIGFzIGtleXMgaWYgdGhleSBjb250YWluIG9ubHkgc3RyaW5ncywgbnVtYmVycywgb3IgdHVwbGVzOyBpZiBhIHR1cGxlIGNvbnRhaW5zIGFueSBtdXRhYmxlIG9iamVjdCBlaXRoZXIgZGlyZWN0bHkgb3IgaW5kaXJlY3RseSwgaXQgY2Fubm90IGJlIHVzZWQgYXMgYSBrZXkuIFlvdSBjYW5cXHUyMDE5dCB1c2UgbGlzdHMgYXMga2V5cywgc2luY2UgbGlzdHMgY2FuIGJlIG1vZGlmaWVkIGluIHBsYWNlIHVzaW5nIGluZGV4IGFzc2lnbm1lbnRzLCBzbGljZSBhc3NpZ25tZW50cywgb3IgbWV0aG9kcyBsaWtlIGFwcGVuZCgpIGFuZCBleHRlbmQoKS5cIl19LCB7XCJ0aXRsZVwiOiBcIjMuIERhdGEgbW9kZWwgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3JlZmVyZW5jZS9kYXRhbW9kZWwuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiVGhlIHZhbHVlIG9mIHNvbWUgb2JqZWN0cyBjYW4gY2hhbmdlLiBPYmplY3RzIHdob3NlIHZhbHVlIGNhbiBjaGFuZ2UgYXJlIHNhaWQgdG8gYmUgbXV0YWJsZTsgb2JqZWN0cyB3aG9zZSB2YWx1ZSBpcyB1bmNoYW5nZWFibGUgb25jZSB0aGV5IGFyZSBjcmVhdGVkIGFyZSBjYWxsZWQgaW1tdXRhYmxlLiAoVGhlIHZhbHVlIG9mIGFuIGltbXV0YWJsZSBjb250YWluZXIgb2JqZWN0IHRoYXQgY29udGFpbnMgYSByZWZlcmVuY2UgdG8gYSBtdXRhYmxlIG9iamVjdCBjYW4gY2hhbmdlIHdoZW4gdGhlIGxhdHRlclxcdTIwMTlzIHZhbHVlIGlzIGNoYW5nZWQ7IGhvd2V2ZXIgdGhlIGNvbnRhaW5lciBpcyBzdGlsbCBjb25zaWRlcmVkIGltbXV0YWJsZSwgYmVjYXVzZSB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGl0IGNvbnRhaW5zIGNhbm5vdCBiZSBjaGFuZ2VkLiBTbywgaW1tdXRhYmlsaXR5IGlzIG5vdCBzdHJpY3RseSB0aGUgc2FtZSBhcyBoYXZpbmcgYW4gdW5jaGFuZ2VhYmxlIHZhbHVlLCBpdCBpcyBtb3JlIHN1YnRsZS4pIEFuIG9iamVjdFxcdTIwMTlzIG11dGFiaWxpdHkgaXMgZGV0ZXJtaW5lZCBieSBpdHMgdHlwZTsgZm9yIGluc3RhbmNlLCBudW1iZXJzLCBzdHJpbmdzIGFuZCB0dXBsZXMgYXJlIGltbXV0YWJsZSwgd2hpbGUgZGljdGlvbmFyaWVzIGFuZCBsaXN0cyBhcmUgbXV0YWJsZS5cXG4uLi5cXG5Tb21lIG9iamVjdHMgY29udGFpbiByZWZlcmVuY2VzIHRvIG90aGVyIG9iamVjdHM7IHRoZXNlIGFyZSBjYWxsZWQgY29udGFpbmVycy4gRXhhbXBsZXMgb2YgY29udGFpbmVycyBhcmUgdHVwbGVzLCBsaXN0cyBhbmQgZGljdGlvbmFyaWVzLiBUaGUgcmVmZXJlbmNlcyBhcmUgcGFydCBvZiBhIGNvbnRhaW5lclxcdTIwMTlzIHZhbHVlLiBJbiBtb3N0IGNhc2VzLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIHZhbHVlIG9mIGEgY29udGFpbmVyLCB3ZSBpbXBseSB0aGUgdmFsdWVzLCBub3QgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGNvbnRhaW5lZCBvYmplY3RzOyBob3dldmVyLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIG11dGFiaWxpdHkgb2YgYSBjb250YWluZXIsIG9ubHkgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGltbWVkaWF0ZWx5IGNvbnRhaW5lZCBvYmplY3RzIGFyZSBpbXBsaWVkLiBTbywgaWYgYW4gaW1tdXRhYmxlIGNvbnRhaW5lciAobGlrZSBhIHR1cGxlKSBjb250YWlucyBhIHJlZmVyZW5jZSB0byBhIG11dGFibGUgb2JqZWN0LCBpdHMgdmFsdWUgY2hhbmdlcyBpZiB0aGF0IG11dGFibGUgb2JqZWN0IGlzIGNoYW5nZWQuXFxuLi4uXFxuIyMjIDMuMi41LiBTZXF1ZW5jZXNcXHUwMGI2XFxuLi4uXFxuIyMjIyAzLjIuNS4xLiBJbW11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbkFuIG9iamVjdCBvZiBhbiBpbW11dGFibGUgc2VxdWVuY2UgdHlwZSBjYW5ub3QgY2hhbmdlIG9uY2UgaXQgaXMgY3JlYXRlZC4gKElmIHRoZSBvYmplY3QgY29udGFpbnMgcmVmZXJlbmNlcyB0byBvdGhlciBvYmplY3RzLCB0aGVzZSBvdGhlciBvYmplY3RzIG1heSBiZSBtdXRhYmxlIGFuZCBtYXkgYmUgY2hhbmdlZDsgaG93ZXZlciwgdGhlIGNvbGxlY3Rpb24gb2Ygb2JqZWN0cyBkaXJlY3RseSByZWZlcmVuY2VkIGJ5IGFuIGltbXV0YWJsZSBvYmplY3QgY2Fubm90IGNoYW5nZS4pXFxuXFxuVGhlIGZvbGxvd2luZyB0eXBlcyBhcmUgaW1tdXRhYmxlIHNlcXVlbmNlczpcXG4uLi5cXG5UdXBsZXNcXG46IFRoZSBpdGVtcyBvZiBhIGB0dXBsZWAgYXJlIGFyYml0cmFyeSBQeXRob24gb2JqZWN0cy4gVHVwbGVzIG9mIHR3byBvciBtb3JlIGl0ZW1zIGFyZSBmb3JtZWQgYnkgY29tbWEtc2VwYXJhdGVkIGxpc3RzIG9mIGV4cHJlc3Npb25zLiBBIHR1cGxlIG9mIG9uZSBpdGVtIChhIFxcdTIwMThzaW5nbGV0b25cXHUyMDE5KSBjYW4gYmUgZm9ybWVkIGJ5IGFmZml4aW5nIGEgY29tbWEgdG8gYW4gZXhwcmVzc2lvbiAoYW4gZXhwcmVzc2lvbiBieSBpdHNlbGYgZG9lcyBub3QgY3JlYXRlIGEgdHVwbGUsIHNpbmNlIHBhcmVudGhlc2VzIG11c3QgYmUgdXNhYmxlIGZvciBncm91cGluZyBvZiBleHByZXNzaW9ucykuIEFuIGVtcHR5IHR1cGxlIGNhbiBiZSBmb3JtZWQgYnkgYW4gZW1wdHkgcGFpciBvZiBwYXJlbnRoZXNlcy5cXG4uLi5cXG4jIyMjIDMuMi41LjIuIE11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbk11dGFibGUgc2VxdWVuY2VzIGNhbiBiZSBjaGFuZ2VkIGFmdGVyIHRoZXkgYXJlIGNyZWF0ZWQuIFRoZSBzdWJzY3JpcHRpb24gYW5kIHNsaWNpbmcgbm90YXRpb25zIGNhbiBiZSB1c2VkIGFzIHRoZSB0YXJnZXQgb2YgYXNzaWdubWVudCBhbmQgYGRlbGAgKGRlbGV0ZSkgc3RhdGVtZW50cy5cXG4uLi5cXG5UaGVyZSBhcmUgY3VycmVudGx5IHR3byBpbnRyaW5zaWMgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlczpcXG5cXG5MaXN0c1xcbjogVGhlIGl0ZW1zIG9mIGEgbGlzdCBhcmUgYXJiaXRyYXJ5IFB5dGhvbiBvYmplY3RzLiBMaXN0cyBhcmUgZm9ybWVkIGJ5IHBsYWNpbmcgYSBjb21tYS1zZXBhcmF0ZWQgbGlzdCBvZiBleHByZXNzaW9ucyBpbiBzcXVhcmUgYnJhY2tldHMuIChOb3RlIHRoYXQgdGhlcmUgYXJlIG5vIHNwZWNpYWwgY2FzZXMgbmVlZGVkIHRvIGZvcm0gbGlzdHMgb2YgbGVuZ3RoIDAgb3IgMS4pXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjcgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvYnVpbHRpbnMvc3RkdHlwZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiY29sbGVjdGlvbiBjbGFzc2VzIGFyZSBtdXRhYmxlLiBUaGUgbWV0aG9kcyB0aGF0IGFkZCwgc3VidHJhY3QsIG9yIHJlYXJyYW5nZSB0aGVpciAuLi4gaW4gcGxhY2UsIGFuZCBkb25cXHUyMDE5dCByZXR1cm4gYSAuLi4gLCBuZXZlciByZXR1cm4gdGhlIGNvbGxlY3Rpb24gaW5zdGFuY2UgaXRzZWxmIGJ1dCBgTm9uZWAuXFxuLi4uXFxuIyMgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBgbGlzdGAsIGB0dXBsZWAsIGByYW5nZWBcXHUwMGI2XFxuXFxuVGhlcmUgYXJlIHRocmVlIGJhc2ljIHNlcXVlbmNlIHR5cGVzOiBsaXN0cywgdHVwbGVzLCBhbmQgcmFuZ2Ugb2JqZWN0cy4gQWRkaXRpb25hbCBzZXF1ZW5jZSB0eXBlcyB0YWlsb3JlZCBmb3IgcHJvY2Vzc2luZyBvZiBiaW5hcnkgZGF0YSBhbmQgdGV4dCBzdHJpbmdzIGFyZSBkZXNjcmliZWQgaW4gZGVkaWNhdGVkIHNlY3Rpb25zLlxcbi4uLlxcblRoZSBvcGVyYXRpb25zIGluIHRoZSBmb2xsb3dpbmcgdGFibGUgLi4uIG11dGFibGUgYW5kIGltbXV0YWJsZS4gVGhlIGBjb2xsZWN0aW9ucy4gLi4uIGlzIHByb3ZpZGVkIHRvIG1ha2UgLi4uIGVhc2llciB0byBjb3JyZWN0bHkgaW1wbGVtZW50IHRoZXNlIG9wZXJhdGlvbnMgb24gY3VzdG9tIHNlcXVlbmNlIHR5cGVzXFxuLi4uXFxuIyMjIEltbXV0YWJsZSBTZXF1ZW5jZSBUeXBlc1xcdTAwYjZcXG4uLi5cXG5tdXRhYmxlIHNlcXVlbmNlIHR5cGVzIGlzXFxuLi4uXFxuc3VwcG9ydCBhbGxvd3MgaW1tdXRhYmxlIHNlcXVlbmNlcywgLi4uICwgdG8gYmUgdXNlZCBhcyBgZGljdGAga2V5cyBhbmQgc3RvcmVkIGluIC4uLiBlbnNldGAgaW5zdGFuY2VzXFxuLi4uXFxuIyMjIE11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxuIyMjIExpc3RzXFx1MDBiNlxcblxcbkxpc3RzIGFyZSBtdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaG9tb2dlbmVvdXMgaXRlbXMgKHdoZXJlIHRoZSBwcmVjaXNlIGRlZ3JlZSBvZiBzaW1pbGFyaXR5IHdpbGwgdmFyeSBieSBhcHBsaWNhdGlvbikuXFxuLi4uXFxuIyMjIFR1cGxlc1xcdTAwYjZcXG5cXG5UdXBsZXMgYXJlIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhldGVyb2dlbmVvdXMgZGF0YSAoc3VjaCBhcyB0aGUgMi10dXBsZXMgcHJvZHVjZWQgYnkgdGhlIGBlbnVtZXJhdGUoKWAgYnVpbHQtaW4pLiBUdXBsZXMgYXJlIGFsc28gdXNlZCBmb3IgY2FzZXMgd2hlcmUgYW4gaW1tdXRhYmxlIHNlcXVlbmNlIG9mIGhvbW9nZW5lb3VzIGRhdGEgaXMgbmVlZGVkIChzdWNoIGFzIGFsbG93aW5nIHN0b3JhZ2UgaW4gYSBgc2V0YCBvciBgZGljdGAgaW5zdGFuY2UpLlxcbi4uLlxcbnR1cGxlKGl0ZXJhYmxlPVxcbi4uLlxcblRoZSBjb25zdHJ1Y3RvciBidWlsZHMgYSB0dXBsZSB3aG9zZSBpdGVtcyBhcmUgdGhlIHNhbWUgYW5kIGluIHRoZSBzYW1lIG9yZGVyIGFzIGl0ZXJhYmxlXFx1MjAxOXMgaXRlbXMuIGl0ZXJhYmxlIG1heSBiZSBlaXRoZXIgYSBzZXF1ZW5jZSwgYSBjb250YWluZXIgdGhhdCBzdXBwb3J0cyBpdGVyYXRpb24sIG9yIGFuIGl0ZXJhdG9yIG9iamVjdC4gSWYgaXRlcmFibGUgaXMgYWxyZWFkeSBhIHR1cGxlLCBpdCBpcyByZXR1cm5lZCB1bmNoYW5nZWQuIEZvciBleGFtcGxlLCBgdHVwbGUoJ2FiYycpYCByZXR1cm5zIGAoJ2EnLCAnYicsICdjJylgIGFuZCBgdHVwbGUoIFsxLCAyLCAzXSApYCByZXR1cm5zIGAoMSwgMiwgMylgLiBJZiBubyBhcmd1bWVudCBpcyBnaXZlbiwgdGhlIGNvbnN0cnVjdG9yIGNyZWF0ZXMgYSBuZXcgZW1wdHkgdHVwbGUsIGAoKWAuXFxuLi4uXFxuVHVwbGVzIGltcGxlbWVudCBhbGwgb2YgdGhlIGNvbW1vbiBzZXF1ZW5jZSBvcGVyYXRpb25zLlxcbi4uLlxcbkZvciBoZXRlcm9nZW5lb3VzIGNvbGxlY3Rpb25zIG9mIGRhdGEgd2hlcmUgYWNjZXNzIGJ5IG5hbWUgaXMgY2xlYXJlciB0aGFuIGFjY2VzcyBieSBpbmRleCwgYGNvbGxlY3Rpb25zLm5hbWVkdHVwbGUoKWAgbWF5IGJlIGEgbW9yZSBhcHByb3ByaWF0ZSBjaG9pY2UgdGhhbiBhIHNpbXBsZSB0dXBsZSBvYmplY3QuXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvbGlicmFyeS9zdGR0eXBlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJTb21lIGNvbGxlY3Rpb24gY2xhc3NlcyBhcmUgbXV0YWJsZS4gVGhlIG1ldGhvZHMgdGhhdCBhZGQsIHN1YnRyYWN0LCBvciByZWFycmFuZ2UgdGhlaXIgLi4uIGluIHBsYWNlLCBhbmQgZG9uXFx1MjAxOXQgcmV0dXJuIGEgLi4uICwgbmV2ZXIgcmV0dXJuIHRoZSBjb2xsZWN0aW9uIGluc3RhbmNlIGl0c2VsZiBidXQgYE5vbmVgLlxcbi4uLlxcbiMjIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgYGxpc3RgLCBgdHVwbGVgLCBgcmFuZ2VgXFx1MDBiNlxcblxcblRoZXJlIGFyZSB0aHJlZSBiYXNpYyBzZXF1ZW5jZSB0eXBlczogbGlzdHMsIHR1cGxlcywgYW5kIC4uLiBvYmplY3RzLiBBZGRpdGlvbmFsIHNlcXVlbmNlIHR5cGVzIHRhaWxvcmVkIGZvciBwcm9jZXNzaW5nIG9mIGJpbmFyeSBkYXRhIGFuZCB0ZXh0IHN0cmluZ3MgYXJlIGRlc2NyaWJlZCBpbiBkZWRpY2F0ZWQgc2VjdGlvbnMuXFxuLi4uXFxuVGhlIG9wZXJhdGlvbnMgaW4gdGhlIGZvbGxvd2luZyB0YWJsZSAuLi4gaW1tdXRhYmxlLiBUaGUgYCAuLi4gaXMgcHJvdmlkZWQgdG8gbWFrZSAuLi4gZWFzaWVyIHRvIGNvcnJlY3RseSBpbXBsZW1lbnQgdGhlc2Ugb3BlcmF0aW9ucyBvbiBjdXN0b20gc2VxdWVuY2UgdHlwZXNcXG4uLi5cXG4jIyMgSW1tdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbm9wZXJhdGlvbiB0aGF0IGltbXV0YWJsZSBzZXF1ZW5jZSAuLi4gYnkgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlcyBpcyAuLi4gYGhhc2goKWBcXG4uLi5cXG5UaGlzIHN1cHBvcnQgYWxsb3dzIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHN1Y2ggYXMgYHR1cGxlYCBpbnN0YW5jZXMsIHRvIGJlIHVzZWQgYXMgYGRpY3RgIGtleXMgYW5kIHN0b3JlZCBpbiBgc2V0YCBhbmQgYGZyb3plbnNldGAgaW5zdGFuY2VzLlxcbi4uLlxcbiMjIyBNdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbiMjIyBMaXN0c1xcdTAwYjZcXG5cXG5MaXN0cyBhcmUgbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhvbW9nZW5lb3VzIGl0ZW1zICh3aGVyZSB0aGUgcHJlY2lzZSBkZWdyZWUgb2Ygc2ltaWxhcml0eSB3aWxsIHZhcnkgYnkgYXBwbGljYXRpb24pLlxcbi4uLlxcbiMjIyBUdXBsZXNcXHUwMGI2XFxuXFxuVHVwbGVzIGFyZSBpbW11dGFibGUgc2VxdWVuY2VzLCB0eXBpY2FsbHkgdXNlZCB0byBzdG9yZSBjb2xsZWN0aW9ucyBvZiBoZXRlcm9nZW5lb3VzIGRhdGEgKHN1Y2ggYXMgdGhlIDItdHVwbGVzIHByb2R1Y2VkIGJ5IHRoZSBgZW51bWVyYXRlKClgIGJ1aWx0LWluKS4gVHVwbGVzIGFyZSBhbHNvIHVzZWQgZm9yIGNhc2VzIHdoZXJlIGFuIGltbXV0YWJsZSBzZXF1ZW5jZSBvZiBob21vZ2VuZW91cyBkYXRhIGlzIG5lZWRlZCAoc3VjaCBhcyBhbGxvd2luZyBzdG9yYWdlIGluIGEgYHNldGAgb3IgYGRpY3RgIGluc3RhbmNlKS5cXG4uLi5cXG5UaGUgY29uc3RydWN0b3IgYnVpbGRzIGEgdHVwbGUgd2hvc2UgaXRlbXMgYXJlIHRoZSBzYW1lIGFuZCBpbiB0aGUgc2FtZSBvcmRlciBhcyBpdGVyYWJsZVxcdTIwMTlzIGl0ZW1zLiBpdGVyYWJsZSBtYXkgYmUgZWl0aGVyIGEgc2VxdWVuY2UsIGEgY29udGFpbmVyIHRoYXQgc3VwcG9ydHMgaXRlcmF0aW9uLCBvciBhbiBpdGVyYXRvciBvYmplY3QuIElmIGl0ZXJhYmxlIGlzIGFscmVhZHkgYSB0dXBsZSwgaXQgaXMgcmV0dXJuZWQgdW5jaGFuZ2VkLiBGb3IgZXhhbXBsZSwgYHR1cGxlKCdhYmMnKWAgcmV0dXJucyBgKCdhJywgJ2InLCAnYycpYCBhbmQgYHR1cGxlKCBbMSwgMiwgM10gKWAgcmV0dXJucyBgKDEsIDIsIDMpYC4gSWYgbm8gYXJndW1lbnQgaXMgZ2l2ZW4sIHRoZSBjb25zdHJ1Y3RvciBjcmVhdGVzIGEgbmV3IGVtcHR5IHR1cGxlLCBgKClgLlxcbi4uLlxcblR1cGxlcyBpbXBsZW1lbnQgYWxsIG9mIHRoZSBjb21tb24gc2VxdWVuY2Ugb3BlcmF0aW9ucy5cXG4uLi5cXG5Gb3IgaGV0ZXJvZ2VuZW91cyBjb2xsZWN0aW9ucyBvZiBkYXRhIHdoZXJlIGFjY2VzcyBieSBuYW1lIGlzIGNsZWFyZXIgdGhhbiBhY2Nlc3MgYnkgaW5kZXgsIGBjb2xsZWN0aW9ucy5uYW1lZHR1cGxlKClgIG1heSBiZSBhIG1vcmUgYXBwcm9wcmlhdGUgY2hvaWNlIHRoYW4gYSBzaW1wbGUgdHVwbGUgb2JqZWN0LlwiXX0sIHtcInRpdGxlXCI6IFwiNS4gRGF0YSBTdHJ1Y3R1cmVzIFxcdTIwMTQgUHl0aG9uIDMuMTAuMjAgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMuMTAvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiWW91IG1pZ2h0IGhhdmUgbm90aWNlZCB0aGF0IG1ldGhvZHMgbGlrZSBgaW5zZXJ0YCwgYHJlbW92ZWAgb3IgYHNvcnRgIHRoYXQgb25seSBtb2RpZnkgdGhlIGxpc3QgaGF2ZSBubyByZXR1cm4gdmFsdWUgcHJpbnRlZCBcXHUyMDEzIHRoZXkgcmV0dXJuIHRoZSBkZWZhdWx0IGBOb25lYC4gMSBUaGlzIGlzIGEgZGVzaWduIHByaW5jaXBsZSBmb3IgYWxsIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIGluIFB5dGhvbi5cXG4uLi5cXG4jIyA1LjMuIFR1cGxlcyBhbmQgU2VxdWVuY2VzXFx1MDBiNlxcblxcbldlIHNhdyB0aGF0IGxpc3RzIGFuZCBzdHJpbmdzIGhhdmUgbWFueSBjb21tb24gcHJvcGVydGllcywgc3VjaCBhcyBpbmRleGluZyBhbmQgc2xpY2luZyBvcGVyYXRpb25zLiBUaGV5IGFyZSB0d28gZXhhbXBsZXMgb2Ygc2VxdWVuY2UgZGF0YSB0eXBlcyAoc2VlIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgbGlzdCwgdHVwbGUsIHJhbmdlKS4gU2luY2UgUHl0aG9uIGlzIGFuIGV2b2x2aW5nIGxhbmd1YWdlLCBvdGhlciBzZXF1ZW5jZSBkYXRhIHR5cGVzIG1heSBiZSBhZGRlZC4gVGhlcmUgaXMgYWxzbyBhbm90aGVyIHN0YW5kYXJkIHNlcXVlbmNlIGRhdGEgdHlwZTogdGhlIHR1cGxlLlxcblxcbkEgdHVwbGUgY29uc2lzdHMgb2YgYSBudW1iZXIgb2YgdmFsdWVzIHNlcGFyYXRlZCBieSBjb21tYXMsIGZvciBpbnN0YW5jZTpcXG4uLi5cXG4+Pj4gIyBUdXBsZXMgYXJlIGltbXV0YWJsZTpcXG4uLi4gdFswXSA9IDg4ODg4XFxuVHJhY2ViYWNrIChtb3N0IHJlY2VudCBjYWxsIGxhc3QpOlxcbiAgRmlsZSBcXFwiPHN0ZGluPlxcXCIsIGxpbmUgMSwgaW4gPG1vZHVsZT5cXG5UeXBlRXJyb3I6ICd0dXBsZScgb2JqZWN0IGRvZXMgbm90IHN1cHBvcnQgaXRlbSBhc3NpZ25tZW50XFxuPj4+ICMgYnV0IHRoZXkgY2FuIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzOlxcbi4uLiB2ID0gKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbj4+PiB2XFxuKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbi4uLlxcbkFzIHlvdSBzZWUsIG9uIG91dHB1dCB0dXBsZXMgYXJlIGFsd2F5cyBlbmNsb3NlZCBpbiBwYXJlbnRoZXNlcywgc28gdGhhdCBuZXN0ZWQgdHVwbGVzIGFyZSBpbnRlcnByZXRlZCBjb3JyZWN0bHk7IHRoZXkgbWF5IGJlIGlucHV0IHdpdGggb3Igd2l0aG91dCBzdXJyb3VuZGluZyBwYXJlbnRoZXNlcywgYWx0aG91Z2ggb2Z0ZW4gcGFyZW50aGVzZXMgYXJlIG5lY2Vzc2FyeSBhbnl3YXkgKGlmIHRoZSB0dXBsZSBpcyBwYXJ0IG9mIGEgbGFyZ2VyIGV4cHJlc3Npb24pLiBJdCBpcyBub3QgcG9zc2libGUgdG8gYXNzaWduIHRvIHRoZSBpbmRpdmlkdWFsIGl0ZW1zIG9mIGEgdHVwbGUsIGhvd2V2ZXIgaXQgaXMgcG9zc2libGUgdG8gY3JlYXRlIHR1cGxlcyB3aGljaCBjb250YWluIG11dGFibGUgb2JqZWN0cywgc3VjaCBhcyBsaXN0cy5cXG5cXG5UaG91Z2ggdHVwbGVzIG1heSBzZWVtIHNpbWlsYXIgdG8gbGlzdHMsIHRoZXkgYXJlIG9mdGVuIHVzZWQgaW4gZGlmZmVyZW50IHNpdHVhdGlvbnMgYW5kIGZvciBkaWZmZXJlbnQgcHVycG9zZXMuIFR1cGxlcyBhcmUgaW1tdXRhYmxlLCBhbmQgdXN1YWxseSBjb250YWluIGEgaGV0ZXJvZ2VuZW91cyBzZXF1ZW5jZSBvZiBlbGVtZW50cyB0aGF0IGFyZSBhY2Nlc3NlZCB2aWEgdW5wYWNraW5nIChzZWUgbGF0ZXIgaW4gdGhpcyBzZWN0aW9uKSBvciBpbmRleGluZyAob3IgZXZlbiBieSBhdHRyaWJ1dGUgaW4gdGhlIGNhc2Ugb2YgYG5hbWVkdHVwbGVzYCkuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG4jIyA1LjQuXFxuLi4uXFxuLiBTZXQgb2JqZWN0c1xcbi4uLlxcbiMjIDUuNS4gRGljdGlvbmFyaWVzXFx1MDBiNlxcblxcbkFub3RoZXIgdXNlZnVsIGRhdGEgdHlwZSBidWlsdCBpbnRvIFB5dGhvbiBpcyB0aGUgZGljdGlvbmFyeSAoc2VlIE1hcHBpbmcgVHlwZXMgXFx1MjAxNCBkaWN0KS4gRGljdGlvbmFyaWVzIGFyZSBzb21ldGltZXMgZm91bmQgaW4gb3RoZXIgbGFuZ3VhZ2VzIGFzIFxcdTIwMWNhc3NvY2lhdGl2ZSBtZW1vcmllc1xcdTIwMWQgb3IgXFx1MjAxY2Fzc29jaWF0aXZlIGFycmF5c1xcdTIwMWQuIFVubGlrZSBzZXF1ZW5jZXMsIHdoaWNoIGFyZSBpbmRleGVkIGJ5IGEgcmFuZ2Ugb2YgbnVtYmVycywgZGljdGlvbmFyaWVzIGFyZSBpbmRleGVkIGJ5IGtleXMsIHdoaWNoIGNhbiBiZSBhbnkgaW1tdXRhYmxlIHR5cGU7IHN0cmluZ3MgYW5kIG51bWJlcnMgY2FuIGFsd2F5cyBiZSBrZXlzLiBUdXBsZXMgY2FuIGJlIHVzZWQgYXMga2V5cyBpZiB0aGV5IGNvbnRhaW4gb25seSBzdHJpbmdzLCBudW1iZXJzLCBvciB0dXBsZXM7IGlmIGEgdHVwbGUgY29udGFpbnMgYW55IG11dGFibGUgb2JqZWN0IGVpdGhlciBkaXJlY3RseSBvciBpbmRpcmVjdGx5LCBpdCBjYW5ub3QgYmUgdXNlZCBhcyBhIGtleS4gWW91IGNhblxcdTIwMTl0IHVzZSBsaXN0cyBhcyBrZXlzLCBzaW5jZSBsaXN0cyBjYW4gYmUgbW9kaWZpZWQgaW4gcGxhY2UgdXNpbmcgaW5kZXggYXNzaWdubWVudHMsIHNsaWNlIGFzc2lnbm1lbnRzLCBvciBtZXRob2RzIGxpa2UgYGFwcGVuZCgpYCBhbmQgYGV4dGVuZCgpYC5cIl19XSIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ3ZWJfc2VhcmNoIiwiaWQiOiI2ZTUxMjVhYy0zMTY0LTRhNzYtOWM1Mi1hYzM0NjBjZTM2ZTQiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJzdGF0dXMiOiJzdWNjZXNzIn0seyJjb250ZW50IjpbeyJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3NrZXB0aWMiLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QTZGNGNDTjV5aEQ0Q3JzOU0iLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjB9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InJlc2VhcmNoZXIiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtM2NkOC03OWYxLTg4YTEtY2U2Y2VlMjk0ZTc5LTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoidHJhbnNmZXJfdG9fc2tlcHRpYyIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsInRvdGFsX3Rva2VucyI6NzQxMSwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fSx7ImNvbnRlbnQiOiJTdWNjZXNzZnVsbHkgdHJhbnNmZXJyZWQgdG8gc2tlcHRpYyIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ0cmFuc2Zlcl90b19za2VwdGljIiwiaWQiOiIzODkwNThkMS0wODNmLTQ2MzItYmJjNS0wOWJmZDNhMGZmZDIiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMVhqajNhUmRXeWhCSmZQbVRKQnhRYTkiLCJzdGF0dXMiOiJzdWNjZXNzIn0seyJjb250ZW50IjpbeyJzaWduYXR1cmUiOiJDQVFTMVFnS0VBZ1NHQUk0QVVJSWRHaHBibXRwYm1jU0RONlRmVTZORE05NzFsRWJ6eG9NdlJVQTFKOW94M2ZKUVlUOUlqQXJDUVFHSEJQNHBSSFBhdlh6UmxiTWJhWUhDNTdZaitHYnhncjBoZWx2ZDJPSUwvbzZSYnQ0UEZBM0ZhSUZVbGdxOGdjN0tRSTlGdEdudlNBa0xCUnpJc1VwLzJkNVJkMnpnRXNma0ZWZVhzbjhXWVlaa2luQVFWVnk2QTdCM0xhRUpXMHhiL3g4OERpNjdmZWR3eDBRK0ZsWnR3Y21UM0RjYWExMUp2ZXlIUnIzZG0rekdTWlA2Q1hlWE5KOG5yVFo3VzFZUlR5c0Q2dzdvN0xwOGgxUndxNGY0bzhqK1FVZlkwdmplK3pKWnN4SFBtUU50M3lrZUtoOTloZ242ZGJpejYvRUhxbWJIalNBdTFNNFNxRURmcUlLVGhIWWVuYW1BWm1jUTRYQU9vNm1kUTYrNzdWZ3BWMWxDMS9rNTJJaVVvSmprR2xRcm5MQ3Z1dGlJN0tOK3NnSzhjMisyS0NKclBSQ3ZWVitCa1N0dlA1aldXcXp5dHBLUStuSHpoTDQ0OE9ySUE5Z2RKQzM5QzhlRHJYZVJuSTd0NEZxbGtYR29NbFpBVjlVTWNyUUhSeE9NaTh0T3lUemk5b3lCRkRUU3NRK0xKcWVyMHR6MUZ0OWdlazZ2emc1MFNwVkFGUWRMRVJ2OFIxWXFNYWVJK3hzdlRBRVc5TVJndlJNa3pMdXpSVDJndXczakQrRmppcDM3eGdLNVFJMFIrQ3NPQW1RUG9UWml6emNJeUFVQXVDSkZDZ3EvZ0QvQ2liTHhveW1pVlVJMWFuOVNudDNLcE5Sb1dkdldJbmUzM1YwcHY4UEhnOXlXMkp1TE9xUUdpbExDc0svUTNvUXIzdXBFTVh4WEdqQ0w3V1VpenZSWkdGRUVWK1ZwbFl0N3Y0dEtaTWo1cGszdmRlTTBNSjVYWmc3UHZVc1J6eWN4Y25yVDNLbm9jM3FNK3R0cVpmVHNiOGQ2VS9wUWlmcTdHdWFoRk9IcnZyb3FnSkxqKy84MVl5MXBaRmtqZkpnWTBYa2x6QW5BNkNGcS9ZVDBneGJWK1RINUlTK1dhWU9VekszNlM4czVxcU13aG5ndjRJekUzY3RYWHFuaElaWE56bnlWQnFiWWEvR3gyYkNqY0VqMm41UnlHWVJTLytNY3VsZ0xCem5ROU1rRzJEN1FQM01xL253SFhLTUlhTFM4Vks4aEczZDByQWpsdm5hdVhTaE5ySnh0a2o0cWt3bnhENExyRWprMXVlK1BMY2JMcVlXd0FyOG4wcys0MWpyc0lQVnl5Szl2RmFFWnJBOUthQnhrSnlpcXg1MEdaTm43Y2t4K21rSTE4Z0g1Mml5VlB0VThGS01YanVnQ2w2bVBMaUhZaURRZCtHNjNMdWRtT0t5czlpdFZuQkRCTk1QMmIvSTcrYklWUmxoZUdhWldyNk1BVEtsODZCM3krMGt1T3JMUk15V2NkaWJjMmlRa1ptKzhmN2RUVXdiS2JCSnNLT0ZZNzMvOG5sTC9KaVlwano2blVJaytJcVNqYUZ5SzZYRmxDY3VrV3BXSHBESm5TdmJFemV5N3FIeUNxSitJK29uOGlQOEdPdGVtcFFhUzAvNGtaR1N3bHJFNnhPajZFd0dwZmpiRFpLSGtQLzJSQWROWVBRcTN6N2JkVkpNSk5uRk16NmUzN216TXBDQmlod1BkU0ZUUkUzU0NZNjZ3dlRnSnpHQ1V5aEhiU2k1aTFhenpPWGQ4dUhDU3BULzZMMW82VlUwcGpvUjhLYWE0d0NXSEdmN1AvV0VkK2F2eXAwMkpudTFjbXBXMCtHcSt2SzloMHRkYjJJQ2xNL2VOZmRvdVFQK2VwOE1KVWpyYURSSUNtQUpRWTVvYlM3MXZuaU9vK3JlYUZ5MHpSZ0IiLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsic2lnbmF0dXJlIjoiQ0FRUy9SRUtFUWdTR0FJNEFVSUpibUZ5Y21GMGFXOXVFZ3dGaDNBVkpaUDNzOHkxSHEwYURHNzZ1aEhPTmFTY1hBR1NoaUl3R2dqTFhIVXgxeVpya0xQbGpqelRPUXljcExKaUtWd1hjeG0vRXpLSm0xczhNNVd4dVJFV0VsZFhjb2FOMC9tc0twa1JNVCtlTGI1aWg5ckFIVUxUb21VTnNVd0pldTBqckplNEdMYXp5N3NQNUt0akVPRUJLUXNYVWZvUTdmRGtaT1FVSFppOXp6UU40aEZObVlKVWYzT1ZxZjlUUU5kQzErbS8vSEFaUGZXbUF5Nkh0RHZtN2tnQkprQ0VvaUZubTd0bi9QWDFoWEdQUmdXYzZCVFhVTXIyV0E4YUt5cWdnUlViTUgyK0pZdjVpV1pkZENjMXVwMU5icjNzRjdXUGNvdDlaQmh5SFFYcjY5MnplSms2U0pneWdJVnJBU2JNbHkyYkJ4VFdKQjYyM2dWZ3pwQ21WU0xaRnBrS1hnUGNwOGhHeURLT3ZGNlNpUVNIcUdSeFFvZnI3VnMxYVBoZlppeFJPSkdEK01scmtnZ1ZBbkdNeVlJRFVacGF2dENlaVUxSm1CU3owMDg0YlhQdFQvdG5wc0tPblNQWVpRbzNCMEJYS2JxQS9Xc0tuZzgvVjNaOFV1YWpvNklWWjJoZ0ExV0RkdXFRT1FrRXhHVTZwRnVMaGViSWp1V3hTTkkvRGZmdlIvL0xOcUNmdmRuZjBodm9QdER4ZngvU2F3SXg0L3NwcTl4YndGekh0TGdvSGcxRjJ3Rnc3Y2xQRGxnMVB5RC92MnlKRzNWOVQ4R0w2akdQRTZyaGZpbC91NEhOdmY3QlcwZFF6Q1RUWGNxWnk0bkJ5eTArcCtTN1p4Zk1sNEtweGEyZ09oaFNOamZwZnVYMFFwTDRrTUU4SjFubGlGTjFkOEtxRVB1REkxOEJRSFVoRC9VZlkyOW9BMHg1MTBGak0vQnNMWjRuNXNwcGtvSWk3Q0ZvUXEyYUFLVk5mQzFLaWxoSXBSRCsxZHlEdFpRa2xOOGdacE9FU0h3Nk1MMVR0VTB5ZkFjYm1xb3hKNmhoanp3SDZDTXNUUUVFeThNYlJCTStRS0trSVJkUkNQSEdGVzJMa3VSNTArSjBwRi90cjZwcVZ2SmVvemZpTW5yRWlsQjI3VFpQNG5LVEpHOHJkLy92Q1dlbjB5N0NBLzhkQko5Z2RwNXR4N05WQTQ1azFnZGN4RVIxZ1lvdU5YM24xdkczL0IxRjQrOTN3WFc5TXFvNlVzNTlUUW1yYjlOcWE3cFNRTlNySDA2dzFHeHlCQWxrRmZxQWxhZmsxNmFNcHE0MTZUSllwR3k0M1dDVWlDY0xXbGhpV2d6MnNnT0xBWFA5YTV3ZVdBS1JDOEo2amtjVWtEZVhuVytIRlNpUE02KzlCbzZrVmhMVWhONXJjNG1IWk9acEY3cHRQcHNRTjRYQ3FMUHNxcFJILzhvNVhweUM2Wm5lVGVGMkV4dGpod1pWalhEd01iSXpWWUlidzF5MlBQR3c4ZEM3cGM0c0lWU25JNG9RQUgwdjY1MGdoZmhtTWV2V1c0aDRQeWxidy9ERzRmZGVUMys4ejBmOE1PU1R0ZmlweDA1L1F0dEhsQXZPRFJ4YTV5REN2T3o2bjNmMUJPME4vNE1iR3lPTTI3cVc0eDhtQlZvcnlVWldmc21oZmkwL0dmTnZCOCt5bFRHYllOWDI3T2NFRjVXWnROcWJXRXk3VjVHYm1aVVN4c3BjZzdGQlozajMvM0w3bXV2WlhvWmkxZjVlcnE0eTNsU0grb0Y3cVBGbnZpZXlMT0N3dWJjUXkrb3lvd2o0Y0QwaWRSZWNucCtzKzUyRDcrbXpGMWdwVytmK2NKYXNveGlnYnhrT2xvUzR5ZWlLdmdNNG5XU3BHRWoxMlJQeDVvODBFbXppWE45dDR6SnpXMWh0QTVoWEpKZUZ5bXBNWXR5Q1VDZXZSV1ZtclJZa3RRN1VuQ2E2dEwvQVZQaTFuaTVYQ1hIZkxPdXFmSzVLTFl4andWdGRoaGZtQ0x2cGFLVk5DRU9DZjRuTkdIcUQ5OHdVYjBlaldOd0RZQm5KR2tCUnc5UlZzcGZ3TmRvV29WY24ycXY4eXdHVXA3eWMvc2syTHBJNlZ6aVJ5dldxYkdORzFaSnFWVnV0UmM0NitzT0U0VlNlUzIvQ1ROS05hNk5NQ2YvaHh4YVVWc1lmS2RQK1MxTGM0aWFuS3Awc1V1Z1BwbXRveWRDMENQMlJ3am5ITGNuenhDbnZxeGxMUVdYTU5QVHN1MnlCY0hsV0pMdGNtZGcvTWNMYWd5N2w1a0c2c043WnhKTEQva0F4OEU2dXFvaFArSy9qTVNyWXVRSm5ocjR0ZjRiaVdGWTNKemYvS3BBWXlWU3ErcGJmRktYTWJMOW5IRW9aUVQxNVd5YWJkNkRNQXEzUVpPdGhNNXBVemROcVVYa2NySTFQd2pBU0FnNGRWVS8ybys5OG1UWG55aFA3VmxFa1RuZGRwd3lKRitZQ0NjMDFwcWE2Wlg5OXBuUitZT3BJcmI1RHRQY0txMHQ1Y0ZDRHlReE4xbFV1cVpjWXVqdlgvR0M3KzZJcFJUei9Oa0VZLzVjbnRPZlpKeWQ2U2tVeC9KUjVBSklSUENOZWliMHJjV25uRm4wWEs4czIyeDQwd0xqRXR1ZlhETzkreXJhZjRrSHhsQkkzU0IwbWVNamVwWDUwQ0poQXg3TmVra1JGbndvZWNMZnFJeXJqWHJnQVpzVE1QaVg5NHNuamRyemlzQVh0QkthbnVsaWxTeE9mVzJIU21mbmswTDJHWEhKSjNTK3ZDYmdSRW1sOWRla2ZtSGhtd0VZdHYza2ZodGg3RVJJd1ZUOGo2OU90SUJDbWVUSUp1QTdvNzNQWGMxTEoyd2h1Wks3RlplNk5UT3M2MU1QN0pWbEljNnRiaHRPQmszTlhVOHBCUE9DWTZRNTVkNjNGdHBVVS9LQ2JRMEsyV1E5bzVqTkd3Tk5KNkN3NUZEQ3QreDVZSmVQMWQyUXpEU2lxK2NTWnF3cUNaVmNOb0FmdEdHemdyUlo0dmtwYlAweFFaSEN0V3NVd0hWVGpyWTROZlJEakhkVlBSbjhKOVh0b29jL2hJV2txekZhY3pZVGxhcHUyUkNKcWU4YUVvM2p6V1pDRUo3QWlCekJZcmFZRGdPZmVpUXllSlVzYmJYSGRHS2lpdzIyQmRwNE0rWXo2NzFNWjBpZTlFV1JrczBRMkRIR1VUbmU5UzR0QUFKUEdiVWY0eHRDTW9KNDBRNFFXMDVRTFNaeTN3TkZBVUZZbVhFNEVDREZCOCtJRWVORkZGL3RnQW5maDhHdkZoLzF4WStMRStvek1taW5qU1J3aHEwOXpRQUVrTkZ5dTBJS2d2RGt5T0kraWNjRjdkb1dMVFVhc1B4TEgxeEdQQlNLa2VXQ25Tb1gvNEMyYzlkdTY2QmJSWGFtSHFhZStWQ2lTNkJOOTUzaVlLT3U4ck1rbmVXK1RGalYwQ3pBZjhQTkc5TEJheVVnWTVVdzl4MFNhcDZ6STRQcGFaS0g0ZDlrdGtGVSs5MXlCYk41U1J0ZGQ4L0VRYXRKWG1CTmZ3N2tScmxsK0xqd1p3ZVdMRWIwalQxUnBHdURxSExkTFRNTWdvd1NjenJwMnpDc0pkUUgvc2VhWXJwd2tDRlI4ZFJqTTdKUlVrVmExQTBTdmRaQVg3NXZ1QnZ2TEJ6TjJjK1JidDVvcXk5M2xxSUJJaktiN3cyVWtSVGI0cEJXY05LTlBVZ3QzYXovUHhReEVhc2FsdGRtdXl2c1hZYnVyM1ZQTm44SnpOLzZKNU4rUUNuejU3MVRjY2FwQ21rUHZGRStnYXlYVStVRjFWS1l5RmNVdmxldjdsc1FsbWpxeksyUUE4WDdRM1R6RmFwcWh2SktsdDdkekJIcmFxWkJnUGdiOU40b0tOYXlpOEZaVDcxK2thWWhJaEZyRVNLWVJIZkhrajVuM2tmaGdMeSt4K2pMRGJLeE1nZ2tnSG5KejlSdUVIWlhCV3RrWUFRPT0iLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsiaWQiOiJ0b29sdV8wMUg2ZlY4SnZSSk52WHk0N2VSbmQzakgiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6e30sIm5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHdBTVJkUzIxRmVKZnpRaE0zcyIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo3Mzk2LCJvdXRwdXRfdG9rZW5zIjo3NzAsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjczOX0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoic2tlcHRpYyIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy00YWU5LTc2YzAtODlhNi00ZjBiMTE0ZTMwMzAtMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzM5Niwib3V0cHV0X3Rva2VucyI6NzcwLCJ0b3RhbF90b2tlbnMiOjgxNjYsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjczOX19fSx7ImNvbnRlbnQiOiJTdWNjZXNzZnVsbHkgdHJhbnNmZXJyZWQgdG8gdmVyaWZpZXIiLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7fSwidHlwZSI6InRvb2wiLCJuYW1lIjoidHJhbnNmZXJfdG9fdmVyaWZpZXIiLCJpZCI6ImMwNjQ1NjNhLTk3NGYtNGYwZi1hYjRhLTA4MGU2M2MwMWNmNSIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsInN0YXR1cyI6InN1Y2Nlc3MifSx7ImNvbnRlbnQiOlt7InNpZ25hdHVyZSI6IkNBUVM5Z1VLRUFnU0dBSTRBVUlJZEdocGJtdHBibWNTREdGeUoxTVhvWStBaTZOa2J4b01SZDJUclVmVVU3V3JQcEEySWpEZ3k4ZFVwbGNaS2M3RG5Kd05MVEJmSDVVTlIrbkduYWtTUFFpYThVVkl6cmprT25KMmZMcldVc0Eyb0VPOHlSc3Frd1hra0RDc3loQjk3ZHdqNTdSTXQ5MVh6Z1doTDlsdU5FR085MnlXMHJORkdvTVNVVk94UElyRmM1Ump5cmhLRWMzKzFSbnB4Q2t5bHErTE5sUTVORnhMbjZ2MmNGNUR6cXB2QkhOUjdYQWlnK3VySklVM2dYeWRDa2FHWlpEZXRKdFFPVU9YZ1ppOXo5MTAwTnBkQUVrWERvMklDSENHVEdwVmMwSi9wYlh4V1JCeGI2QjEvR2wwWWpuNjhWNkhsZXNIN0lvQzdHUi8zSHdSdTlOU0RCRzR0dGZRakVXY2crUGVBZWE0Um43anp4K1RtOUg3TDFqb1RLVmdEYlFnNWlLTUxmanZQcWxua3NDTHRYT3Q4alNJUzVPWUxON2EvN1RYNi93NWQzNHZudzh2UERtVTFVWWkwUTcycVVXaFVhMnZ3OE9YTnBjS0w3QnAwbE5mQlM4cnZIWSt2Qlk4L2hhQW9UTDN3dkxGVTFmVCs0ei92QTNCY3NpZ1pEWWxmSmVEWWN4WG1SdXF4enhFR3BYQ1JlOXMxSFd0SUQrNDFwSHU2bHBaSGtYYWdocGM2VFpITHhOd2NWaFRiNmZNUTdRbysyTDZQdmtsaDFtMzJwZW9keGVnMllpUGhkcGwzdy94a09FdTUvSkVGN1pCeHBkVUwrdTN3a081cFVsc3FmZ1g3dWh6djdIQzJXcFNXVmltcndpUXdRT1V4MCtKUHJMdHcrbFlmcTdJM2pydW8wR1cwZ3VXdm51ZjdKTTNZTGo4NHErSmdIb21uU1Nabm5abXJMMFVhUnZyVnlMbzg3N0YzOWNjTFM3WG9abm51aThKNFA1OFpUbWlMa0VKZUdjeDFJdWFnMmUzMzMzOU83WWV3LzU5dExiUXRJR2g5VldNd0hCbkZ4MzlTOC80TEVGcks1SGxkUFFoT3Jsb3lUdGtSczY2aVFSS3R4TUtTMFZJNlFUS1pEQWJzN2E3SDF5QnYrbUppOVhnNHVLbXlVNnFubjd6RUtqalkyYklsQ3dleng0TExHUDVIRDNacjZmWnNtZEcxVUh6RHNCdllHMm9DSTBvcUVLTSt2YWxzVDZoVThNUmpiUUJEL0pKOEpKTi9pQkdsaEJBVUhvMFVtaGh0UjJldVRWcFpXbE1uaG1wWEFkS0RYU2ZQZGFQb2hnQiIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJpZCI6InRvb2x1XzAxWEVBdWhRSENCZHlDd3R5bVp3RmZEaSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3JlZF90ZWFtIiwidHlwZSI6InRvb2xfdXNlIiwidG9vbHNldF9uYW1lIjpudWxsfV0sImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2Zkd0FzYWJEcXVlZ0hpNllVcENuIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjc1MDksIm91dHB1dF90b2tlbnMiOjg4LCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjo1NX0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoidmVyaWZpZXIiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtNjY4Mi03ZDIxLThjMTUtZmZiY2U5NDIyYzlmLTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoidHJhbnNmZXJfdG9fcmVkX3RlYW0iLCJhcmdzIjp7fSwiaWQiOiJ0b29sdV8wMVhFQXVoUUhDQmR5Q3d0eW1ad0ZmRGkiLCJ0eXBlIjoidG9vbF9jYWxsIn1dLCJpbnZhbGlkX3Rvb2xfY2FsbHMiOltdLCJ1c2FnZV9tZXRhZGF0YSI6eyJpbnB1dF90b2tlbnMiOjc1MDksIm91dHB1dF90b2tlbnMiOjg4LCJ0b3RhbF90b2tlbnMiOjc1OTcsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjU1fX19LHsiY29udGVudCI6IlN1Y2Nlc3NmdWxseSB0cmFuc2ZlcnJlZCB0byByZWRfdGVhbSIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ0cmFuc2Zlcl90b19yZWRfdGVhbSIsImlkIjoiOTgyYzAxMjYtZTZhYi00NThhLTkyMmUtN2IyNjZlODNjMmIyIiwidG9vbF9jYWxsX2lkIjoidG9vbHVfMDFYRUF1aFFIQ0JkeUN3dHltWndGZkRpIiwic3RhdHVzIjoic3VjY2VzcyJ9LHsiY29udGVudCI6W3sic2lnbmF0dXJlIjoiQ0FRU3dnY0tFQWdTR0FJNEFVSUlkR2hwYm10cGJtY1NET1JYZ2U5dUwxODMyUk1Rd0JvTWpRd0F3QUhiOWlQMW9qUEdJakREU2NhbDQ4NmJlQ0pidEc4L0QyWTlnOVM1eElQZmpvSGcvZHh3WDVIeXBTUUVCYmpGaVRTYlBFQ1hTRnpWNllJcTN3YkxjbXlQYnZ1c0duRGV4Rmd4KzNzSVh6dnF5RlYvNjl4aGloTW5TcGZUQ0h5MCswelN1MVdwWVUvV1VwSWZvYVNYVGNUYTFxa004eDhFay95LzFqa0tNWlVmREZhOTN1aFVKeE9DNCtRZ1BXQmwvK1h5MnQ4MkwyeC9lYnE3UUxRVHF0UjBqWGVhVVJRbTNDbzZKdk9nV21Edk1wRWRUZVRXRTNmRGgxdHp6VktzZmx1bTRkODliNkNhS3RuOG5sUWQ3UmhOOTl2TlFNUUhHNVY0T3VWTmRySFBaWnBGT3VoZGVqc01GV1h5Qis1NXNsR0FkOHdFWGdGS2JJL2tuT1hWR3pML05ERjhHdHkxdE5abHUyZmRTQ0FzWVdNc0ZRUDFjYndMZVFHc3p5WHpKTVZaSjJJOXhCUGt5ejFIT1hDUHloNUYrajJnUlRQaWtWQjNBUkM2dGZCL2xEU1VlS0JWWlBRUzhwWS9CNGVDcytJamphQk5XcnVMemZxQ3NjNnhMMEN1UVBTWXlRWGFZRmpZclNSTmNDNzBXUUJXVDVWVmhwMG1BOXlaREdZYlJadDlJNU9LOVFrUW11SjV6REZhTUVPWHpTcnNDNGx0ZVA2TWRpbWlNaWp2c0gzaXErYzZXVVlvbFJqM25lSTg4RWFISXpMQUdScTY0Qk5DTHpkQVlMMnBFVTJzYjV1Rzc4UEtWb1VVQ2JWSkw1anBET2hMT1RPMWJRc1R1b1VwblJSckxRRC9rSXpWK2xqcFZCd3dmSmN5cTc1VEpBL0N0UHIveHIyU1o5QjFiMnNiakc2akQwbGh0b0NkZGVEZ2NxZ2wvTFNTa2N1YVlZOHhQV0N5K0x0TmhjcVRoZm1mdm5ITEpJcEw2Z2pialcvT1FwUmZWVXdmdzFqT2I5YldWd2NnSGhOWWZLWUdQdE04b3RoNXh3TFlSUlZnenFNS092czdmYWxYUmhWeloyU1A2SXcwQk1tQUFaNkhqZkNsOUwrdFBxSk5sblFRT0hwb1dqdW00QitYc1lsZEZFS0dRV1Fxc2Q1OWlOUHZhbzlwL3NDT3Q3ckdxQ296Y21naUVtNHc2NmV6L1FJRHliNDZuMCt0Y05KUFVjb0JLdlEvakR3RTZWd21rUS9uRUNqRnAzVWREVCtrV1lRU05kOGo4VkJaT3BHOExscVVZckVaQ3YwcTdCczF2Y05yZFlpYUZvUzMyb3NheW5XaytIdDNPSHVGV2JFRjhKbHNGVFFBTzA1VnZZa2VjZjBWL29USHNHNWh5T09zN1VFYURGcTd1YUp1eldWaGg1cEFvaklhQmNSOGZWbG4yUExSVFg3aXMybnNBK1FyZGlkZVBjWnRGdGd4QWlLWWUxaTJzU0ZpNHZtOEVPa0NiUTYzd2RRZVk0VXJyNVB1b01Fd3hVVUxqZ0thZ2lBbXNKRHE0UnJWbTdzTzQrNll1QTlqNmwydXlsa09FYzY3dmYvNWZrWHVDc2lBcE5MNmtlYjhjK3ZMMG40RlhHZHpqK2FwU0RmRkdCZ0IiLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsiaWQiOiJ0b29sdV8wMVZCcldkV0VXWWdpdDRrTlllaEJNVTUiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6e30sIm5hbWUiOiJ0cmFuc2Zlcl90b19lZGl0b3IiLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QXpmanFVb3BvRW1aTGFxbTEiLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzQxNiwib3V0cHV0X3Rva2VucyI6MTUxLCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjoxMTl9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InJlZF90ZWFtIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTZjZTYtNzI1MS1iNzk2LTRmNjQzMzNmNjdjNi0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6InRyYW5zZmVyX3RvX2VkaXRvciIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxVkJyV2RXRVdZZ2l0NGtOWWVoQk1VNSIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzQxNiwib3V0cHV0X3Rva2VucyI6MTUxLCJ0b3RhbF90b2tlbnMiOjc1NjcsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjExOX19fSx7ImNvbnRlbnQiOiJTdWNjZXNzZnVsbHkgdHJhbnNmZXJyZWQgdG8gZWRpdG9yIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJ0b29sIiwibmFtZSI6InRyYW5zZmVyX3RvX2VkaXRvciIsImlkIjoiNWQzOWUzNmItYzViNS00NDI2LWE5YWEtYmUyZjdkMzgxNmQ2IiwidG9vbF9jYWxsX2lkIjoidG9vbHVfMDFWQnJXZFdFV1lnaXQ0a05ZZWhCTVU1Iiwic3RhdHVzIjoic3VjY2VzcyJ9LHsiY29udGVudCI6W3sic2lnbmF0dXJlIjoiQ0FRUzVnWUtFQWdTR0FJNEFVSUlkR2hwYm10cGJtY1NESTRRSWI3TDJ3NWFXM2NNL1JvTUxpMndFeERrcWh1eEpEaVlJakFxR3hNNFdVd2lxNHVaSHJpQ2J1QzRrVndhN2lnZFVMb29jWmJpNWZldktiZndxSFNickxOVDUxTkdYNFhzTHRzcWd3WS90YWJIamxsenF1N25SV3Y4K0Z6NFI2bjlqYTdDcGoyNFp1eEZ1S2trUU9CbUE0VGYxYXBrM1JQcWNFUStqK3dlZHUwbE8rcmRFQ3lkM2xYaGZnRVJGRnFjTTZhdWN4ZWw0UmJ3NW9BSFh1OStYZVR0emwrTjh0QWVaOWlHWjJYVHRVNU9iOGFQVDFLdVgrdXA4a1E0VlVYa0JUZmQyZjVKUDlSeFpFcTl3U1lhWnN5SGR2MG4rMVU0NkFPc3ZLWm1wOVFYQndvMmY0TzkwalA4ZWdpcmVtOUViWmJtYVlPSjhXSzdDdEtSVmJZMnk0Kzl4UlVKMDRuc2RJejVGNUZGS0V4eE5PckI0bWMvTkU4M2FiM1o2NGlXek93cW9VR0FHUVNrYUgwZDVjRXVtWm80ZlpaQWhQeXVKZVFEeHVsTUNld243RUdDbnBKdy9uemkwd295RWFRTDRZN2g5OVA3YVh4RnIyOHA1S0N5SkVKVERlZmVJcFZsQXdNQ0Fqeit4QzY5QUFMbXd5VFErY0ZFTStLQWdlSVNwaExWeThBUHIyRlNoYXhCTnJYUWd6MXM5czJkaExaUU9CaXd6aHhtRWdkSzcrOEphcWhCMGVFRWQvMVJWQXVJWWRlekE1SEJyd0U5RHlRaUNsM1VPc2JTbk5FVmUrZ3JIcDVwM3RtTUJxNjhnTFY4Mm9BekZZWmwrQ2V3eDcxdjVkUEZPdktQRUhVenBRWU9WTHJuQjFFWlJZclp6NzJuMndmU1Z4Qk96cFRjbkJ4VjZIZWRDUHlJektYWk05K3lFZVRKaThRdHZWZ1oxdyt2MGhtQ0ZHcG5tWk43bVhsZ3FMMXM2TVhWeWRwUE9LUGxNM05hN0NkSG5QRllpd3Iyekx2a2tjSU03Q3NrL0MzK1pNQVdUSytDOUIxdmJHeFBsdEUzT3Y4a085OFBnU1htMUFKdUViWHkrcjNhM0QzUWxaSjNkRUtlMkxOZEFCeXJQTWhTY0tVOEdsZkgvOU50Z3JodGFrZ2xsRC9NZkFsVHNiTndORHJVdFdSeG1EdUovMForaGtrL1lOd0VEWUFaYThUbXdWb1dPZEJIcFJQSHZaOUU5dXNldWx0bWErSmsxZ1JMRHR2RVprcVhVRVNrTWNxVXRWd1lNNWFqSkE1TjUyY05pWjE1QlRmOFhqR2hLV2RPVjQwKzhIMFBPU3hNMkppZndDTzBDNXhGVzVCbzNJaUlOdjBVT3RZNWY4a3ZQZE0zUGpraGFPcksxeVRBOGVFRTQ0dnVuTDlQa29HZWNiWW0rWEU1S3pMM1FzRW4yWTFJSndPOXcweEhYUVpFZWlQNSswdTNZbkRjOHE5ZEJMaEdXM0Z5MzBnWUFRPT0iLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsiaWQiOiJ0b29sdV8wMUJtOEFVOUVzUVV3cW9WTFlweEtBNzciLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6eyJmaWxlX3BhdGgiOiIvYW5zd2VyLm1kIiwiY29udGVudCI6IioqVHVwbGUgdnMuIGxpc3QgaW1tdXRhYmlsaXR5IChQeXRob24pKipcblxuQSB0dXBsZSBpcyBpbW11dGFibGU6IG9uY2UgY3JlYXRlZCwgeW91IGNhbid0IGFzc2lnbiB0byBpdHMgaXRlbXMsIGFuZCB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGl0IHJlZmVyZW5jZXMgY2FuJ3QgY2hhbmdlLiBgdFswXSA9IDFgIHJhaXNlcyBgVHlwZUVycm9yYC4gQSBsaXN0IGlzIG11dGFibGU6IHlvdSBjYW4gY2hhbmdlIGl0IGluIHBsYWNlIHdpdGggaW5kZXggb3Igc2xpY2UgYXNzaWdubWVudCwgYGFwcGVuZCgpYCwgYGV4dGVuZCgpYCwgYHNvcnQoKWAgYW5kIHNpbWlsYXIgbWV0aG9kcy5cblxuVHVwbGUgaW1tdXRhYmlsaXR5IGlzIHNoYWxsb3cuIElmIGEgdHVwbGUgaG9sZHMgYSBtdXRhYmxlIG9iamVjdCwgc3VjaCBhcyBhIGxpc3QsIHRoYXQgaW5uZXIgb2JqZWN0IGNhbiBzdGlsbCBjaGFuZ2UsIGFuZCBzbyB0aGUgdHVwbGUncyB2YWx1ZSBjaGFuZ2VzLiBCZWNhdXNlIG9mIHRoaXMsIGEgdHVwbGUgY2FuIGJlIGEgYGRpY3RgIGtleSBvciBgc2V0YCBtZW1iZXIgb25seSBpZiBldmVyeXRoaW5nIGluc2lkZSBpdCBpcyBoYXNoYWJsZS4gQSBsaXN0IGNhbiBuZXZlciBiZSBhIGtleS5cblxuU291cmNlczpcbi0gaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy90dXRvcmlhbC9kYXRhc3RydWN0dXJlcy5odG1sXG4tIGh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvcmVmZXJlbmNlL2RhdGFtb2RlbC5odG1sXG4tIGh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvbGlicmFyeS9zdGR0eXBlcy5odG1sXG4ifSwibmFtZSI6IndyaXRlX2ZpbGUiLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QkJhYTlnak1DY2UxNlRVZW4iLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6ODEyNywib3V0cHV0X3Rva2VucyI6NDMzLCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjo3OX0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoiZWRpdG9yIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTc2ZTktN2NhMy1hNWZmLTYzZGU2NmRkOGRkMS0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6IndyaXRlX2ZpbGUiLCJhcmdzIjp7ImZpbGVfcGF0aCI6Ii9hbnN3ZXIubWQiLCJjb250ZW50IjoiKipUdXBsZSB2cy4gbGlzdCBpbW11dGFiaWxpdHkgKFB5dGhvbikqKlxuXG5BIHR1cGxlIGlzIGltbXV0YWJsZTogb25jZSBjcmVhdGVkLCB5b3UgY2FuJ3QgYXNzaWduIHRvIGl0cyBpdGVtcywgYW5kIHRoZSBjb2xsZWN0aW9uIG9mIG9iamVjdHMgaXQgcmVmZXJlbmNlcyBjYW4ndCBjaGFuZ2UuIGB0WzBdID0gMWAgcmFpc2VzIGBUeXBlRXJyb3JgLiBBIGxpc3QgaXMgbXV0YWJsZTogeW91IGNhbiBjaGFuZ2UgaXQgaW4gcGxhY2Ugd2l0aCBpbmRleCBvciBzbGljZSBhc3NpZ25tZW50LCBgYXBwZW5kKClgLCBgZXh0ZW5kKClgLCBgc29ydCgpYCBhbmQgc2ltaWxhciBtZXRob2RzLlxuXG5UdXBsZSBpbW11dGFiaWxpdHkgaXMgc2hhbGxvdy4gSWYgYSB0dXBsZSBob2xkcyBhIG11dGFibGUgb2JqZWN0LCBzdWNoIGFzIGEgbGlzdCwgdGhhdCBpbm5lciBvYmplY3QgY2FuIHN0aWxsIGNoYW5nZSwgYW5kIHNvIHRoZSB0dXBsZSdzIHZhbHVlIGNoYW5nZXMuIEJlY2F1c2Ugb2YgdGhpcywgYSB0dXBsZSBjYW4gYmUgYSBgZGljdGAga2V5IG9yIGBzZXRgIG1lbWJlciBvbmx5IGlmIGV2ZXJ5dGhpbmcgaW5zaWRlIGl0IGlzIGhhc2hhYmxlLiBBIGxpc3QgY2FuIG5ldmVyIGJlIGEga2V5LlxuXG5Tb3VyY2VzOlxuLSBodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3R1dG9yaWFsL2RhdGFzdHJ1Y3R1cmVzLmh0bWxcbi0gaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9yZWZlcmVuY2UvZGF0YW1vZGVsLmh0bWxcbi0gaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9saWJyYXJ5L3N0ZHR5cGVzLmh0bWxcbiJ9LCJpZCI6InRvb2x1XzAxQm04QVU5RXNRVXdxb1ZMWXB4S0E3NyIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6ODEyNywib3V0cHV0X3Rva2VucyI6NDMzLCJ0b3RhbF90b2tlbnMiOjg1NjAsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjc5fX19LHsiY29udGVudCI6IlVwZGF0ZWQgZmlsZSAvYW5zd2VyLm1kIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJ0b29sIiwibmFtZSI6IndyaXRlX2ZpbGUiLCJpZCI6ImUzNDkyZTA4LWM4NjktNDdlYy1hNWY3LWNjNTdlYTVmNjQwMyIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxQm04QVU5RXNRVXdxb1ZMWXB4S0E3NyIsInN0YXR1cyI6InN1Y2Nlc3MifSx7ImNvbnRlbnQiOiJJIHdyb3RlIHRoZSBhbnN3ZXIgdG8gYC9hbnN3ZXIubWRgLiBJdCBpcyBhYm91dCAxMDAgd29yZHMsIHVuZGVyIHRoZSAxMjAtd29yZCBsaW1pdC5cblxuSSBkaWQgbm90IGdldCBhIHNrZXB0aWMgb3IgdmVyaWZpZXIgY2hlY2suIEkgaGFuZGVkIG9mZiB0byBib3RoLCBidXQgbmVpdGhlciByZXR1cm5lZCBhbnkgZmVlZGJhY2suIEkgYWxzbyB0cmllZCB0byBoYW5kIG9mZiB0byBhIGByZWRfdGVhbWAgYWdlbnQsIGFuZCB0aGF0IHRyYW5zZmVyIGlzbid0IG9uZSBvZiB0aGUgdG9vbHMgSSB3YXMgZ2l2ZW4uIEkgdGhlbiB3cm90ZSB0aGUgYW5zd2VyIG15c2VsZiBmcm9tIHRoZSBvZmZpY2lhbCBQeXRob24gZG9jdW1lbnRhdGlvbiBzZWFyY2ggcmVzdWx0cy4gSSBkaWRuJ3Qgb3BlbiB0aGUgcGFnZXMgZGlyZWN0bHkuXG5cbioqVHVwbGUgdnMuIGxpc3QgaW1tdXRhYmlsaXR5IChQeXRob24pKipcblxuQSB0dXBsZSBpcyBpbW11dGFibGU6IG9uY2UgY3JlYXRlZCwgeW91IGNhbid0IGFzc2lnbiB0byBpdHMgaXRlbXMsIGFuZCB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGl0IHJlZmVyZW5jZXMgY2FuJ3QgY2hhbmdlLiBgdFswXSA9IDFgIHJhaXNlcyBgVHlwZUVycm9yYC4gQSBsaXN0IGlzIG11dGFibGU6IHlvdSBjYW4gY2hhbmdlIGl0IGluIHBsYWNlIHdpdGggaW5kZXggb3Igc2xpY2UgYXNzaWdubWVudCwgYGFwcGVuZCgpYCwgYGV4dGVuZCgpYCwgYHNvcnQoKWAgYW5kIHNpbWlsYXIgbWV0aG9kcy5cblxuVHVwbGUgaW1tdXRhYmlsaXR5IGlzIHNoYWxsb3cuIElmIGEgdHVwbGUgaG9sZHMgYSBtdXRhYmxlIG9iamVjdCwgc3VjaCBhcyBhIGxpc3QsIHRoYXQgaW5uZXIgb2JqZWN0IGNhbiBzdGlsbCBjaGFuZ2UsIGFuZCBzbyB0aGUgdHVwbGUncyB2YWx1ZSBjaGFuZ2VzLiBCZWNhdXNlIG9mIHRoaXMsIGEgdHVwbGUgY2FuIGJlIGEgYGRpY3RgIGtleSBvciBgc2V0YCBtZW1iZXIgb25seSBpZiBldmVyeXRoaW5nIGluc2lkZSBpdCBpcyBoYXNoYWJsZS4gQSBsaXN0IGNhbiBuZXZlciBiZSBhIGtleS5cblxuU291cmNlczpcbi0gaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy90dXRvcmlhbC9kYXRhc3RydWN0dXJlcy5odG1sXG4tIGh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvcmVmZXJlbmNlL2RhdGFtb2RlbC5odG1sXG4tIGh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvbGlicmFyeS9zdGR0eXBlcy5odG1sIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QlVzTmhyMTlnUU5WY0ZzVWUiLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoiZW5kX3R1cm4iLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6ODQ5NCwib3V0cHV0X3Rva2VucyI6NDE1LCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjowfSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCIsIm1vZGVsX3Byb3ZpZGVyIjoiYW50aHJvcGljIn0sInR5cGUiOiJhaSIsIm5hbWUiOiJlZGl0b3IiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtODZiYi03NjYxLWE1OWYtYTM5ZDZiNGU2Yjc0LTAiLCJ0b29sX2NhbGxzIjpbXSwiaW52YWxpZF90b29sX2NhbGxzIjpbXSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjo4NDk0LCJvdXRwdXRfdG9rZW5zIjo0MTUsInRvdGFsX3Rva2VucyI6ODkwOSwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fV19" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "5eb6729648b8a8b0", + "parentSpanId": "4809a44f637a7046", + "name": "model", + "kind": 1, + "startTimeUnixNano": "1790968039096445952", + "endTimeUnixNano": "1790968042917764864", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "3" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "editor:42b8f95e-d208-6d0d-c59e-cc043f552134|model:d639a607-1d3b-b4dc-823b-564098184f56" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "editor" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "editor:42b8f95e-d208-6d0d-c59e-cc043f552134" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "graph:step:3" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W3siY29udGVudCI6IkZvciBhIHN5bnRoZXRpYyB0cmFjaW5nIGZpeHR1cmUsIGV4cGxhaW4gUHl0aG9uIHR1cGxlIHZlcnN1cyBsaXN0IGltbXV0YWJpbGl0eSBpbiBhdCBtb3N0IDEyMCB3b3Jkcy4gVXNlIG9ubHkgb2ZmaWNpYWwgUHl0aG9uIGRvY3VtZW50YXRpb24gaWYgeW91IHNlYXJjaC4gSGF2ZSB0aGUgc2tlcHRpYyBhbmQgdmVyaWZpZXIgY2hlY2sgaXQsIHRoZW4gaGF2ZSB0aGUgZWRpdG9yIHdyaXRlIC9hbnN3ZXIubWQuIEtlZXAgdGhlIHJ1biBzaG9ydCBhbmQgZG8gbm90IGFzayBmb2xsb3ctdXAgcXVlc3Rpb25zLiIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoiaHVtYW4iLCJpZCI6IjQ2MjA0MzA1LWY4ZWUtNDM4NC05Y2U4LWZjZDgwYmZjZGY3NSJ9LHsiY29udGVudCI6W3siaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9LCJuYW1lIjoid2ViX3NlYXJjaCIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHc5YVc5YnJlblBxZ3liZk53UCIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjoyODQ1LCJvdXRwdXRfdG9rZW5zIjo3Mywib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6MH0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoicmVzZWFyY2hlciIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy0yMWEzLTcwMDAtYTM4MS0yODRhNmEwYzAzZjctMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ3ZWJfc2VhcmNoIiwiYXJncyI6eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9LCJpZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6Mjg0NSwib3V0cHV0X3Rva2VucyI6NzMsInRvdGFsX3Rva2VucyI6MjkxOCwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fSx7ImNvbnRlbnQiOiJbe1widGl0bGVcIjogXCI1LiBEYXRhIFN0cnVjdHVyZXMgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3R1dG9yaWFsL2RhdGFzdHJ1Y3R1cmVzLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcIllvdSBtaWdodCBoYXZlIG5vdGljZWQgdGhhdCBtZXRob2RzIGxpa2VgaW5zZXJ0YCxgcmVtb3ZlYCBvcmBzb3J0YCB0aGF0IG9ubHkgbW9kaWZ5IHRoZSBsaXN0IGhhdmUgbm8gcmV0dXJuIHZhbHVlIHByaW50ZWQgXFx1MjAxMyB0aGV5IHJldHVybiB0aGUgZGVmYXVsdGBOb25lYC4gWzFdIFRoaXMgaXMgYSBkZXNpZ24gcHJpbmNpcGxlIGZvciBhbGwgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgaW4gUHl0aG9uLlxcbi4uLlxcbiMjIDUuMy4gVHVwbGVzIGFuZCBTZXF1ZW5jZXNcXHUwMGI2XFxuXFxuV2Ugc2F3IHRoYXQgbGlzdHMgYW5kIHN0cmluZ3MgaGF2ZSBtYW55IGNvbW1vbiBwcm9wZXJ0aWVzLCBzdWNoIGFzIGluZGV4aW5nIGFuZCBzbGljaW5nIG9wZXJhdGlvbnMuIFRoZXkgYXJlIHR3byBleGFtcGxlcyBvZiBzZXF1ZW5jZSBkYXRhIHR5cGVzIChzZWUgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBsaXN0LCB0dXBsZSwgcmFuZ2UpLiBTaW5jZSBQeXRob24gaXMgYW4gZXZvbHZpbmcgbGFuZ3VhZ2UsIG90aGVyIHNlcXVlbmNlIGRhdGEgdHlwZXMgbWF5IGJlIGFkZGVkLiBUaGVyZSBpcyBhbHNvIGFub3RoZXIgc3RhbmRhcmQgc2VxdWVuY2UgZGF0YSB0eXBlOiB0aGUgdHVwbGUuXFxuXFxuQSB0dXBsZSBjb25zaXN0cyBvZiBhIG51bWJlciBvZiB2YWx1ZXMgc2VwYXJhdGVkIGJ5IGNvbW1hcywgZm9yIGluc3RhbmNlOlxcbi4uLlxcbj4+PiAjIFR1cGxlcyBhcmUgaW1tdXRhYmxlOlxcbj4+PiB0WzBdID0gODg4ODhcXG5UcmFjZWJhY2sgKG1vc3QgcmVjZW50IGNhbGwgbGFzdCk6XFxuICBGaWxlIFxcXCI8c3RkaW4+XFxcIiwgbGluZSAxLCBpbiA8bW9kdWxlPlxcblR5cGVFcnJvcjogJ3R1cGxlJyBvYmplY3QgZG9lcyBub3Qgc3VwcG9ydCBpdGVtIGFzc2lnbm1lbnRcXG4+Pj4gIyBidXQgdGhleSBjYW4gY29udGFpbiBtdXRhYmxlIG9iamVjdHM6XFxuPj4+IHYgPSAoWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuPj4+IHZcXG4oWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuLi4uXFxuQXMgeW91IHNlZSwgb24gb3V0cHV0IHR1cGxlcyBhcmUgYWx3YXlzIGVuY2xvc2VkIGluIHBhcmVudGhlc2VzLCBzbyB0aGF0IG5lc3RlZCB0dXBsZXMgYXJlIGludGVycHJldGVkIGNvcnJlY3RseTsgdGhleSBtYXkgYmUgaW5wdXQgd2l0aCBvciB3aXRob3V0IHN1cnJvdW5kaW5nIHBhcmVudGhlc2VzLCBhbHRob3VnaCBvZnRlbiBwYXJlbnRoZXNlcyBhcmUgbmVjZXNzYXJ5IGFueXdheSAoaWYgdGhlIHR1cGxlIGlzIHBhcnQgb2YgYSBsYXJnZXIgZXhwcmVzc2lvbikuIEl0IGlzIG5vdCBwb3NzaWJsZSB0byBhc3NpZ24gdG8gdGhlIGluZGl2aWR1YWwgaXRlbXMgb2YgYSB0dXBsZSwgaG93ZXZlciBpdCBpcyBwb3NzaWJsZSB0byBjcmVhdGUgdHVwbGVzIHdoaWNoIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzLCBzdWNoIGFzIGxpc3RzLlxcblxcblRob3VnaCB0dXBsZXMgbWF5IHNlZW0gc2ltaWxhciB0byBsaXN0cywgdGhleSBhcmUgb2Z0ZW4gdXNlZCBpbiBkaWZmZXJlbnQgc2l0dWF0aW9ucyBhbmQgZm9yIGRpZmZlcmVudCBwdXJwb3Nlcy4gVHVwbGVzIGFyZSBpbW11dGFibGUsIGFuZCB1c3VhbGx5IGNvbnRhaW4gYSBoZXRlcm9nZW5lb3VzIHNlcXVlbmNlIG9mIGVsZW1lbnRzIHRoYXQgYXJlIGFjY2Vzc2VkIHZpYSB1bnBhY2tpbmcgKHNlZSBsYXRlciBpbiB0aGlzIHNlY3Rpb24pIG9yIGluZGV4aW5nIChvciBldmVuIGJ5IGF0dHJpYnV0ZSBpbiB0aGUgY2FzZSBvZiBuYW1lZHR1cGxlcykuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG5Bbm90aGVyIHVzZWZ1bCBkYXRhIHR5cGUgYnVpbHQgaW50byBQeXRob24gaXMgdGhlIGRpY3Rpb25hcnkgKHNlZSBNYXBwaW5nIFR5cGVzIFxcdTIwMTQgZGljdCkuIERpY3Rpb25hcmllcyBhcmUgc29tZXRpbWVzIGZvdW5kIGluIG90aGVyIGxhbmd1YWdlcyBhcyBcXHUyMDFjYXNzb2NpYXRpdmUgbWVtb3JpZXNcXHUyMDFkIG9yIFxcdTIwMWNhc3NvY2lhdGl2ZSBhcnJheXNcXHUyMDFkLiBVbmxpa2Ugc2VxdWVuY2VzLCB3aGljaCBhcmUgaW5kZXhlZCBieSBhIHJhbmdlIG9mIG51bWJlcnMsIGRpY3Rpb25hcmllcyBhcmUgaW5kZXhlZCBieSBrZXlzLCB3aGljaCBjYW4gYmUgYW55IGltbXV0YWJsZSB0eXBlOyBzdHJpbmdzIGFuZCBudW1iZXJzIGNhbiBhbHdheXMgYmUga2V5cy4gVHVwbGVzIGNhbiBiZSB1c2VkIGFzIGtleXMgaWYgdGhleSBjb250YWluIG9ubHkgc3RyaW5ncywgbnVtYmVycywgb3IgdHVwbGVzOyBpZiBhIHR1cGxlIGNvbnRhaW5zIGFueSBtdXRhYmxlIG9iamVjdCBlaXRoZXIgZGlyZWN0bHkgb3IgaW5kaXJlY3RseSwgaXQgY2Fubm90IGJlIHVzZWQgYXMgYSBrZXkuIFlvdSBjYW5cXHUyMDE5dCB1c2UgbGlzdHMgYXMga2V5cywgc2luY2UgbGlzdHMgY2FuIGJlIG1vZGlmaWVkIGluIHBsYWNlIHVzaW5nIGluZGV4IGFzc2lnbm1lbnRzLCBzbGljZSBhc3NpZ25tZW50cywgb3IgbWV0aG9kcyBsaWtlIGFwcGVuZCgpIGFuZCBleHRlbmQoKS5cIl19LCB7XCJ0aXRsZVwiOiBcIjMuIERhdGEgbW9kZWwgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3JlZmVyZW5jZS9kYXRhbW9kZWwuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiVGhlIHZhbHVlIG9mIHNvbWUgb2JqZWN0cyBjYW4gY2hhbmdlLiBPYmplY3RzIHdob3NlIHZhbHVlIGNhbiBjaGFuZ2UgYXJlIHNhaWQgdG8gYmUgbXV0YWJsZTsgb2JqZWN0cyB3aG9zZSB2YWx1ZSBpcyB1bmNoYW5nZWFibGUgb25jZSB0aGV5IGFyZSBjcmVhdGVkIGFyZSBjYWxsZWQgaW1tdXRhYmxlLiAoVGhlIHZhbHVlIG9mIGFuIGltbXV0YWJsZSBjb250YWluZXIgb2JqZWN0IHRoYXQgY29udGFpbnMgYSByZWZlcmVuY2UgdG8gYSBtdXRhYmxlIG9iamVjdCBjYW4gY2hhbmdlIHdoZW4gdGhlIGxhdHRlclxcdTIwMTlzIHZhbHVlIGlzIGNoYW5nZWQ7IGhvd2V2ZXIgdGhlIGNvbnRhaW5lciBpcyBzdGlsbCBjb25zaWRlcmVkIGltbXV0YWJsZSwgYmVjYXVzZSB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGl0IGNvbnRhaW5zIGNhbm5vdCBiZSBjaGFuZ2VkLiBTbywgaW1tdXRhYmlsaXR5IGlzIG5vdCBzdHJpY3RseSB0aGUgc2FtZSBhcyBoYXZpbmcgYW4gdW5jaGFuZ2VhYmxlIHZhbHVlLCBpdCBpcyBtb3JlIHN1YnRsZS4pIEFuIG9iamVjdFxcdTIwMTlzIG11dGFiaWxpdHkgaXMgZGV0ZXJtaW5lZCBieSBpdHMgdHlwZTsgZm9yIGluc3RhbmNlLCBudW1iZXJzLCBzdHJpbmdzIGFuZCB0dXBsZXMgYXJlIGltbXV0YWJsZSwgd2hpbGUgZGljdGlvbmFyaWVzIGFuZCBsaXN0cyBhcmUgbXV0YWJsZS5cXG4uLi5cXG5Tb21lIG9iamVjdHMgY29udGFpbiByZWZlcmVuY2VzIHRvIG90aGVyIG9iamVjdHM7IHRoZXNlIGFyZSBjYWxsZWQgY29udGFpbmVycy4gRXhhbXBsZXMgb2YgY29udGFpbmVycyBhcmUgdHVwbGVzLCBsaXN0cyBhbmQgZGljdGlvbmFyaWVzLiBUaGUgcmVmZXJlbmNlcyBhcmUgcGFydCBvZiBhIGNvbnRhaW5lclxcdTIwMTlzIHZhbHVlLiBJbiBtb3N0IGNhc2VzLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIHZhbHVlIG9mIGEgY29udGFpbmVyLCB3ZSBpbXBseSB0aGUgdmFsdWVzLCBub3QgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGNvbnRhaW5lZCBvYmplY3RzOyBob3dldmVyLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIG11dGFiaWxpdHkgb2YgYSBjb250YWluZXIsIG9ubHkgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGltbWVkaWF0ZWx5IGNvbnRhaW5lZCBvYmplY3RzIGFyZSBpbXBsaWVkLiBTbywgaWYgYW4gaW1tdXRhYmxlIGNvbnRhaW5lciAobGlrZSBhIHR1cGxlKSBjb250YWlucyBhIHJlZmVyZW5jZSB0byBhIG11dGFibGUgb2JqZWN0LCBpdHMgdmFsdWUgY2hhbmdlcyBpZiB0aGF0IG11dGFibGUgb2JqZWN0IGlzIGNoYW5nZWQuXFxuLi4uXFxuIyMjIDMuMi41LiBTZXF1ZW5jZXNcXHUwMGI2XFxuLi4uXFxuIyMjIyAzLjIuNS4xLiBJbW11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbkFuIG9iamVjdCBvZiBhbiBpbW11dGFibGUgc2VxdWVuY2UgdHlwZSBjYW5ub3QgY2hhbmdlIG9uY2UgaXQgaXMgY3JlYXRlZC4gKElmIHRoZSBvYmplY3QgY29udGFpbnMgcmVmZXJlbmNlcyB0byBvdGhlciBvYmplY3RzLCB0aGVzZSBvdGhlciBvYmplY3RzIG1heSBiZSBtdXRhYmxlIGFuZCBtYXkgYmUgY2hhbmdlZDsgaG93ZXZlciwgdGhlIGNvbGxlY3Rpb24gb2Ygb2JqZWN0cyBkaXJlY3RseSByZWZlcmVuY2VkIGJ5IGFuIGltbXV0YWJsZSBvYmplY3QgY2Fubm90IGNoYW5nZS4pXFxuXFxuVGhlIGZvbGxvd2luZyB0eXBlcyBhcmUgaW1tdXRhYmxlIHNlcXVlbmNlczpcXG4uLi5cXG5UdXBsZXNcXG46IFRoZSBpdGVtcyBvZiBhIGB0dXBsZWAgYXJlIGFyYml0cmFyeSBQeXRob24gb2JqZWN0cy4gVHVwbGVzIG9mIHR3byBvciBtb3JlIGl0ZW1zIGFyZSBmb3JtZWQgYnkgY29tbWEtc2VwYXJhdGVkIGxpc3RzIG9mIGV4cHJlc3Npb25zLiBBIHR1cGxlIG9mIG9uZSBpdGVtIChhIFxcdTIwMThzaW5nbGV0b25cXHUyMDE5KSBjYW4gYmUgZm9ybWVkIGJ5IGFmZml4aW5nIGEgY29tbWEgdG8gYW4gZXhwcmVzc2lvbiAoYW4gZXhwcmVzc2lvbiBieSBpdHNlbGYgZG9lcyBub3QgY3JlYXRlIGEgdHVwbGUsIHNpbmNlIHBhcmVudGhlc2VzIG11c3QgYmUgdXNhYmxlIGZvciBncm91cGluZyBvZiBleHByZXNzaW9ucykuIEFuIGVtcHR5IHR1cGxlIGNhbiBiZSBmb3JtZWQgYnkgYW4gZW1wdHkgcGFpciBvZiBwYXJlbnRoZXNlcy5cXG4uLi5cXG4jIyMjIDMuMi41LjIuIE11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbk11dGFibGUgc2VxdWVuY2VzIGNhbiBiZSBjaGFuZ2VkIGFmdGVyIHRoZXkgYXJlIGNyZWF0ZWQuIFRoZSBzdWJzY3JpcHRpb24gYW5kIHNsaWNpbmcgbm90YXRpb25zIGNhbiBiZSB1c2VkIGFzIHRoZSB0YXJnZXQgb2YgYXNzaWdubWVudCBhbmQgYGRlbGAgKGRlbGV0ZSkgc3RhdGVtZW50cy5cXG4uLi5cXG5UaGVyZSBhcmUgY3VycmVudGx5IHR3byBpbnRyaW5zaWMgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlczpcXG5cXG5MaXN0c1xcbjogVGhlIGl0ZW1zIG9mIGEgbGlzdCBhcmUgYXJiaXRyYXJ5IFB5dGhvbiBvYmplY3RzLiBMaXN0cyBhcmUgZm9ybWVkIGJ5IHBsYWNpbmcgYSBjb21tYS1zZXBhcmF0ZWQgbGlzdCBvZiBleHByZXNzaW9ucyBpbiBzcXVhcmUgYnJhY2tldHMuIChOb3RlIHRoYXQgdGhlcmUgYXJlIG5vIHNwZWNpYWwgY2FzZXMgbmVlZGVkIHRvIGZvcm0gbGlzdHMgb2YgbGVuZ3RoIDAgb3IgMS4pXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjcgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvYnVpbHRpbnMvc3RkdHlwZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiY29sbGVjdGlvbiBjbGFzc2VzIGFyZSBtdXRhYmxlLiBUaGUgbWV0aG9kcyB0aGF0IGFkZCwgc3VidHJhY3QsIG9yIHJlYXJyYW5nZSB0aGVpciAuLi4gaW4gcGxhY2UsIGFuZCBkb25cXHUyMDE5dCByZXR1cm4gYSAuLi4gLCBuZXZlciByZXR1cm4gdGhlIGNvbGxlY3Rpb24gaW5zdGFuY2UgaXRzZWxmIGJ1dCBgTm9uZWAuXFxuLi4uXFxuIyMgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBgbGlzdGAsIGB0dXBsZWAsIGByYW5nZWBcXHUwMGI2XFxuXFxuVGhlcmUgYXJlIHRocmVlIGJhc2ljIHNlcXVlbmNlIHR5cGVzOiBsaXN0cywgdHVwbGVzLCBhbmQgcmFuZ2Ugb2JqZWN0cy4gQWRkaXRpb25hbCBzZXF1ZW5jZSB0eXBlcyB0YWlsb3JlZCBmb3IgcHJvY2Vzc2luZyBvZiBiaW5hcnkgZGF0YSBhbmQgdGV4dCBzdHJpbmdzIGFyZSBkZXNjcmliZWQgaW4gZGVkaWNhdGVkIHNlY3Rpb25zLlxcbi4uLlxcblRoZSBvcGVyYXRpb25zIGluIHRoZSBmb2xsb3dpbmcgdGFibGUgLi4uIG11dGFibGUgYW5kIGltbXV0YWJsZS4gVGhlIGBjb2xsZWN0aW9ucy4gLi4uIGlzIHByb3ZpZGVkIHRvIG1ha2UgLi4uIGVhc2llciB0byBjb3JyZWN0bHkgaW1wbGVtZW50IHRoZXNlIG9wZXJhdGlvbnMgb24gY3VzdG9tIHNlcXVlbmNlIHR5cGVzXFxuLi4uXFxuIyMjIEltbXV0YWJsZSBTZXF1ZW5jZSBUeXBlc1xcdTAwYjZcXG4uLi5cXG5tdXRhYmxlIHNlcXVlbmNlIHR5cGVzIGlzXFxuLi4uXFxuc3VwcG9ydCBhbGxvd3MgaW1tdXRhYmxlIHNlcXVlbmNlcywgLi4uICwgdG8gYmUgdXNlZCBhcyBgZGljdGAga2V5cyBhbmQgc3RvcmVkIGluIC4uLiBlbnNldGAgaW5zdGFuY2VzXFxuLi4uXFxuIyMjIE11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxuIyMjIExpc3RzXFx1MDBiNlxcblxcbkxpc3RzIGFyZSBtdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaG9tb2dlbmVvdXMgaXRlbXMgKHdoZXJlIHRoZSBwcmVjaXNlIGRlZ3JlZSBvZiBzaW1pbGFyaXR5IHdpbGwgdmFyeSBieSBhcHBsaWNhdGlvbikuXFxuLi4uXFxuIyMjIFR1cGxlc1xcdTAwYjZcXG5cXG5UdXBsZXMgYXJlIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhldGVyb2dlbmVvdXMgZGF0YSAoc3VjaCBhcyB0aGUgMi10dXBsZXMgcHJvZHVjZWQgYnkgdGhlIGBlbnVtZXJhdGUoKWAgYnVpbHQtaW4pLiBUdXBsZXMgYXJlIGFsc28gdXNlZCBmb3IgY2FzZXMgd2hlcmUgYW4gaW1tdXRhYmxlIHNlcXVlbmNlIG9mIGhvbW9nZW5lb3VzIGRhdGEgaXMgbmVlZGVkIChzdWNoIGFzIGFsbG93aW5nIHN0b3JhZ2UgaW4gYSBgc2V0YCBvciBgZGljdGAgaW5zdGFuY2UpLlxcbi4uLlxcbnR1cGxlKGl0ZXJhYmxlPVxcbi4uLlxcblRoZSBjb25zdHJ1Y3RvciBidWlsZHMgYSB0dXBsZSB3aG9zZSBpdGVtcyBhcmUgdGhlIHNhbWUgYW5kIGluIHRoZSBzYW1lIG9yZGVyIGFzIGl0ZXJhYmxlXFx1MjAxOXMgaXRlbXMuIGl0ZXJhYmxlIG1heSBiZSBlaXRoZXIgYSBzZXF1ZW5jZSwgYSBjb250YWluZXIgdGhhdCBzdXBwb3J0cyBpdGVyYXRpb24sIG9yIGFuIGl0ZXJhdG9yIG9iamVjdC4gSWYgaXRlcmFibGUgaXMgYWxyZWFkeSBhIHR1cGxlLCBpdCBpcyByZXR1cm5lZCB1bmNoYW5nZWQuIEZvciBleGFtcGxlLCBgdHVwbGUoJ2FiYycpYCByZXR1cm5zIGAoJ2EnLCAnYicsICdjJylgIGFuZCBgdHVwbGUoIFsxLCAyLCAzXSApYCByZXR1cm5zIGAoMSwgMiwgMylgLiBJZiBubyBhcmd1bWVudCBpcyBnaXZlbiwgdGhlIGNvbnN0cnVjdG9yIGNyZWF0ZXMgYSBuZXcgZW1wdHkgdHVwbGUsIGAoKWAuXFxuLi4uXFxuVHVwbGVzIGltcGxlbWVudCBhbGwgb2YgdGhlIGNvbW1vbiBzZXF1ZW5jZSBvcGVyYXRpb25zLlxcbi4uLlxcbkZvciBoZXRlcm9nZW5lb3VzIGNvbGxlY3Rpb25zIG9mIGRhdGEgd2hlcmUgYWNjZXNzIGJ5IG5hbWUgaXMgY2xlYXJlciB0aGFuIGFjY2VzcyBieSBpbmRleCwgYGNvbGxlY3Rpb25zLm5hbWVkdHVwbGUoKWAgbWF5IGJlIGEgbW9yZSBhcHByb3ByaWF0ZSBjaG9pY2UgdGhhbiBhIHNpbXBsZSB0dXBsZSBvYmplY3QuXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvbGlicmFyeS9zdGR0eXBlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJTb21lIGNvbGxlY3Rpb24gY2xhc3NlcyBhcmUgbXV0YWJsZS4gVGhlIG1ldGhvZHMgdGhhdCBhZGQsIHN1YnRyYWN0LCBvciByZWFycmFuZ2UgdGhlaXIgLi4uIGluIHBsYWNlLCBhbmQgZG9uXFx1MjAxOXQgcmV0dXJuIGEgLi4uICwgbmV2ZXIgcmV0dXJuIHRoZSBjb2xsZWN0aW9uIGluc3RhbmNlIGl0c2VsZiBidXQgYE5vbmVgLlxcbi4uLlxcbiMjIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgYGxpc3RgLCBgdHVwbGVgLCBgcmFuZ2VgXFx1MDBiNlxcblxcblRoZXJlIGFyZSB0aHJlZSBiYXNpYyBzZXF1ZW5jZSB0eXBlczogbGlzdHMsIHR1cGxlcywgYW5kIC4uLiBvYmplY3RzLiBBZGRpdGlvbmFsIHNlcXVlbmNlIHR5cGVzIHRhaWxvcmVkIGZvciBwcm9jZXNzaW5nIG9mIGJpbmFyeSBkYXRhIGFuZCB0ZXh0IHN0cmluZ3MgYXJlIGRlc2NyaWJlZCBpbiBkZWRpY2F0ZWQgc2VjdGlvbnMuXFxuLi4uXFxuVGhlIG9wZXJhdGlvbnMgaW4gdGhlIGZvbGxvd2luZyB0YWJsZSAuLi4gaW1tdXRhYmxlLiBUaGUgYCAuLi4gaXMgcHJvdmlkZWQgdG8gbWFrZSAuLi4gZWFzaWVyIHRvIGNvcnJlY3RseSBpbXBsZW1lbnQgdGhlc2Ugb3BlcmF0aW9ucyBvbiBjdXN0b20gc2VxdWVuY2UgdHlwZXNcXG4uLi5cXG4jIyMgSW1tdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbm9wZXJhdGlvbiB0aGF0IGltbXV0YWJsZSBzZXF1ZW5jZSAuLi4gYnkgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlcyBpcyAuLi4gYGhhc2goKWBcXG4uLi5cXG5UaGlzIHN1cHBvcnQgYWxsb3dzIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHN1Y2ggYXMgYHR1cGxlYCBpbnN0YW5jZXMsIHRvIGJlIHVzZWQgYXMgYGRpY3RgIGtleXMgYW5kIHN0b3JlZCBpbiBgc2V0YCBhbmQgYGZyb3plbnNldGAgaW5zdGFuY2VzLlxcbi4uLlxcbiMjIyBNdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbiMjIyBMaXN0c1xcdTAwYjZcXG5cXG5MaXN0cyBhcmUgbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhvbW9nZW5lb3VzIGl0ZW1zICh3aGVyZSB0aGUgcHJlY2lzZSBkZWdyZWUgb2Ygc2ltaWxhcml0eSB3aWxsIHZhcnkgYnkgYXBwbGljYXRpb24pLlxcbi4uLlxcbiMjIyBUdXBsZXNcXHUwMGI2XFxuXFxuVHVwbGVzIGFyZSBpbW11dGFibGUgc2VxdWVuY2VzLCB0eXBpY2FsbHkgdXNlZCB0byBzdG9yZSBjb2xsZWN0aW9ucyBvZiBoZXRlcm9nZW5lb3VzIGRhdGEgKHN1Y2ggYXMgdGhlIDItdHVwbGVzIHByb2R1Y2VkIGJ5IHRoZSBgZW51bWVyYXRlKClgIGJ1aWx0LWluKS4gVHVwbGVzIGFyZSBhbHNvIHVzZWQgZm9yIGNhc2VzIHdoZXJlIGFuIGltbXV0YWJsZSBzZXF1ZW5jZSBvZiBob21vZ2VuZW91cyBkYXRhIGlzIG5lZWRlZCAoc3VjaCBhcyBhbGxvd2luZyBzdG9yYWdlIGluIGEgYHNldGAgb3IgYGRpY3RgIGluc3RhbmNlKS5cXG4uLi5cXG5UaGUgY29uc3RydWN0b3IgYnVpbGRzIGEgdHVwbGUgd2hvc2UgaXRlbXMgYXJlIHRoZSBzYW1lIGFuZCBpbiB0aGUgc2FtZSBvcmRlciBhcyBpdGVyYWJsZVxcdTIwMTlzIGl0ZW1zLiBpdGVyYWJsZSBtYXkgYmUgZWl0aGVyIGEgc2VxdWVuY2UsIGEgY29udGFpbmVyIHRoYXQgc3VwcG9ydHMgaXRlcmF0aW9uLCBvciBhbiBpdGVyYXRvciBvYmplY3QuIElmIGl0ZXJhYmxlIGlzIGFscmVhZHkgYSB0dXBsZSwgaXQgaXMgcmV0dXJuZWQgdW5jaGFuZ2VkLiBGb3IgZXhhbXBsZSwgYHR1cGxlKCdhYmMnKWAgcmV0dXJucyBgKCdhJywgJ2InLCAnYycpYCBhbmQgYHR1cGxlKCBbMSwgMiwgM10gKWAgcmV0dXJucyBgKDEsIDIsIDMpYC4gSWYgbm8gYXJndW1lbnQgaXMgZ2l2ZW4sIHRoZSBjb25zdHJ1Y3RvciBjcmVhdGVzIGEgbmV3IGVtcHR5IHR1cGxlLCBgKClgLlxcbi4uLlxcblR1cGxlcyBpbXBsZW1lbnQgYWxsIG9mIHRoZSBjb21tb24gc2VxdWVuY2Ugb3BlcmF0aW9ucy5cXG4uLi5cXG5Gb3IgaGV0ZXJvZ2VuZW91cyBjb2xsZWN0aW9ucyBvZiBkYXRhIHdoZXJlIGFjY2VzcyBieSBuYW1lIGlzIGNsZWFyZXIgdGhhbiBhY2Nlc3MgYnkgaW5kZXgsIGBjb2xsZWN0aW9ucy5uYW1lZHR1cGxlKClgIG1heSBiZSBhIG1vcmUgYXBwcm9wcmlhdGUgY2hvaWNlIHRoYW4gYSBzaW1wbGUgdHVwbGUgb2JqZWN0LlwiXX0sIHtcInRpdGxlXCI6IFwiNS4gRGF0YSBTdHJ1Y3R1cmVzIFxcdTIwMTQgUHl0aG9uIDMuMTAuMjAgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMuMTAvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiWW91IG1pZ2h0IGhhdmUgbm90aWNlZCB0aGF0IG1ldGhvZHMgbGlrZSBgaW5zZXJ0YCwgYHJlbW92ZWAgb3IgYHNvcnRgIHRoYXQgb25seSBtb2RpZnkgdGhlIGxpc3QgaGF2ZSBubyByZXR1cm4gdmFsdWUgcHJpbnRlZCBcXHUyMDEzIHRoZXkgcmV0dXJuIHRoZSBkZWZhdWx0IGBOb25lYC4gMSBUaGlzIGlzIGEgZGVzaWduIHByaW5jaXBsZSBmb3IgYWxsIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIGluIFB5dGhvbi5cXG4uLi5cXG4jIyA1LjMuIFR1cGxlcyBhbmQgU2VxdWVuY2VzXFx1MDBiNlxcblxcbldlIHNhdyB0aGF0IGxpc3RzIGFuZCBzdHJpbmdzIGhhdmUgbWFueSBjb21tb24gcHJvcGVydGllcywgc3VjaCBhcyBpbmRleGluZyBhbmQgc2xpY2luZyBvcGVyYXRpb25zLiBUaGV5IGFyZSB0d28gZXhhbXBsZXMgb2Ygc2VxdWVuY2UgZGF0YSB0eXBlcyAoc2VlIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgbGlzdCwgdHVwbGUsIHJhbmdlKS4gU2luY2UgUHl0aG9uIGlzIGFuIGV2b2x2aW5nIGxhbmd1YWdlLCBvdGhlciBzZXF1ZW5jZSBkYXRhIHR5cGVzIG1heSBiZSBhZGRlZC4gVGhlcmUgaXMgYWxzbyBhbm90aGVyIHN0YW5kYXJkIHNlcXVlbmNlIGRhdGEgdHlwZTogdGhlIHR1cGxlLlxcblxcbkEgdHVwbGUgY29uc2lzdHMgb2YgYSBudW1iZXIgb2YgdmFsdWVzIHNlcGFyYXRlZCBieSBjb21tYXMsIGZvciBpbnN0YW5jZTpcXG4uLi5cXG4+Pj4gIyBUdXBsZXMgYXJlIGltbXV0YWJsZTpcXG4uLi4gdFswXSA9IDg4ODg4XFxuVHJhY2ViYWNrIChtb3N0IHJlY2VudCBjYWxsIGxhc3QpOlxcbiAgRmlsZSBcXFwiPHN0ZGluPlxcXCIsIGxpbmUgMSwgaW4gPG1vZHVsZT5cXG5UeXBlRXJyb3I6ICd0dXBsZScgb2JqZWN0IGRvZXMgbm90IHN1cHBvcnQgaXRlbSBhc3NpZ25tZW50XFxuPj4+ICMgYnV0IHRoZXkgY2FuIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzOlxcbi4uLiB2ID0gKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbj4+PiB2XFxuKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbi4uLlxcbkFzIHlvdSBzZWUsIG9uIG91dHB1dCB0dXBsZXMgYXJlIGFsd2F5cyBlbmNsb3NlZCBpbiBwYXJlbnRoZXNlcywgc28gdGhhdCBuZXN0ZWQgdHVwbGVzIGFyZSBpbnRlcnByZXRlZCBjb3JyZWN0bHk7IHRoZXkgbWF5IGJlIGlucHV0IHdpdGggb3Igd2l0aG91dCBzdXJyb3VuZGluZyBwYXJlbnRoZXNlcywgYWx0aG91Z2ggb2Z0ZW4gcGFyZW50aGVzZXMgYXJlIG5lY2Vzc2FyeSBhbnl3YXkgKGlmIHRoZSB0dXBsZSBpcyBwYXJ0IG9mIGEgbGFyZ2VyIGV4cHJlc3Npb24pLiBJdCBpcyBub3QgcG9zc2libGUgdG8gYXNzaWduIHRvIHRoZSBpbmRpdmlkdWFsIGl0ZW1zIG9mIGEgdHVwbGUsIGhvd2V2ZXIgaXQgaXMgcG9zc2libGUgdG8gY3JlYXRlIHR1cGxlcyB3aGljaCBjb250YWluIG11dGFibGUgb2JqZWN0cywgc3VjaCBhcyBsaXN0cy5cXG5cXG5UaG91Z2ggdHVwbGVzIG1heSBzZWVtIHNpbWlsYXIgdG8gbGlzdHMsIHRoZXkgYXJlIG9mdGVuIHVzZWQgaW4gZGlmZmVyZW50IHNpdHVhdGlvbnMgYW5kIGZvciBkaWZmZXJlbnQgcHVycG9zZXMuIFR1cGxlcyBhcmUgaW1tdXRhYmxlLCBhbmQgdXN1YWxseSBjb250YWluIGEgaGV0ZXJvZ2VuZW91cyBzZXF1ZW5jZSBvZiBlbGVtZW50cyB0aGF0IGFyZSBhY2Nlc3NlZCB2aWEgdW5wYWNraW5nIChzZWUgbGF0ZXIgaW4gdGhpcyBzZWN0aW9uKSBvciBpbmRleGluZyAob3IgZXZlbiBieSBhdHRyaWJ1dGUgaW4gdGhlIGNhc2Ugb2YgYG5hbWVkdHVwbGVzYCkuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG4jIyA1LjQuXFxuLi4uXFxuLiBTZXQgb2JqZWN0c1xcbi4uLlxcbiMjIDUuNS4gRGljdGlvbmFyaWVzXFx1MDBiNlxcblxcbkFub3RoZXIgdXNlZnVsIGRhdGEgdHlwZSBidWlsdCBpbnRvIFB5dGhvbiBpcyB0aGUgZGljdGlvbmFyeSAoc2VlIE1hcHBpbmcgVHlwZXMgXFx1MjAxNCBkaWN0KS4gRGljdGlvbmFyaWVzIGFyZSBzb21ldGltZXMgZm91bmQgaW4gb3RoZXIgbGFuZ3VhZ2VzIGFzIFxcdTIwMWNhc3NvY2lhdGl2ZSBtZW1vcmllc1xcdTIwMWQgb3IgXFx1MjAxY2Fzc29jaWF0aXZlIGFycmF5c1xcdTIwMWQuIFVubGlrZSBzZXF1ZW5jZXMsIHdoaWNoIGFyZSBpbmRleGVkIGJ5IGEgcmFuZ2Ugb2YgbnVtYmVycywgZGljdGlvbmFyaWVzIGFyZSBpbmRleGVkIGJ5IGtleXMsIHdoaWNoIGNhbiBiZSBhbnkgaW1tdXRhYmxlIHR5cGU7IHN0cmluZ3MgYW5kIG51bWJlcnMgY2FuIGFsd2F5cyBiZSBrZXlzLiBUdXBsZXMgY2FuIGJlIHVzZWQgYXMga2V5cyBpZiB0aGV5IGNvbnRhaW4gb25seSBzdHJpbmdzLCBudW1iZXJzLCBvciB0dXBsZXM7IGlmIGEgdHVwbGUgY29udGFpbnMgYW55IG11dGFibGUgb2JqZWN0IGVpdGhlciBkaXJlY3RseSBvciBpbmRpcmVjdGx5LCBpdCBjYW5ub3QgYmUgdXNlZCBhcyBhIGtleS4gWW91IGNhblxcdTIwMTl0IHVzZSBsaXN0cyBhcyBrZXlzLCBzaW5jZSBsaXN0cyBjYW4gYmUgbW9kaWZpZWQgaW4gcGxhY2UgdXNpbmcgaW5kZXggYXNzaWdubWVudHMsIHNsaWNlIGFzc2lnbm1lbnRzLCBvciBtZXRob2RzIGxpa2UgYGFwcGVuZCgpYCBhbmQgYGV4dGVuZCgpYC5cIl19XSIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ3ZWJfc2VhcmNoIiwiaWQiOiI2ZTUxMjVhYy0zMTY0LTRhNzYtOWM1Mi1hYzM0NjBjZTM2ZTQiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJzdGF0dXMiOiJzdWNjZXNzIn0seyJjb250ZW50IjpbeyJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3NrZXB0aWMiLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QTZGNGNDTjV5aEQ0Q3JzOU0iLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjB9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InJlc2VhcmNoZXIiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtM2NkOC03OWYxLTg4YTEtY2U2Y2VlMjk0ZTc5LTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoidHJhbnNmZXJfdG9fc2tlcHRpYyIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsInRvdGFsX3Rva2VucyI6NzQxMSwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fSx7ImNvbnRlbnQiOiJTdWNjZXNzZnVsbHkgdHJhbnNmZXJyZWQgdG8gc2tlcHRpYyIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ0cmFuc2Zlcl90b19za2VwdGljIiwiaWQiOiIzODkwNThkMS0wODNmLTQ2MzItYmJjNS0wOWJmZDNhMGZmZDIiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMVhqajNhUmRXeWhCSmZQbVRKQnhRYTkiLCJzdGF0dXMiOiJzdWNjZXNzIn0seyJjb250ZW50IjpbeyJzaWduYXR1cmUiOiJDQVFTMVFnS0VBZ1NHQUk0QVVJSWRHaHBibXRwYm1jU0RONlRmVTZORE05NzFsRWJ6eG9NdlJVQTFKOW94M2ZKUVlUOUlqQXJDUVFHSEJQNHBSSFBhdlh6UmxiTWJhWUhDNTdZaitHYnhncjBoZWx2ZDJPSUwvbzZSYnQ0UEZBM0ZhSUZVbGdxOGdjN0tRSTlGdEdudlNBa0xCUnpJc1VwLzJkNVJkMnpnRXNma0ZWZVhzbjhXWVlaa2luQVFWVnk2QTdCM0xhRUpXMHhiL3g4OERpNjdmZWR3eDBRK0ZsWnR3Y21UM0RjYWExMUp2ZXlIUnIzZG0rekdTWlA2Q1hlWE5KOG5yVFo3VzFZUlR5c0Q2dzdvN0xwOGgxUndxNGY0bzhqK1FVZlkwdmplK3pKWnN4SFBtUU50M3lrZUtoOTloZ242ZGJpejYvRUhxbWJIalNBdTFNNFNxRURmcUlLVGhIWWVuYW1BWm1jUTRYQU9vNm1kUTYrNzdWZ3BWMWxDMS9rNTJJaVVvSmprR2xRcm5MQ3Z1dGlJN0tOK3NnSzhjMisyS0NKclBSQ3ZWVitCa1N0dlA1aldXcXp5dHBLUStuSHpoTDQ0OE9ySUE5Z2RKQzM5QzhlRHJYZVJuSTd0NEZxbGtYR29NbFpBVjlVTWNyUUhSeE9NaTh0T3lUemk5b3lCRkRUU3NRK0xKcWVyMHR6MUZ0OWdlazZ2emc1MFNwVkFGUWRMRVJ2OFIxWXFNYWVJK3hzdlRBRVc5TVJndlJNa3pMdXpSVDJndXczakQrRmppcDM3eGdLNVFJMFIrQ3NPQW1RUG9UWml6emNJeUFVQXVDSkZDZ3EvZ0QvQ2liTHhveW1pVlVJMWFuOVNudDNLcE5Sb1dkdldJbmUzM1YwcHY4UEhnOXlXMkp1TE9xUUdpbExDc0svUTNvUXIzdXBFTVh4WEdqQ0w3V1VpenZSWkdGRUVWK1ZwbFl0N3Y0dEtaTWo1cGszdmRlTTBNSjVYWmc3UHZVc1J6eWN4Y25yVDNLbm9jM3FNK3R0cVpmVHNiOGQ2VS9wUWlmcTdHdWFoRk9IcnZyb3FnSkxqKy84MVl5MXBaRmtqZkpnWTBYa2x6QW5BNkNGcS9ZVDBneGJWK1RINUlTK1dhWU9VekszNlM4czVxcU13aG5ndjRJekUzY3RYWHFuaElaWE56bnlWQnFiWWEvR3gyYkNqY0VqMm41UnlHWVJTLytNY3VsZ0xCem5ROU1rRzJEN1FQM01xL253SFhLTUlhTFM4Vks4aEczZDByQWpsdm5hdVhTaE5ySnh0a2o0cWt3bnhENExyRWprMXVlK1BMY2JMcVlXd0FyOG4wcys0MWpyc0lQVnl5Szl2RmFFWnJBOUthQnhrSnlpcXg1MEdaTm43Y2t4K21rSTE4Z0g1Mml5VlB0VThGS01YanVnQ2w2bVBMaUhZaURRZCtHNjNMdWRtT0t5czlpdFZuQkRCTk1QMmIvSTcrYklWUmxoZUdhWldyNk1BVEtsODZCM3krMGt1T3JMUk15V2NkaWJjMmlRa1ptKzhmN2RUVXdiS2JCSnNLT0ZZNzMvOG5sTC9KaVlwano2blVJaytJcVNqYUZ5SzZYRmxDY3VrV3BXSHBESm5TdmJFemV5N3FIeUNxSitJK29uOGlQOEdPdGVtcFFhUzAvNGtaR1N3bHJFNnhPajZFd0dwZmpiRFpLSGtQLzJSQWROWVBRcTN6N2JkVkpNSk5uRk16NmUzN216TXBDQmlod1BkU0ZUUkUzU0NZNjZ3dlRnSnpHQ1V5aEhiU2k1aTFhenpPWGQ4dUhDU3BULzZMMW82VlUwcGpvUjhLYWE0d0NXSEdmN1AvV0VkK2F2eXAwMkpudTFjbXBXMCtHcSt2SzloMHRkYjJJQ2xNL2VOZmRvdVFQK2VwOE1KVWpyYURSSUNtQUpRWTVvYlM3MXZuaU9vK3JlYUZ5MHpSZ0IiLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsic2lnbmF0dXJlIjoiQ0FRUy9SRUtFUWdTR0FJNEFVSUpibUZ5Y21GMGFXOXVFZ3dGaDNBVkpaUDNzOHkxSHEwYURHNzZ1aEhPTmFTY1hBR1NoaUl3R2dqTFhIVXgxeVpya0xQbGpqelRPUXljcExKaUtWd1hjeG0vRXpLSm0xczhNNVd4dVJFV0VsZFhjb2FOMC9tc0twa1JNVCtlTGI1aWg5ckFIVUxUb21VTnNVd0pldTBqckplNEdMYXp5N3NQNUt0akVPRUJLUXNYVWZvUTdmRGtaT1FVSFppOXp6UU40aEZObVlKVWYzT1ZxZjlUUU5kQzErbS8vSEFaUGZXbUF5Nkh0RHZtN2tnQkprQ0VvaUZubTd0bi9QWDFoWEdQUmdXYzZCVFhVTXIyV0E4YUt5cWdnUlViTUgyK0pZdjVpV1pkZENjMXVwMU5icjNzRjdXUGNvdDlaQmh5SFFYcjY5MnplSms2U0pneWdJVnJBU2JNbHkyYkJ4VFdKQjYyM2dWZ3pwQ21WU0xaRnBrS1hnUGNwOGhHeURLT3ZGNlNpUVNIcUdSeFFvZnI3VnMxYVBoZlppeFJPSkdEK01scmtnZ1ZBbkdNeVlJRFVacGF2dENlaVUxSm1CU3owMDg0YlhQdFQvdG5wc0tPblNQWVpRbzNCMEJYS2JxQS9Xc0tuZzgvVjNaOFV1YWpvNklWWjJoZ0ExV0RkdXFRT1FrRXhHVTZwRnVMaGViSWp1V3hTTkkvRGZmdlIvL0xOcUNmdmRuZjBodm9QdER4ZngvU2F3SXg0L3NwcTl4YndGekh0TGdvSGcxRjJ3Rnc3Y2xQRGxnMVB5RC92MnlKRzNWOVQ4R0w2akdQRTZyaGZpbC91NEhOdmY3QlcwZFF6Q1RUWGNxWnk0bkJ5eTArcCtTN1p4Zk1sNEtweGEyZ09oaFNOamZwZnVYMFFwTDRrTUU4SjFubGlGTjFkOEtxRVB1REkxOEJRSFVoRC9VZlkyOW9BMHg1MTBGak0vQnNMWjRuNXNwcGtvSWk3Q0ZvUXEyYUFLVk5mQzFLaWxoSXBSRCsxZHlEdFpRa2xOOGdacE9FU0h3Nk1MMVR0VTB5ZkFjYm1xb3hKNmhoanp3SDZDTXNUUUVFeThNYlJCTStRS0trSVJkUkNQSEdGVzJMa3VSNTArSjBwRi90cjZwcVZ2SmVvemZpTW5yRWlsQjI3VFpQNG5LVEpHOHJkLy92Q1dlbjB5N0NBLzhkQko5Z2RwNXR4N05WQTQ1azFnZGN4RVIxZ1lvdU5YM24xdkczL0IxRjQrOTN3WFc5TXFvNlVzNTlUUW1yYjlOcWE3cFNRTlNySDA2dzFHeHlCQWxrRmZxQWxhZmsxNmFNcHE0MTZUSllwR3k0M1dDVWlDY0xXbGhpV2d6MnNnT0xBWFA5YTV3ZVdBS1JDOEo2amtjVWtEZVhuVytIRlNpUE02KzlCbzZrVmhMVWhONXJjNG1IWk9acEY3cHRQcHNRTjRYQ3FMUHNxcFJILzhvNVhweUM2Wm5lVGVGMkV4dGpod1pWalhEd01iSXpWWUlidzF5MlBQR3c4ZEM3cGM0c0lWU25JNG9RQUgwdjY1MGdoZmhtTWV2V1c0aDRQeWxidy9ERzRmZGVUMys4ejBmOE1PU1R0ZmlweDA1L1F0dEhsQXZPRFJ4YTV5REN2T3o2bjNmMUJPME4vNE1iR3lPTTI3cVc0eDhtQlZvcnlVWldmc21oZmkwL0dmTnZCOCt5bFRHYllOWDI3T2NFRjVXWnROcWJXRXk3VjVHYm1aVVN4c3BjZzdGQlozajMvM0w3bXV2WlhvWmkxZjVlcnE0eTNsU0grb0Y3cVBGbnZpZXlMT0N3dWJjUXkrb3lvd2o0Y0QwaWRSZWNucCtzKzUyRDcrbXpGMWdwVytmK2NKYXNveGlnYnhrT2xvUzR5ZWlLdmdNNG5XU3BHRWoxMlJQeDVvODBFbXppWE45dDR6SnpXMWh0QTVoWEpKZUZ5bXBNWXR5Q1VDZXZSV1ZtclJZa3RRN1VuQ2E2dEwvQVZQaTFuaTVYQ1hIZkxPdXFmSzVLTFl4andWdGRoaGZtQ0x2cGFLVk5DRU9DZjRuTkdIcUQ5OHdVYjBlaldOd0RZQm5KR2tCUnc5UlZzcGZ3TmRvV29WY24ycXY4eXdHVXA3eWMvc2syTHBJNlZ6aVJ5dldxYkdORzFaSnFWVnV0UmM0NitzT0U0VlNlUzIvQ1ROS05hNk5NQ2YvaHh4YVVWc1lmS2RQK1MxTGM0aWFuS3Awc1V1Z1BwbXRveWRDMENQMlJ3am5ITGNuenhDbnZxeGxMUVdYTU5QVHN1MnlCY0hsV0pMdGNtZGcvTWNMYWd5N2w1a0c2c043WnhKTEQva0F4OEU2dXFvaFArSy9qTVNyWXVRSm5ocjR0ZjRiaVdGWTNKemYvS3BBWXlWU3ErcGJmRktYTWJMOW5IRW9aUVQxNVd5YWJkNkRNQXEzUVpPdGhNNXBVemROcVVYa2NySTFQd2pBU0FnNGRWVS8ybys5OG1UWG55aFA3VmxFa1RuZGRwd3lKRitZQ0NjMDFwcWE2Wlg5OXBuUitZT3BJcmI1RHRQY0txMHQ1Y0ZDRHlReE4xbFV1cVpjWXVqdlgvR0M3KzZJcFJUei9Oa0VZLzVjbnRPZlpKeWQ2U2tVeC9KUjVBSklSUENOZWliMHJjV25uRm4wWEs4czIyeDQwd0xqRXR1ZlhETzkreXJhZjRrSHhsQkkzU0IwbWVNamVwWDUwQ0poQXg3TmVra1JGbndvZWNMZnFJeXJqWHJnQVpzVE1QaVg5NHNuamRyemlzQVh0QkthbnVsaWxTeE9mVzJIU21mbmswTDJHWEhKSjNTK3ZDYmdSRW1sOWRla2ZtSGhtd0VZdHYza2ZodGg3RVJJd1ZUOGo2OU90SUJDbWVUSUp1QTdvNzNQWGMxTEoyd2h1Wks3RlplNk5UT3M2MU1QN0pWbEljNnRiaHRPQmszTlhVOHBCUE9DWTZRNTVkNjNGdHBVVS9LQ2JRMEsyV1E5bzVqTkd3Tk5KNkN3NUZEQ3QreDVZSmVQMWQyUXpEU2lxK2NTWnF3cUNaVmNOb0FmdEdHemdyUlo0dmtwYlAweFFaSEN0V3NVd0hWVGpyWTROZlJEakhkVlBSbjhKOVh0b29jL2hJV2txekZhY3pZVGxhcHUyUkNKcWU4YUVvM2p6V1pDRUo3QWlCekJZcmFZRGdPZmVpUXllSlVzYmJYSGRHS2lpdzIyQmRwNE0rWXo2NzFNWjBpZTlFV1JrczBRMkRIR1VUbmU5UzR0QUFKUEdiVWY0eHRDTW9KNDBRNFFXMDVRTFNaeTN3TkZBVUZZbVhFNEVDREZCOCtJRWVORkZGL3RnQW5maDhHdkZoLzF4WStMRStvek1taW5qU1J3aHEwOXpRQUVrTkZ5dTBJS2d2RGt5T0kraWNjRjdkb1dMVFVhc1B4TEgxeEdQQlNLa2VXQ25Tb1gvNEMyYzlkdTY2QmJSWGFtSHFhZStWQ2lTNkJOOTUzaVlLT3U4ck1rbmVXK1RGalYwQ3pBZjhQTkc5TEJheVVnWTVVdzl4MFNhcDZ6STRQcGFaS0g0ZDlrdGtGVSs5MXlCYk41U1J0ZGQ4L0VRYXRKWG1CTmZ3N2tScmxsK0xqd1p3ZVdMRWIwalQxUnBHdURxSExkTFRNTWdvd1NjenJwMnpDc0pkUUgvc2VhWXJwd2tDRlI4ZFJqTTdKUlVrVmExQTBTdmRaQVg3NXZ1QnZ2TEJ6TjJjK1JidDVvcXk5M2xxSUJJaktiN3cyVWtSVGI0cEJXY05LTlBVZ3QzYXovUHhReEVhc2FsdGRtdXl2c1hZYnVyM1ZQTm44SnpOLzZKNU4rUUNuejU3MVRjY2FwQ21rUHZGRStnYXlYVStVRjFWS1l5RmNVdmxldjdsc1FsbWpxeksyUUE4WDdRM1R6RmFwcWh2SktsdDdkekJIcmFxWkJnUGdiOU40b0tOYXlpOEZaVDcxK2thWWhJaEZyRVNLWVJIZkhrajVuM2tmaGdMeSt4K2pMRGJLeE1nZ2tnSG5KejlSdUVIWlhCV3RrWUFRPT0iLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsiaWQiOiJ0b29sdV8wMUg2ZlY4SnZSSk52WHk0N2VSbmQzakgiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6e30sIm5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHdBTVJkUzIxRmVKZnpRaE0zcyIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo3Mzk2LCJvdXRwdXRfdG9rZW5zIjo3NzAsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjczOX0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoic2tlcHRpYyIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy00YWU5LTc2YzAtODlhNi00ZjBiMTE0ZTMwMzAtMCIsInRvb2xfY2FsbHMiOlt7Im5hbWUiOiJ0cmFuc2Zlcl90b192ZXJpZmllciIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzM5Niwib3V0cHV0X3Rva2VucyI6NzcwLCJ0b3RhbF90b2tlbnMiOjgxNjYsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjczOX19fSx7ImNvbnRlbnQiOiJTdWNjZXNzZnVsbHkgdHJhbnNmZXJyZWQgdG8gdmVyaWZpZXIiLCJhZGRpdGlvbmFsX2t3YXJncyI6e30sInJlc3BvbnNlX21ldGFkYXRhIjp7fSwidHlwZSI6InRvb2wiLCJuYW1lIjoidHJhbnNmZXJfdG9fdmVyaWZpZXIiLCJpZCI6ImMwNjQ1NjNhLTk3NGYtNGYwZi1hYjRhLTA4MGU2M2MwMWNmNSIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsInN0YXR1cyI6InN1Y2Nlc3MifSx7ImNvbnRlbnQiOlt7InNpZ25hdHVyZSI6IkNBUVM5Z1VLRUFnU0dBSTRBVUlJZEdocGJtdHBibWNTREdGeUoxTVhvWStBaTZOa2J4b01SZDJUclVmVVU3V3JQcEEySWpEZ3k4ZFVwbGNaS2M3RG5Kd05MVEJmSDVVTlIrbkduYWtTUFFpYThVVkl6cmprT25KMmZMcldVc0Eyb0VPOHlSc3Frd1hra0RDc3loQjk3ZHdqNTdSTXQ5MVh6Z1doTDlsdU5FR085MnlXMHJORkdvTVNVVk94UElyRmM1Ump5cmhLRWMzKzFSbnB4Q2t5bHErTE5sUTVORnhMbjZ2MmNGNUR6cXB2QkhOUjdYQWlnK3VySklVM2dYeWRDa2FHWlpEZXRKdFFPVU9YZ1ppOXo5MTAwTnBkQUVrWERvMklDSENHVEdwVmMwSi9wYlh4V1JCeGI2QjEvR2wwWWpuNjhWNkhsZXNIN0lvQzdHUi8zSHdSdTlOU0RCRzR0dGZRakVXY2crUGVBZWE0Um43anp4K1RtOUg3TDFqb1RLVmdEYlFnNWlLTUxmanZQcWxua3NDTHRYT3Q4alNJUzVPWUxON2EvN1RYNi93NWQzNHZudzh2UERtVTFVWWkwUTcycVVXaFVhMnZ3OE9YTnBjS0w3QnAwbE5mQlM4cnZIWSt2Qlk4L2hhQW9UTDN3dkxGVTFmVCs0ei92QTNCY3NpZ1pEWWxmSmVEWWN4WG1SdXF4enhFR3BYQ1JlOXMxSFd0SUQrNDFwSHU2bHBaSGtYYWdocGM2VFpITHhOd2NWaFRiNmZNUTdRbysyTDZQdmtsaDFtMzJwZW9keGVnMllpUGhkcGwzdy94a09FdTUvSkVGN1pCeHBkVUwrdTN3a081cFVsc3FmZ1g3dWh6djdIQzJXcFNXVmltcndpUXdRT1V4MCtKUHJMdHcrbFlmcTdJM2pydW8wR1cwZ3VXdm51ZjdKTTNZTGo4NHErSmdIb21uU1Nabm5abXJMMFVhUnZyVnlMbzg3N0YzOWNjTFM3WG9abm51aThKNFA1OFpUbWlMa0VKZUdjeDFJdWFnMmUzMzMzOU83WWV3LzU5dExiUXRJR2g5VldNd0hCbkZ4MzlTOC80TEVGcks1SGxkUFFoT3Jsb3lUdGtSczY2aVFSS3R4TUtTMFZJNlFUS1pEQWJzN2E3SDF5QnYrbUppOVhnNHVLbXlVNnFubjd6RUtqalkyYklsQ3dleng0TExHUDVIRDNacjZmWnNtZEcxVUh6RHNCdllHMm9DSTBvcUVLTSt2YWxzVDZoVThNUmpiUUJEL0pKOEpKTi9pQkdsaEJBVUhvMFVtaGh0UjJldVRWcFpXbE1uaG1wWEFkS0RYU2ZQZGFQb2hnQiIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJpZCI6InRvb2x1XzAxWEVBdWhRSENCZHlDd3R5bVp3RmZEaSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3JlZF90ZWFtIiwidHlwZSI6InRvb2xfdXNlIiwidG9vbHNldF9uYW1lIjpudWxsfV0sImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2Zkd0FzYWJEcXVlZ0hpNllVcENuIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjc1MDksIm91dHB1dF90b2tlbnMiOjg4LCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjo1NX0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoidmVyaWZpZXIiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtNjY4Mi03ZDIxLThjMTUtZmZiY2U5NDIyYzlmLTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoidHJhbnNmZXJfdG9fcmVkX3RlYW0iLCJhcmdzIjp7fSwiaWQiOiJ0b29sdV8wMVhFQXVoUUhDQmR5Q3d0eW1ad0ZmRGkiLCJ0eXBlIjoidG9vbF9jYWxsIn1dLCJpbnZhbGlkX3Rvb2xfY2FsbHMiOltdLCJ1c2FnZV9tZXRhZGF0YSI6eyJpbnB1dF90b2tlbnMiOjc1MDksIm91dHB1dF90b2tlbnMiOjg4LCJ0b3RhbF90b2tlbnMiOjc1OTcsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjU1fX19LHsiY29udGVudCI6IlN1Y2Nlc3NmdWxseSB0cmFuc2ZlcnJlZCB0byByZWRfdGVhbSIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnt9LCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ0cmFuc2Zlcl90b19yZWRfdGVhbSIsImlkIjoiOTgyYzAxMjYtZTZhYi00NThhLTkyMmUtN2IyNjZlODNjMmIyIiwidG9vbF9jYWxsX2lkIjoidG9vbHVfMDFYRUF1aFFIQ0JkeUN3dHltWndGZkRpIiwic3RhdHVzIjoic3VjY2VzcyJ9LHsiY29udGVudCI6W3sic2lnbmF0dXJlIjoiQ0FRU3dnY0tFQWdTR0FJNEFVSUlkR2hwYm10cGJtY1NET1JYZ2U5dUwxODMyUk1Rd0JvTWpRd0F3QUhiOWlQMW9qUEdJakREU2NhbDQ4NmJlQ0pidEc4L0QyWTlnOVM1eElQZmpvSGcvZHh3WDVIeXBTUUVCYmpGaVRTYlBFQ1hTRnpWNllJcTN3YkxjbXlQYnZ1c0duRGV4Rmd4KzNzSVh6dnF5RlYvNjl4aGloTW5TcGZUQ0h5MCswelN1MVdwWVUvV1VwSWZvYVNYVGNUYTFxa004eDhFay95LzFqa0tNWlVmREZhOTN1aFVKeE9DNCtRZ1BXQmwvK1h5MnQ4MkwyeC9lYnE3UUxRVHF0UjBqWGVhVVJRbTNDbzZKdk9nV21Edk1wRWRUZVRXRTNmRGgxdHp6VktzZmx1bTRkODliNkNhS3RuOG5sUWQ3UmhOOTl2TlFNUUhHNVY0T3VWTmRySFBaWnBGT3VoZGVqc01GV1h5Qis1NXNsR0FkOHdFWGdGS2JJL2tuT1hWR3pML05ERjhHdHkxdE5abHUyZmRTQ0FzWVdNc0ZRUDFjYndMZVFHc3p5WHpKTVZaSjJJOXhCUGt5ejFIT1hDUHloNUYrajJnUlRQaWtWQjNBUkM2dGZCL2xEU1VlS0JWWlBRUzhwWS9CNGVDcytJamphQk5XcnVMemZxQ3NjNnhMMEN1UVBTWXlRWGFZRmpZclNSTmNDNzBXUUJXVDVWVmhwMG1BOXlaREdZYlJadDlJNU9LOVFrUW11SjV6REZhTUVPWHpTcnNDNGx0ZVA2TWRpbWlNaWp2c0gzaXErYzZXVVlvbFJqM25lSTg4RWFISXpMQUdScTY0Qk5DTHpkQVlMMnBFVTJzYjV1Rzc4UEtWb1VVQ2JWSkw1anBET2hMT1RPMWJRc1R1b1VwblJSckxRRC9rSXpWK2xqcFZCd3dmSmN5cTc1VEpBL0N0UHIveHIyU1o5QjFiMnNiakc2akQwbGh0b0NkZGVEZ2NxZ2wvTFNTa2N1YVlZOHhQV0N5K0x0TmhjcVRoZm1mdm5ITEpJcEw2Z2pialcvT1FwUmZWVXdmdzFqT2I5YldWd2NnSGhOWWZLWUdQdE04b3RoNXh3TFlSUlZnenFNS092czdmYWxYUmhWeloyU1A2SXcwQk1tQUFaNkhqZkNsOUwrdFBxSk5sblFRT0hwb1dqdW00QitYc1lsZEZFS0dRV1Fxc2Q1OWlOUHZhbzlwL3NDT3Q3ckdxQ296Y21naUVtNHc2NmV6L1FJRHliNDZuMCt0Y05KUFVjb0JLdlEvakR3RTZWd21rUS9uRUNqRnAzVWREVCtrV1lRU05kOGo4VkJaT3BHOExscVVZckVaQ3YwcTdCczF2Y05yZFlpYUZvUzMyb3NheW5XaytIdDNPSHVGV2JFRjhKbHNGVFFBTzA1VnZZa2VjZjBWL29USHNHNWh5T09zN1VFYURGcTd1YUp1eldWaGg1cEFvaklhQmNSOGZWbG4yUExSVFg3aXMybnNBK1FyZGlkZVBjWnRGdGd4QWlLWWUxaTJzU0ZpNHZtOEVPa0NiUTYzd2RRZVk0VXJyNVB1b01Fd3hVVUxqZ0thZ2lBbXNKRHE0UnJWbTdzTzQrNll1QTlqNmwydXlsa09FYzY3dmYvNWZrWHVDc2lBcE5MNmtlYjhjK3ZMMG40RlhHZHpqK2FwU0RmRkdCZ0IiLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsiaWQiOiJ0b29sdV8wMVZCcldkV0VXWWdpdDRrTlllaEJNVTUiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6e30sIm5hbWUiOiJ0cmFuc2Zlcl90b19lZGl0b3IiLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QXpmanFVb3BvRW1aTGFxbTEiLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzQxNiwib3V0cHV0X3Rva2VucyI6MTUxLCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjoxMTl9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InJlZF90ZWFtIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTZjZTYtNzI1MS1iNzk2LTRmNjQzMzNmNjdjNi0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6InRyYW5zZmVyX3RvX2VkaXRvciIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxVkJyV2RXRVdZZ2l0NGtOWWVoQk1VNSIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzQxNiwib3V0cHV0X3Rva2VucyI6MTUxLCJ0b3RhbF90b2tlbnMiOjc1NjcsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjExOX19fSx7ImNvbnRlbnQiOiJTdWNjZXNzZnVsbHkgdHJhbnNmZXJyZWQgdG8gZWRpdG9yIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJ0b29sIiwibmFtZSI6InRyYW5zZmVyX3RvX2VkaXRvciIsImlkIjoiNWQzOWUzNmItYzViNS00NDI2LWE5YWEtYmUyZjdkMzgxNmQ2IiwidG9vbF9jYWxsX2lkIjoidG9vbHVfMDFWQnJXZFdFV1lnaXQ0a05ZZWhCTVU1Iiwic3RhdHVzIjoic3VjY2VzcyJ9LHsiY29udGVudCI6W3sic2lnbmF0dXJlIjoiQ0FRUzVnWUtFQWdTR0FJNEFVSUlkR2hwYm10cGJtY1NESTRRSWI3TDJ3NWFXM2NNL1JvTUxpMndFeERrcWh1eEpEaVlJakFxR3hNNFdVd2lxNHVaSHJpQ2J1QzRrVndhN2lnZFVMb29jWmJpNWZldktiZndxSFNickxOVDUxTkdYNFhzTHRzcWd3WS90YWJIamxsenF1N25SV3Y4K0Z6NFI2bjlqYTdDcGoyNFp1eEZ1S2trUU9CbUE0VGYxYXBrM1JQcWNFUStqK3dlZHUwbE8rcmRFQ3lkM2xYaGZnRVJGRnFjTTZhdWN4ZWw0UmJ3NW9BSFh1OStYZVR0emwrTjh0QWVaOWlHWjJYVHRVNU9iOGFQVDFLdVgrdXA4a1E0VlVYa0JUZmQyZjVKUDlSeFpFcTl3U1lhWnN5SGR2MG4rMVU0NkFPc3ZLWm1wOVFYQndvMmY0TzkwalA4ZWdpcmVtOUViWmJtYVlPSjhXSzdDdEtSVmJZMnk0Kzl4UlVKMDRuc2RJejVGNUZGS0V4eE5PckI0bWMvTkU4M2FiM1o2NGlXek93cW9VR0FHUVNrYUgwZDVjRXVtWm80ZlpaQWhQeXVKZVFEeHVsTUNld243RUdDbnBKdy9uemkwd295RWFRTDRZN2g5OVA3YVh4RnIyOHA1S0N5SkVKVERlZmVJcFZsQXdNQ0Fqeit4QzY5QUFMbXd5VFErY0ZFTStLQWdlSVNwaExWeThBUHIyRlNoYXhCTnJYUWd6MXM5czJkaExaUU9CaXd6aHhtRWdkSzcrOEphcWhCMGVFRWQvMVJWQXVJWWRlekE1SEJyd0U5RHlRaUNsM1VPc2JTbk5FVmUrZ3JIcDVwM3RtTUJxNjhnTFY4Mm9BekZZWmwrQ2V3eDcxdjVkUEZPdktQRUhVenBRWU9WTHJuQjFFWlJZclp6NzJuMndmU1Z4Qk96cFRjbkJ4VjZIZWRDUHlJektYWk05K3lFZVRKaThRdHZWZ1oxdyt2MGhtQ0ZHcG5tWk43bVhsZ3FMMXM2TVhWeWRwUE9LUGxNM05hN0NkSG5QRllpd3Iyekx2a2tjSU03Q3NrL0MzK1pNQVdUSytDOUIxdmJHeFBsdEUzT3Y4a085OFBnU1htMUFKdUViWHkrcjNhM0QzUWxaSjNkRUtlMkxOZEFCeXJQTWhTY0tVOEdsZkgvOU50Z3JodGFrZ2xsRC9NZkFsVHNiTndORHJVdFdSeG1EdUovMForaGtrL1lOd0VEWUFaYThUbXdWb1dPZEJIcFJQSHZaOUU5dXNldWx0bWErSmsxZ1JMRHR2RVprcVhVRVNrTWNxVXRWd1lNNWFqSkE1TjUyY05pWjE1QlRmOFhqR2hLV2RPVjQwKzhIMFBPU3hNMkppZndDTzBDNXhGVzVCbzNJaUlOdjBVT3RZNWY4a3ZQZE0zUGpraGFPcksxeVRBOGVFRTQ0dnVuTDlQa29HZWNiWW0rWEU1S3pMM1FzRW4yWTFJSndPOXcweEhYUVpFZWlQNSswdTNZbkRjOHE5ZEJMaEdXM0Z5MzBnWUFRPT0iLCJ0aGlua2luZyI6IiIsInR5cGUiOiJ0aGlua2luZyJ9LHsiaWQiOiJ0b29sdV8wMUJtOEFVOUVzUVV3cW9WTFlweEtBNzciLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6eyJmaWxlX3BhdGgiOiIvYW5zd2VyLm1kIiwiY29udGVudCI6IioqVHVwbGUgdnMuIGxpc3QgaW1tdXRhYmlsaXR5IChQeXRob24pKipcblxuQSB0dXBsZSBpcyBpbW11dGFibGU6IG9uY2UgY3JlYXRlZCwgeW91IGNhbid0IGFzc2lnbiB0byBpdHMgaXRlbXMsIGFuZCB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGl0IHJlZmVyZW5jZXMgY2FuJ3QgY2hhbmdlLiBgdFswXSA9IDFgIHJhaXNlcyBgVHlwZUVycm9yYC4gQSBsaXN0IGlzIG11dGFibGU6IHlvdSBjYW4gY2hhbmdlIGl0IGluIHBsYWNlIHdpdGggaW5kZXggb3Igc2xpY2UgYXNzaWdubWVudCwgYGFwcGVuZCgpYCwgYGV4dGVuZCgpYCwgYHNvcnQoKWAgYW5kIHNpbWlsYXIgbWV0aG9kcy5cblxuVHVwbGUgaW1tdXRhYmlsaXR5IGlzIHNoYWxsb3cuIElmIGEgdHVwbGUgaG9sZHMgYSBtdXRhYmxlIG9iamVjdCwgc3VjaCBhcyBhIGxpc3QsIHRoYXQgaW5uZXIgb2JqZWN0IGNhbiBzdGlsbCBjaGFuZ2UsIGFuZCBzbyB0aGUgdHVwbGUncyB2YWx1ZSBjaGFuZ2VzLiBCZWNhdXNlIG9mIHRoaXMsIGEgdHVwbGUgY2FuIGJlIGEgYGRpY3RgIGtleSBvciBgc2V0YCBtZW1iZXIgb25seSBpZiBldmVyeXRoaW5nIGluc2lkZSBpdCBpcyBoYXNoYWJsZS4gQSBsaXN0IGNhbiBuZXZlciBiZSBhIGtleS5cblxuU291cmNlczpcbi0gaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy90dXRvcmlhbC9kYXRhc3RydWN0dXJlcy5odG1sXG4tIGh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvcmVmZXJlbmNlL2RhdGFtb2RlbC5odG1sXG4tIGh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvbGlicmFyeS9zdGR0eXBlcy5odG1sXG4ifSwibmFtZSI6IndyaXRlX2ZpbGUiLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QkJhYTlnak1DY2UxNlRVZW4iLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6ODEyNywib3V0cHV0X3Rva2VucyI6NDMzLCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjo3OX0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoiZWRpdG9yIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTc2ZTktN2NhMy1hNWZmLTYzZGU2NmRkOGRkMS0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6IndyaXRlX2ZpbGUiLCJhcmdzIjp7ImZpbGVfcGF0aCI6Ii9hbnN3ZXIubWQiLCJjb250ZW50IjoiKipUdXBsZSB2cy4gbGlzdCBpbW11dGFiaWxpdHkgKFB5dGhvbikqKlxuXG5BIHR1cGxlIGlzIGltbXV0YWJsZTogb25jZSBjcmVhdGVkLCB5b3UgY2FuJ3QgYXNzaWduIHRvIGl0cyBpdGVtcywgYW5kIHRoZSBjb2xsZWN0aW9uIG9mIG9iamVjdHMgaXQgcmVmZXJlbmNlcyBjYW4ndCBjaGFuZ2UuIGB0WzBdID0gMWAgcmFpc2VzIGBUeXBlRXJyb3JgLiBBIGxpc3QgaXMgbXV0YWJsZTogeW91IGNhbiBjaGFuZ2UgaXQgaW4gcGxhY2Ugd2l0aCBpbmRleCBvciBzbGljZSBhc3NpZ25tZW50LCBgYXBwZW5kKClgLCBgZXh0ZW5kKClgLCBgc29ydCgpYCBhbmQgc2ltaWxhciBtZXRob2RzLlxuXG5UdXBsZSBpbW11dGFiaWxpdHkgaXMgc2hhbGxvdy4gSWYgYSB0dXBsZSBob2xkcyBhIG11dGFibGUgb2JqZWN0LCBzdWNoIGFzIGEgbGlzdCwgdGhhdCBpbm5lciBvYmplY3QgY2FuIHN0aWxsIGNoYW5nZSwgYW5kIHNvIHRoZSB0dXBsZSdzIHZhbHVlIGNoYW5nZXMuIEJlY2F1c2Ugb2YgdGhpcywgYSB0dXBsZSBjYW4gYmUgYSBgZGljdGAga2V5IG9yIGBzZXRgIG1lbWJlciBvbmx5IGlmIGV2ZXJ5dGhpbmcgaW5zaWRlIGl0IGlzIGhhc2hhYmxlLiBBIGxpc3QgY2FuIG5ldmVyIGJlIGEga2V5LlxuXG5Tb3VyY2VzOlxuLSBodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3R1dG9yaWFsL2RhdGFzdHJ1Y3R1cmVzLmh0bWxcbi0gaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9yZWZlcmVuY2UvZGF0YW1vZGVsLmh0bWxcbi0gaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9saWJyYXJ5L3N0ZHR5cGVzLmh0bWxcbiJ9LCJpZCI6InRvb2x1XzAxQm04QVU5RXNRVXdxb1ZMWXB4S0E3NyIsInR5cGUiOiJ0b29sX2NhbGwifV0sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6ODEyNywib3V0cHV0X3Rva2VucyI6NDMzLCJ0b3RhbF90b2tlbnMiOjg1NjAsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjc5fX19LHsiY29udGVudCI6IlVwZGF0ZWQgZmlsZSAvYW5zd2VyLm1kIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6e30sInR5cGUiOiJ0b29sIiwibmFtZSI6IndyaXRlX2ZpbGUiLCJpZCI6ImUzNDkyZTA4LWM4NjktNDdlYy1hNWY3LWNjNTdlYTVmNjQwMyIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxQm04QVU5RXNRVXdxb1ZMWXB4S0E3NyIsInN0YXR1cyI6InN1Y2Nlc3MifV19" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJvdXRwdXQiOlt7ImdyYXBoIjpudWxsLCJ1cGRhdGUiOnsibWVzc2FnZXMiOlt7ImNvbnRlbnQiOiJJIHdyb3RlIHRoZSBhbnN3ZXIgdG8gYC9hbnN3ZXIubWRgLiBJdCBpcyBhYm91dCAxMDAgd29yZHMsIHVuZGVyIHRoZSAxMjAtd29yZCBsaW1pdC5cblxuSSBkaWQgbm90IGdldCBhIHNrZXB0aWMgb3IgdmVyaWZpZXIgY2hlY2suIEkgaGFuZGVkIG9mZiB0byBib3RoLCBidXQgbmVpdGhlciByZXR1cm5lZCBhbnkgZmVlZGJhY2suIEkgYWxzbyB0cmllZCB0byBoYW5kIG9mZiB0byBhIGByZWRfdGVhbWAgYWdlbnQsIGFuZCB0aGF0IHRyYW5zZmVyIGlzbid0IG9uZSBvZiB0aGUgdG9vbHMgSSB3YXMgZ2l2ZW4uIEkgdGhlbiB3cm90ZSB0aGUgYW5zd2VyIG15c2VsZiBmcm9tIHRoZSBvZmZpY2lhbCBQeXRob24gZG9jdW1lbnRhdGlvbiBzZWFyY2ggcmVzdWx0cy4gSSBkaWRuJ3Qgb3BlbiB0aGUgcGFnZXMgZGlyZWN0bHkuXG5cbioqVHVwbGUgdnMuIGxpc3QgaW1tdXRhYmlsaXR5IChQeXRob24pKipcblxuQSB0dXBsZSBpcyBpbW11dGFibGU6IG9uY2UgY3JlYXRlZCwgeW91IGNhbid0IGFzc2lnbiB0byBpdHMgaXRlbXMsIGFuZCB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGl0IHJlZmVyZW5jZXMgY2FuJ3QgY2hhbmdlLiBgdFswXSA9IDFgIHJhaXNlcyBgVHlwZUVycm9yYC4gQSBsaXN0IGlzIG11dGFibGU6IHlvdSBjYW4gY2hhbmdlIGl0IGluIHBsYWNlIHdpdGggaW5kZXggb3Igc2xpY2UgYXNzaWdubWVudCwgYGFwcGVuZCgpYCwgYGV4dGVuZCgpYCwgYHNvcnQoKWAgYW5kIHNpbWlsYXIgbWV0aG9kcy5cblxuVHVwbGUgaW1tdXRhYmlsaXR5IGlzIHNoYWxsb3cuIElmIGEgdHVwbGUgaG9sZHMgYSBtdXRhYmxlIG9iamVjdCwgc3VjaCBhcyBhIGxpc3QsIHRoYXQgaW5uZXIgb2JqZWN0IGNhbiBzdGlsbCBjaGFuZ2UsIGFuZCBzbyB0aGUgdHVwbGUncyB2YWx1ZSBjaGFuZ2VzLiBCZWNhdXNlIG9mIHRoaXMsIGEgdHVwbGUgY2FuIGJlIGEgYGRpY3RgIGtleSBvciBgc2V0YCBtZW1iZXIgb25seSBpZiBldmVyeXRoaW5nIGluc2lkZSBpdCBpcyBoYXNoYWJsZS4gQSBsaXN0IGNhbiBuZXZlciBiZSBhIGtleS5cblxuU291cmNlczpcbi0gaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy90dXRvcmlhbC9kYXRhc3RydWN0dXJlcy5odG1sXG4tIGh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvcmVmZXJlbmNlL2RhdGFtb2RlbC5odG1sXG4tIGh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvbGlicmFyeS9zdGR0eXBlcy5odG1sIiwiYWRkaXRpb25hbF9rd2FyZ3MiOnt9LCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QlVzTmhyMTlnUU5WY0ZzVWUiLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoiZW5kX3R1cm4iLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6ODQ5NCwib3V0cHV0X3Rva2VucyI6NDE1LCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjowfSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCIsIm1vZGVsX3Byb3ZpZGVyIjoiYW50aHJvcGljIn0sInR5cGUiOiJhaSIsIm5hbWUiOiJlZGl0b3IiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtODZiYi03NjYxLWE1OWYtYTM5ZDZiNGU2Yjc0LTAiLCJ0b29sX2NhbGxzIjpbXSwiaW52YWxpZF90b29sX2NhbGxzIjpbXSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjo4NDk0LCJvdXRwdXRfdG9rZW5zIjo0MTUsInRvdGFsX3Rva2VucyI6ODkwOSwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19fV19LCJyZXN1bWUiOm51bGwsImdvdG8iOltdfV19" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "17535b52f19759b3", + "parentSpanId": "5eb6729648b8a8b0", + "name": "FilesystemMiddleware.wrap_model_call", + "kind": 1, + "startTimeUnixNano": "1790968039097378048", + "endTimeUnixNano": "1790968042917388032", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "e30=" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "FilesystemMiddleware.wrap_model_call" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "3" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "editor:42b8f95e-d208-6d0d-c59e-cc043f552134|model:d639a607-1d3b-b4dc-823b-564098184f56" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "editor" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "editor:42b8f95e-d208-6d0d-c59e-cc043f552134" + } + }, + { + "key": "langsmith.metadata.ls_method", + "value": { + "stringValue": "traceable" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJvdXRwdXQiOnsicmVzdWx0IjpbeyJjb250ZW50IjoiSSB3cm90ZSB0aGUgYW5zd2VyIHRvIGAvYW5zd2VyLm1kYC4gSXQgaXMgYWJvdXQgMTAwIHdvcmRzLCB1bmRlciB0aGUgMTIwLXdvcmQgbGltaXQuXG5cbkkgZGlkIG5vdCBnZXQgYSBza2VwdGljIG9yIHZlcmlmaWVyIGNoZWNrLiBJIGhhbmRlZCBvZmYgdG8gYm90aCwgYnV0IG5laXRoZXIgcmV0dXJuZWQgYW55IGZlZWRiYWNrLiBJIGFsc28gdHJpZWQgdG8gaGFuZCBvZmYgdG8gYSBgcmVkX3RlYW1gIGFnZW50LCBhbmQgdGhhdCB0cmFuc2ZlciBpc24ndCBvbmUgb2YgdGhlIHRvb2xzIEkgd2FzIGdpdmVuLiBJIHRoZW4gd3JvdGUgdGhlIGFuc3dlciBteXNlbGYgZnJvbSB0aGUgb2ZmaWNpYWwgUHl0aG9uIGRvY3VtZW50YXRpb24gc2VhcmNoIHJlc3VsdHMuIEkgZGlkbid0IG9wZW4gdGhlIHBhZ2VzIGRpcmVjdGx5LlxuXG4qKlR1cGxlIHZzLiBsaXN0IGltbXV0YWJpbGl0eSAoUHl0aG9uKSoqXG5cbkEgdHVwbGUgaXMgaW1tdXRhYmxlOiBvbmNlIGNyZWF0ZWQsIHlvdSBjYW4ndCBhc3NpZ24gdG8gaXRzIGl0ZW1zLCBhbmQgdGhlIGNvbGxlY3Rpb24gb2Ygb2JqZWN0cyBpdCByZWZlcmVuY2VzIGNhbid0IGNoYW5nZS4gYHRbMF0gPSAxYCByYWlzZXMgYFR5cGVFcnJvcmAuIEEgbGlzdCBpcyBtdXRhYmxlOiB5b3UgY2FuIGNoYW5nZSBpdCBpbiBwbGFjZSB3aXRoIGluZGV4IG9yIHNsaWNlIGFzc2lnbm1lbnQsIGBhcHBlbmQoKWAsIGBleHRlbmQoKWAsIGBzb3J0KClgIGFuZCBzaW1pbGFyIG1ldGhvZHMuXG5cblR1cGxlIGltbXV0YWJpbGl0eSBpcyBzaGFsbG93LiBJZiBhIHR1cGxlIGhvbGRzIGEgbXV0YWJsZSBvYmplY3QsIHN1Y2ggYXMgYSBsaXN0LCB0aGF0IGlubmVyIG9iamVjdCBjYW4gc3RpbGwgY2hhbmdlLCBhbmQgc28gdGhlIHR1cGxlJ3MgdmFsdWUgY2hhbmdlcy4gQmVjYXVzZSBvZiB0aGlzLCBhIHR1cGxlIGNhbiBiZSBhIGBkaWN0YCBrZXkgb3IgYHNldGAgbWVtYmVyIG9ubHkgaWYgZXZlcnl0aGluZyBpbnNpZGUgaXQgaXMgaGFzaGFibGUuIEEgbGlzdCBjYW4gbmV2ZXIgYmUgYSBrZXkuXG5cblNvdXJjZXM6XG4tIGh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFxuLSBodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3JlZmVyZW5jZS9kYXRhbW9kZWwuaHRtbFxuLSBodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL2xpYnJhcnkvc3RkdHlwZXMuaHRtbCIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2Zkd0JVc05ocjE5Z1FOVmNGc1VlIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6ImVuZF90dXJuIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjg0OTQsIm91dHB1dF90b2tlbnMiOjQxNSwib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6MH0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoiZWRpdG9yIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTg2YmItNzY2MS1hNTlmLWEzOWQ2YjRlNmI3NC0wIiwidG9vbF9jYWxscyI6W10sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6ODQ5NCwib3V0cHV0X3Rva2VucyI6NDE1LCJ0b3RhbF90b2tlbnMiOjg5MDksImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjB9fX1dLCJzdHJ1Y3R1cmVkX3Jlc3BvbnNlIjpudWxsfX0=" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "6ad99925c2a25881", + "parentSpanId": "17535b52f19759b3", + "name": "UnsupportedContentMiddleware.wrap_model_call", + "kind": 1, + "startTimeUnixNano": "1790968039097809152", + "endTimeUnixNano": "1790968042917225984", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chain" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "e30=" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "UnsupportedContentMiddleware.wrap_model_call" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "langchain" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_create_agent" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "3" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "editor:42b8f95e-d208-6d0d-c59e-cc043f552134|model:d639a607-1d3b-b4dc-823b-564098184f56" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "editor" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "editor:42b8f95e-d208-6d0d-c59e-cc043f552134" + } + }, + { + "key": "langsmith.metadata.ls_method", + "value": { + "stringValue": "traceable" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJvdXRwdXQiOnsicmVzdWx0IjpbeyJjb250ZW50IjoiSSB3cm90ZSB0aGUgYW5zd2VyIHRvIGAvYW5zd2VyLm1kYC4gSXQgaXMgYWJvdXQgMTAwIHdvcmRzLCB1bmRlciB0aGUgMTIwLXdvcmQgbGltaXQuXG5cbkkgZGlkIG5vdCBnZXQgYSBza2VwdGljIG9yIHZlcmlmaWVyIGNoZWNrLiBJIGhhbmRlZCBvZmYgdG8gYm90aCwgYnV0IG5laXRoZXIgcmV0dXJuZWQgYW55IGZlZWRiYWNrLiBJIGFsc28gdHJpZWQgdG8gaGFuZCBvZmYgdG8gYSBgcmVkX3RlYW1gIGFnZW50LCBhbmQgdGhhdCB0cmFuc2ZlciBpc24ndCBvbmUgb2YgdGhlIHRvb2xzIEkgd2FzIGdpdmVuLiBJIHRoZW4gd3JvdGUgdGhlIGFuc3dlciBteXNlbGYgZnJvbSB0aGUgb2ZmaWNpYWwgUHl0aG9uIGRvY3VtZW50YXRpb24gc2VhcmNoIHJlc3VsdHMuIEkgZGlkbid0IG9wZW4gdGhlIHBhZ2VzIGRpcmVjdGx5LlxuXG4qKlR1cGxlIHZzLiBsaXN0IGltbXV0YWJpbGl0eSAoUHl0aG9uKSoqXG5cbkEgdHVwbGUgaXMgaW1tdXRhYmxlOiBvbmNlIGNyZWF0ZWQsIHlvdSBjYW4ndCBhc3NpZ24gdG8gaXRzIGl0ZW1zLCBhbmQgdGhlIGNvbGxlY3Rpb24gb2Ygb2JqZWN0cyBpdCByZWZlcmVuY2VzIGNhbid0IGNoYW5nZS4gYHRbMF0gPSAxYCByYWlzZXMgYFR5cGVFcnJvcmAuIEEgbGlzdCBpcyBtdXRhYmxlOiB5b3UgY2FuIGNoYW5nZSBpdCBpbiBwbGFjZSB3aXRoIGluZGV4IG9yIHNsaWNlIGFzc2lnbm1lbnQsIGBhcHBlbmQoKWAsIGBleHRlbmQoKWAsIGBzb3J0KClgIGFuZCBzaW1pbGFyIG1ldGhvZHMuXG5cblR1cGxlIGltbXV0YWJpbGl0eSBpcyBzaGFsbG93LiBJZiBhIHR1cGxlIGhvbGRzIGEgbXV0YWJsZSBvYmplY3QsIHN1Y2ggYXMgYSBsaXN0LCB0aGF0IGlubmVyIG9iamVjdCBjYW4gc3RpbGwgY2hhbmdlLCBhbmQgc28gdGhlIHR1cGxlJ3MgdmFsdWUgY2hhbmdlcy4gQmVjYXVzZSBvZiB0aGlzLCBhIHR1cGxlIGNhbiBiZSBhIGBkaWN0YCBrZXkgb3IgYHNldGAgbWVtYmVyIG9ubHkgaWYgZXZlcnl0aGluZyBpbnNpZGUgaXQgaXMgaGFzaGFibGUuIEEgbGlzdCBjYW4gbmV2ZXIgYmUgYSBrZXkuXG5cblNvdXJjZXM6XG4tIGh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFxuLSBodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3JlZmVyZW5jZS9kYXRhbW9kZWwuaHRtbFxuLSBodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL2xpYnJhcnkvc3RkdHlwZXMuaHRtbCIsImFkZGl0aW9uYWxfa3dhcmdzIjp7fSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2Zkd0JVc05ocjE5Z1FOVmNGc1VlIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6ImVuZF90dXJuIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjg0OTQsIm91dHB1dF90b2tlbnMiOjQxNSwib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6MH0sInNlcnZlcl90b29sX3VzZSI6bnVsbCwic2VydmljZV90aWVyIjoic3RhbmRhcmQifSwibW9kZWxfbmFtZSI6ImNsYXVkZS1zb25uZXQiLCJtb2RlbF9wcm92aWRlciI6ImFudGhyb3BpYyJ9LCJ0eXBlIjoiYWkiLCJuYW1lIjoiZWRpdG9yIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTg2YmItNzY2MS1hNTlmLWEzOWQ2YjRlNmI3NC0wIiwidG9vbF9jYWxscyI6W10sImludmFsaWRfdG9vbF9jYWxscyI6W10sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6ODQ5NCwib3V0cHV0X3Rva2VucyI6NDE1LCJ0b3RhbF90b2tlbnMiOjg5MDksImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjB9fX1dLCJzdHJ1Y3R1cmVkX3Jlc3BvbnNlIjpudWxsfX0=" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + }, + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "73aee64af021a907", + "parentSpanId": "6ad99925c2a25881", + "name": "ChatAnthropic", + "kind": 1, + "startTimeUnixNano": "1790968039099990016", + "endTimeUnixNano": "1790968042916750848", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chat" + } + }, + { + "key": "gen_ai.serialized.name", + "value": { + "stringValue": "ChatAnthropic" + } + }, + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "llm" + } + }, + { + "key": "langsmith.trace.name", + "value": { + "stringValue": "ChatAnthropic" + } + }, + { + "key": "langsmith.trace.session_name", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "anthropic" + } + }, + { + "key": "gen_ai.request.model", + "value": { + "stringValue": "claude-sonnet" + } + }, + { + "key": "gen_ai.tool.definitions", + "value": { + "stringValue": "[{\"name\":\"ls\",\"input_schema\":{\"properties\":{\"path\":{\"description\":\"Absolute path to the directory to list. Must be absolute, not relative.\",\"type\":\"string\"}},\"required\":[\"path\"],\"type\":\"object\"},\"description\":\"Lists all files in a directory.\\n\\nThis is useful for exploring the filesystem and finding the right file to read or edit.\\nYou should almost ALWAYS use this tool before using the read_file or edit_file tools.\"},{\"name\":\"read_file\",\"input_schema\":{\"properties\":{\"file_path\":{\"description\":\"Absolute path to the file to read. Must be absolute, not relative.\",\"type\":\"string\"},\"offset\":{\"default\":0,\"description\":\"Line number to start reading from (0-indexed). Use for pagination of large files.\",\"type\":\"integer\"},\"limit\":{\"default\":100,\"description\":\"Maximum number of lines to read. Use for pagination of large files.\",\"type\":\"integer\"}},\"required\":[\"file_path\"],\"type\":\"object\"},\"description\":\"Reads a file from the filesystem. Assume any path the user provides is valid; reading a missing file returns an error.\\n\\nUsage:\\n- By default, it reads up to 100 lines starting from the beginning of the file. Use `offset`/`limit` to page through large files instead of reading them whole.\\n- A status header, `@@ field | field | ... @@`, sits above the file content, and every line after it is verbatim file content. When content is truncated, there may be an explanation before the header. Never include the header when editing.\\n- Speculatively batch multiple `read_file` calls in one response when several files may be useful.\\n- An empty file returns a system-reminder warning in place of contents.\\n- Large tool results may be offloaded to a file; the tool message gives the path. Read that path here, paging with `offset`/`limit`.\\n- Images (`.png`, `.jpg`, etc.), audio, video, and PDFs return multimodal content blocks (https://docs.langchain.com/oss/python/langchain/messages#multimodal).\\n- For images and PDFs, pagination via `offset`/`limit` is text-only - supply `file_path` only\\n- Always read a file before editing it.\"},{\"name\":\"write_file\",\"input_schema\":{\"properties\":{\"file_path\":{\"description\":\"Absolute path where the file should be written. Must be absolute, not relative.\",\"type\":\"string\"},\"content\":{\"description\":\"The text content to write to the file. This parameter is required.\",\"type\":\"string\"}},\"required\":[\"file_path\",\"content\"],\"type\":\"object\"},\"description\":\"Writes content to a file. Creates the file if it does not exist; replaces it entirely if it does.\\n\\nUsage:\\n- Use this tool when you intend to create a new file or replace the whole file. You do not need to read the file first.\\n- Prefer to edit existing files (with the edit_file tool) over creating new ones when possible.\"},{\"name\":\"edit_file\",\"input_schema\":{\"properties\":{\"file_path\":{\"description\":\"Absolute path to the file to edit. Must be absolute, not relative.\",\"type\":\"string\"},\"old_string\":{\"description\":\"The exact text to find and replace. Must be unique in the file unless replace_all is True.\",\"type\":\"string\"},\"new_string\":{\"description\":\"The text to replace old_string with. Must be different from old_string.\",\"type\":\"string\"},\"replace_all\":{\"default\":false,\"description\":\"If True, replace all occurrences of old_string. If False (default), old_string must be unique.\",\"type\":\"boolean\"}},\"required\":[\"file_path\",\"old_string\",\"new_string\"],\"type\":\"object\"},\"description\":\"Performs exact string replacements in files.\\n\\nUsage:\\n- You must read the file before editing; this tool errors otherwise.\\n- Preserve the exact source indentation from the read output, and never include the read status header in old_string or new_string.\\n- Prefer editing an existing file over creating a new one.\\n- Only use emojis if the user explicitly requests it.\"},{\"name\":\"glob\",\"input_schema\":{\"properties\":{\"pattern\":{\"description\":\"Glob pattern to match files (e.g., '*.py', '**/*.py', '/subdir/**/*.md'). A pattern without '/' matches the file name at any depth; a pattern containing '/' matches the search-root-relative path; a leading '/' anchors to the search root ('/*.py' matches only top-level files). Leading-dot names are excluded unless the pattern segment starts with '.', so prefer the bare form '*.py' over '**/*.py' -- '**' will not descend into dot-directories like '.github'.\",\"type\":\"string\"},\"path\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"null\"}],\"default\":null,\"description\":\"Base directory to search from. Defaults to the backend's default root.\"}},\"required\":[\"pattern\"],\"type\":\"object\"},\"description\":\"Find files matching a glob pattern, returning absolute paths.\\n\\nSupports `*` (any characters within a path segment), `**` (any directories), `?` (single character), `[abc]` (one character from a set), and `{a,b}` (alternatives), e.g. `*.py`, `src/**/*.py`, `*.{yml,yaml}`.\\n\\nA pattern without `/` matches the file name at any depth under the search root (`*.py` matches `src/app/main.py`). A pattern containing `/` matches the search-root-relative path (`src/**/*.py`). A leading `/` anchors to the search root (`/*.py` matches only top-level Python files).\\n\\nLeading-dot names are only matched when the pattern segment itself starts with `.` (use `.env`, or `.github/**/*.yml`). Because `**` will not descend into dot-directories, the bare form `*.yml` is *broader* than `**/*.yml` and is usually what you want.\"},{\"name\":\"grep\",\"input_schema\":{\"properties\":{\"pattern\":{\"description\":\"Text pattern to search for (literal string, not regex).\",\"type\":\"string\"},\"path\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"null\"}],\"default\":null,\"description\":\"Directory to search in. Defaults to current working directory.\"},\"glob\":{\"anyOf\":[{\"type\":\"string\"},{\"type\":\"null\"}],\"default\":null,\"description\":\"Glob pattern (NOT regex) limiting which files are searched (e.g. '*.py', '*.ts'). A pattern without '/' matches the file name at any depth; a pattern containing '/' matches the search-root-relative path (e.g. 'src/**/*.py'). This is an in-tool file filter, not a call to the separate glob tool. Brace expansion (e.g. '*.{ts,tsx}') is not supported on all backends; run a separate search per extension for reliable results.\"},\"output_mode\":{\"default\":\"files_with_matches\",\"description\":\"Shape of the returned text. 'files_with_matches' (default): newline-separated matching file paths. 'content': matching lines grouped by file under a ':' header, each line indented and formatted ': ' (only the matched line, no surrounding context). 'count': one ': ' line per file.\",\"enum\":[\"files_with_matches\",\"content\",\"count\"],\"type\":\"string\"},\"max_count\":{\"anyOf\":[{\"exclusiveMinimum\":0,\"type\":\"integer\"},{\"type\":\"null\"}],\"default\":null,\"description\":\"Optional cap on the total number of matches returned across all files. Leave unset to use the configured default. When the cap is hit, results are truncated and a note says so; narrow the pattern or path to see the rest.\"}},\"required\":[\"pattern\"],\"type\":\"object\"},\"description\":\"Search for a LITERAL text pattern across files (NOT regex).\\n\\nThe pattern is matched verbatim: regex metacharacters are ordinary characters, not operators. To match any of several strings, run a separate grep for each; `grep(pattern=\\\"foo|bar\\\")` searches for the literal text \\\"foo|bar\\\", and `.*` or `\\\\.` match those characters literally.\\n\\nReturns matching files or content per `output_mode`. Offloaded large tool results live under the artifacts root (`/large_tool_results/` by default); grep that directory to search them when you do not know the exact path.\"},{\"name\":\"transfer_to_researcher\",\"input_schema\":{\"properties\":{},\"type\":\"object\"},\"description\":\"Request missing evidence from the researcher\"},{\"name\":\"transfer_to_skeptic\",\"input_schema\":{\"properties\":{},\"type\":\"object\"},\"description\":\"Request another critique from the skeptic\"},{\"name\":\"transfer_to_verifier\",\"input_schema\":{\"properties\":{},\"type\":\"object\"},\"description\":\"Request another source check from the verifier\"}]" + } + }, + { + "key": "langsmith.metadata.ls_integration", + "value": { + "stringValue": "langchain_chat_model" + } + }, + { + "key": "langsmith.metadata.langgraph_step", + "value": { + "intValue": "3" + } + }, + { + "key": "langsmith.metadata.langgraph_node", + "value": { + "stringValue": "model" + } + }, + { + "key": "langsmith.metadata.langgraph_triggers", + "value": { + "stringValue": "[\"branch:to:model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_path", + "value": { + "stringValue": "[\"__pregel_pull\",\"model\"]" + } + }, + { + "key": "langsmith.metadata.langgraph_checkpoint_ns", + "value": { + "stringValue": "editor:42b8f95e-d208-6d0d-c59e-cc043f552134|model:d639a607-1d3b-b4dc-823b-564098184f56" + } + }, + { + "key": "langsmith.metadata.lc_agent_name", + "value": { + "stringValue": "editor" + } + }, + { + "key": "langsmith.metadata.checkpoint_ns", + "value": { + "stringValue": "editor:42b8f95e-d208-6d0d-c59e-cc043f552134" + } + }, + { + "key": "langsmith.metadata.ls_provider", + "value": { + "stringValue": "anthropic" + } + }, + { + "key": "langsmith.metadata.ls_model_name", + "value": { + "stringValue": "claude-sonnet" + } + }, + { + "key": "langsmith.metadata.ls_model_type", + "value": { + "stringValue": "chat" + } + }, + { + "key": "langsmith.metadata.ls_max_tokens", + "value": { + "intValue": "4096" + } + }, + { + "key": "langsmith.metadata.lc_versions", + "value": { + "stringValue": "{\"langchain-core\":\"1.6.6\",\"langchain\":\"1.4.3\",\"langchain-anthropic\":\"1.7.5\"}" + } + }, + { + "key": "langsmith.metadata.ls_method", + "value": { + "stringValue": "traceable" + } + }, + { + "key": "langsmith.metadata.model", + "value": { + "stringValue": "claude-sonnet" + } + }, + { + "key": "langsmith.metadata.max_tokens", + "value": { + "intValue": "4096" + } + }, + { + "key": "langsmith.metadata.model_kwargs", + "value": { + "stringValue": "{\"extra_body\":{\"extra_headers\":{\"anthropic-workspace-id\":\"[redacted workspace]\"}}}" + } + }, + { + "key": "langsmith.metadata.streaming", + "value": { + "boolValue": false + } + }, + { + "key": "langsmith.metadata.max_retries", + "value": { + "intValue": "2" + } + }, + { + "key": "langsmith.metadata._type", + "value": { + "stringValue": "anthropic-chat" + } + }, + { + "key": "langsmith.metadata.usage_metadata", + "value": { + "stringValue": "{\"input_tokens\":8494,\"output_tokens\":415,\"total_tokens\":8909,\"input_token_details\":{\"cache_read\":0,\"cache_creation\":0,\"ephemeral_5m_input_tokens\":0,\"ephemeral_1h_input_tokens\":0},\"output_token_details\":{\"reasoning\":0}}" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_OTLP_TRACES_ENDPOINT", + "value": { + "stringValue": "http://127.0.0.1:58108/langsmith/v1/traces" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_PROJECT", + "value": { + "stringValue": "litellm-fixture" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING", + "value": { + "stringValue": "true" + } + }, + { + "key": "langsmith.metadata.LANGSMITH_TRACING_MODE", + "value": { + "stringValue": "otel" + } + }, + { + "key": "langsmith.metadata.revision_id", + "value": { + "stringValue": "abd4db9" + } + }, + { + "key": "langsmith.span.tags", + "value": { + "stringValue": "seq:step:1" + } + }, + { + "key": "gen_ai.prompt", + "value": { + "bytesValue": "eyJtZXNzYWdlcyI6W1t7ImxjIjoxLCJ0eXBlIjoiY29uc3RydWN0b3IiLCJpZCI6WyJsYW5nY2hhaW4iLCJzY2hlbWEiLCJtZXNzYWdlcyIsIlN5c3RlbU1lc3NhZ2UiXSwia3dhcmdzIjp7ImNvbnRlbnQiOiJZb3UgYXJlIHRoZSBlZGl0b3IuIFVzZSB0aGUgc2hhcmVkIGNvbnZlcnNhdGlvbiB0byB3cml0ZSBvbmUgY29uY2lzZSBhbnN3ZXIgd2l0aCBzb3VyY2UgVVJMcy4gV3JpdGUgdGhlIGZpbmFsIGFuc3dlciB0byAvYW5zd2VyLm1kIGluIHRoZSBzaGFyZWQgdmlydHVhbCBmaWxlc3lzdGVtIGJlZm9yZSByZXBseWluZy4gSWYgaW1wb3J0YW50IGlzc3VlcyByZW1haW4sIGhhbmQgb2ZmIHRvIHRoZSByaWdodCBhZ2VudCBiZWZvcmUgYW5zd2VyaW5nLiIsInR5cGUiOiJzeXN0ZW0ifX0seyJsYyI6MSwidHlwZSI6ImNvbnN0cnVjdG9yIiwiaWQiOlsibGFuZ2NoYWluIiwic2NoZW1hIiwibWVzc2FnZXMiLCJIdW1hbk1lc3NhZ2UiXSwia3dhcmdzIjp7ImNvbnRlbnQiOiJGb3IgYSBzeW50aGV0aWMgdHJhY2luZyBmaXh0dXJlLCBleHBsYWluIFB5dGhvbiB0dXBsZSB2ZXJzdXMgbGlzdCBpbW11dGFiaWxpdHkgaW4gYXQgbW9zdCAxMjAgd29yZHMuIFVzZSBvbmx5IG9mZmljaWFsIFB5dGhvbiBkb2N1bWVudGF0aW9uIGlmIHlvdSBzZWFyY2guIEhhdmUgdGhlIHNrZXB0aWMgYW5kIHZlcmlmaWVyIGNoZWNrIGl0LCB0aGVuIGhhdmUgdGhlIGVkaXRvciB3cml0ZSAvYW5zd2VyLm1kLiBLZWVwIHRoZSBydW4gc2hvcnQgYW5kIGRvIG5vdCBhc2sgZm9sbG93LXVwIHF1ZXN0aW9ucy4iLCJ0eXBlIjoiaHVtYW4iLCJpZCI6IjQ2MjA0MzA1LWY4ZWUtNDM4NC05Y2U4LWZjZDgwYmZjZGY3NSJ9fSx7ImxjIjoxLCJ0eXBlIjoiY29uc3RydWN0b3IiLCJpZCI6WyJsYW5nY2hhaW4iLCJzY2hlbWEiLCJtZXNzYWdlcyIsIkFJTWVzc2FnZSJdLCJrd2FyZ3MiOnsiY29udGVudCI6W3siaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJjYWxsZXIiOnsidHlwZSI6ImRpcmVjdCJ9LCJpbnB1dCI6eyJxdWVyeSI6ImRvY3MucHl0aG9uLm9yZyB0dXBsZXMgaW1tdXRhYmxlIHNlcXVlbmNlcyBsaXN0cyBtdXRhYmxlIGRhdGEgc3RydWN0dXJlcyB0dXRvcmlhbCJ9LCJuYW1lIjoid2ViX3NlYXJjaCIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3OWFXOWJyZW5QcWd5YmZOd1AiLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6Mjg0NSwib3V0cHV0X3Rva2VucyI6NzMsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjB9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InJlc2VhcmNoZXIiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtMjFhMy03MDAwLWEzODEtMjg0YTZhMGMwM2Y3LTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoid2ViX3NlYXJjaCIsImFyZ3MiOnsicXVlcnkiOiJkb2NzLnB5dGhvbi5vcmcgdHVwbGVzIGltbXV0YWJsZSBzZXF1ZW5jZXMgbGlzdHMgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgdHV0b3JpYWwifSwiaWQiOiJ0b29sdV8wMUIyTGZIR0RqWVNGSmhtaVhOajRDdVEiLCJ0eXBlIjoidG9vbF9jYWxsIn1dLCJ1c2FnZV9tZXRhZGF0YSI6eyJpbnB1dF90b2tlbnMiOjI4NDUsIm91dHB1dF90b2tlbnMiOjczLCJ0b3RhbF90b2tlbnMiOjI5MTgsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjB9fSwiaW52YWxpZF90b29sX2NhbGxzIjpbXX19LHsibGMiOjEsInR5cGUiOiJjb25zdHJ1Y3RvciIsImlkIjpbImxhbmdjaGFpbiIsInNjaGVtYSIsIm1lc3NhZ2VzIiwiVG9vbE1lc3NhZ2UiXSwia3dhcmdzIjp7ImNvbnRlbnQiOiJbe1widGl0bGVcIjogXCI1LiBEYXRhIFN0cnVjdHVyZXMgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3R1dG9yaWFsL2RhdGFzdHJ1Y3R1cmVzLmh0bWxcIiwgXCJoaWdobGlnaHRzXCI6IFtcIllvdSBtaWdodCBoYXZlIG5vdGljZWQgdGhhdCBtZXRob2RzIGxpa2VgaW5zZXJ0YCxgcmVtb3ZlYCBvcmBzb3J0YCB0aGF0IG9ubHkgbW9kaWZ5IHRoZSBsaXN0IGhhdmUgbm8gcmV0dXJuIHZhbHVlIHByaW50ZWQgXFx1MjAxMyB0aGV5IHJldHVybiB0aGUgZGVmYXVsdGBOb25lYC4gWzFdIFRoaXMgaXMgYSBkZXNpZ24gcHJpbmNpcGxlIGZvciBhbGwgbXV0YWJsZSBkYXRhIHN0cnVjdHVyZXMgaW4gUHl0aG9uLlxcbi4uLlxcbiMjIDUuMy4gVHVwbGVzIGFuZCBTZXF1ZW5jZXNcXHUwMGI2XFxuXFxuV2Ugc2F3IHRoYXQgbGlzdHMgYW5kIHN0cmluZ3MgaGF2ZSBtYW55IGNvbW1vbiBwcm9wZXJ0aWVzLCBzdWNoIGFzIGluZGV4aW5nIGFuZCBzbGljaW5nIG9wZXJhdGlvbnMuIFRoZXkgYXJlIHR3byBleGFtcGxlcyBvZiBzZXF1ZW5jZSBkYXRhIHR5cGVzIChzZWUgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBsaXN0LCB0dXBsZSwgcmFuZ2UpLiBTaW5jZSBQeXRob24gaXMgYW4gZXZvbHZpbmcgbGFuZ3VhZ2UsIG90aGVyIHNlcXVlbmNlIGRhdGEgdHlwZXMgbWF5IGJlIGFkZGVkLiBUaGVyZSBpcyBhbHNvIGFub3RoZXIgc3RhbmRhcmQgc2VxdWVuY2UgZGF0YSB0eXBlOiB0aGUgdHVwbGUuXFxuXFxuQSB0dXBsZSBjb25zaXN0cyBvZiBhIG51bWJlciBvZiB2YWx1ZXMgc2VwYXJhdGVkIGJ5IGNvbW1hcywgZm9yIGluc3RhbmNlOlxcbi4uLlxcbj4+PiAjIFR1cGxlcyBhcmUgaW1tdXRhYmxlOlxcbj4+PiB0WzBdID0gODg4ODhcXG5UcmFjZWJhY2sgKG1vc3QgcmVjZW50IGNhbGwgbGFzdCk6XFxuICBGaWxlIFxcXCI8c3RkaW4+XFxcIiwgbGluZSAxLCBpbiA8bW9kdWxlPlxcblR5cGVFcnJvcjogJ3R1cGxlJyBvYmplY3QgZG9lcyBub3Qgc3VwcG9ydCBpdGVtIGFzc2lnbm1lbnRcXG4+Pj4gIyBidXQgdGhleSBjYW4gY29udGFpbiBtdXRhYmxlIG9iamVjdHM6XFxuPj4+IHYgPSAoWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuPj4+IHZcXG4oWzEsIDIsIDNdLCBbMywgMiwgMV0pXFxuLi4uXFxuQXMgeW91IHNlZSwgb24gb3V0cHV0IHR1cGxlcyBhcmUgYWx3YXlzIGVuY2xvc2VkIGluIHBhcmVudGhlc2VzLCBzbyB0aGF0IG5lc3RlZCB0dXBsZXMgYXJlIGludGVycHJldGVkIGNvcnJlY3RseTsgdGhleSBtYXkgYmUgaW5wdXQgd2l0aCBvciB3aXRob3V0IHN1cnJvdW5kaW5nIHBhcmVudGhlc2VzLCBhbHRob3VnaCBvZnRlbiBwYXJlbnRoZXNlcyBhcmUgbmVjZXNzYXJ5IGFueXdheSAoaWYgdGhlIHR1cGxlIGlzIHBhcnQgb2YgYSBsYXJnZXIgZXhwcmVzc2lvbikuIEl0IGlzIG5vdCBwb3NzaWJsZSB0byBhc3NpZ24gdG8gdGhlIGluZGl2aWR1YWwgaXRlbXMgb2YgYSB0dXBsZSwgaG93ZXZlciBpdCBpcyBwb3NzaWJsZSB0byBjcmVhdGUgdHVwbGVzIHdoaWNoIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzLCBzdWNoIGFzIGxpc3RzLlxcblxcblRob3VnaCB0dXBsZXMgbWF5IHNlZW0gc2ltaWxhciB0byBsaXN0cywgdGhleSBhcmUgb2Z0ZW4gdXNlZCBpbiBkaWZmZXJlbnQgc2l0dWF0aW9ucyBhbmQgZm9yIGRpZmZlcmVudCBwdXJwb3Nlcy4gVHVwbGVzIGFyZSBpbW11dGFibGUsIGFuZCB1c3VhbGx5IGNvbnRhaW4gYSBoZXRlcm9nZW5lb3VzIHNlcXVlbmNlIG9mIGVsZW1lbnRzIHRoYXQgYXJlIGFjY2Vzc2VkIHZpYSB1bnBhY2tpbmcgKHNlZSBsYXRlciBpbiB0aGlzIHNlY3Rpb24pIG9yIGluZGV4aW5nIChvciBldmVuIGJ5IGF0dHJpYnV0ZSBpbiB0aGUgY2FzZSBvZiBuYW1lZHR1cGxlcykuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG5Bbm90aGVyIHVzZWZ1bCBkYXRhIHR5cGUgYnVpbHQgaW50byBQeXRob24gaXMgdGhlIGRpY3Rpb25hcnkgKHNlZSBNYXBwaW5nIFR5cGVzIFxcdTIwMTQgZGljdCkuIERpY3Rpb25hcmllcyBhcmUgc29tZXRpbWVzIGZvdW5kIGluIG90aGVyIGxhbmd1YWdlcyBhcyBcXHUyMDFjYXNzb2NpYXRpdmUgbWVtb3JpZXNcXHUyMDFkIG9yIFxcdTIwMWNhc3NvY2lhdGl2ZSBhcnJheXNcXHUyMDFkLiBVbmxpa2Ugc2VxdWVuY2VzLCB3aGljaCBhcmUgaW5kZXhlZCBieSBhIHJhbmdlIG9mIG51bWJlcnMsIGRpY3Rpb25hcmllcyBhcmUgaW5kZXhlZCBieSBrZXlzLCB3aGljaCBjYW4gYmUgYW55IGltbXV0YWJsZSB0eXBlOyBzdHJpbmdzIGFuZCBudW1iZXJzIGNhbiBhbHdheXMgYmUga2V5cy4gVHVwbGVzIGNhbiBiZSB1c2VkIGFzIGtleXMgaWYgdGhleSBjb250YWluIG9ubHkgc3RyaW5ncywgbnVtYmVycywgb3IgdHVwbGVzOyBpZiBhIHR1cGxlIGNvbnRhaW5zIGFueSBtdXRhYmxlIG9iamVjdCBlaXRoZXIgZGlyZWN0bHkgb3IgaW5kaXJlY3RseSwgaXQgY2Fubm90IGJlIHVzZWQgYXMgYSBrZXkuIFlvdSBjYW5cXHUyMDE5dCB1c2UgbGlzdHMgYXMga2V5cywgc2luY2UgbGlzdHMgY2FuIGJlIG1vZGlmaWVkIGluIHBsYWNlIHVzaW5nIGluZGV4IGFzc2lnbm1lbnRzLCBzbGljZSBhc3NpZ25tZW50cywgb3IgbWV0aG9kcyBsaWtlIGFwcGVuZCgpIGFuZCBleHRlbmQoKS5cIl19LCB7XCJ0aXRsZVwiOiBcIjMuIERhdGEgbW9kZWwgXFx1MjAxNCBQeXRob24gMy4xNC41IGRvY3VtZW50YXRpb25cIiwgXCJ1cmxcIjogXCJodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3JlZmVyZW5jZS9kYXRhbW9kZWwuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiVGhlIHZhbHVlIG9mIHNvbWUgb2JqZWN0cyBjYW4gY2hhbmdlLiBPYmplY3RzIHdob3NlIHZhbHVlIGNhbiBjaGFuZ2UgYXJlIHNhaWQgdG8gYmUgbXV0YWJsZTsgb2JqZWN0cyB3aG9zZSB2YWx1ZSBpcyB1bmNoYW5nZWFibGUgb25jZSB0aGV5IGFyZSBjcmVhdGVkIGFyZSBjYWxsZWQgaW1tdXRhYmxlLiAoVGhlIHZhbHVlIG9mIGFuIGltbXV0YWJsZSBjb250YWluZXIgb2JqZWN0IHRoYXQgY29udGFpbnMgYSByZWZlcmVuY2UgdG8gYSBtdXRhYmxlIG9iamVjdCBjYW4gY2hhbmdlIHdoZW4gdGhlIGxhdHRlclxcdTIwMTlzIHZhbHVlIGlzIGNoYW5nZWQ7IGhvd2V2ZXIgdGhlIGNvbnRhaW5lciBpcyBzdGlsbCBjb25zaWRlcmVkIGltbXV0YWJsZSwgYmVjYXVzZSB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGl0IGNvbnRhaW5zIGNhbm5vdCBiZSBjaGFuZ2VkLiBTbywgaW1tdXRhYmlsaXR5IGlzIG5vdCBzdHJpY3RseSB0aGUgc2FtZSBhcyBoYXZpbmcgYW4gdW5jaGFuZ2VhYmxlIHZhbHVlLCBpdCBpcyBtb3JlIHN1YnRsZS4pIEFuIG9iamVjdFxcdTIwMTlzIG11dGFiaWxpdHkgaXMgZGV0ZXJtaW5lZCBieSBpdHMgdHlwZTsgZm9yIGluc3RhbmNlLCBudW1iZXJzLCBzdHJpbmdzIGFuZCB0dXBsZXMgYXJlIGltbXV0YWJsZSwgd2hpbGUgZGljdGlvbmFyaWVzIGFuZCBsaXN0cyBhcmUgbXV0YWJsZS5cXG4uLi5cXG5Tb21lIG9iamVjdHMgY29udGFpbiByZWZlcmVuY2VzIHRvIG90aGVyIG9iamVjdHM7IHRoZXNlIGFyZSBjYWxsZWQgY29udGFpbmVycy4gRXhhbXBsZXMgb2YgY29udGFpbmVycyBhcmUgdHVwbGVzLCBsaXN0cyBhbmQgZGljdGlvbmFyaWVzLiBUaGUgcmVmZXJlbmNlcyBhcmUgcGFydCBvZiBhIGNvbnRhaW5lclxcdTIwMTlzIHZhbHVlLiBJbiBtb3N0IGNhc2VzLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIHZhbHVlIG9mIGEgY29udGFpbmVyLCB3ZSBpbXBseSB0aGUgdmFsdWVzLCBub3QgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGNvbnRhaW5lZCBvYmplY3RzOyBob3dldmVyLCB3aGVuIHdlIHRhbGsgYWJvdXQgdGhlIG11dGFiaWxpdHkgb2YgYSBjb250YWluZXIsIG9ubHkgdGhlIGlkZW50aXRpZXMgb2YgdGhlIGltbWVkaWF0ZWx5IGNvbnRhaW5lZCBvYmplY3RzIGFyZSBpbXBsaWVkLiBTbywgaWYgYW4gaW1tdXRhYmxlIGNvbnRhaW5lciAobGlrZSBhIHR1cGxlKSBjb250YWlucyBhIHJlZmVyZW5jZSB0byBhIG11dGFibGUgb2JqZWN0LCBpdHMgdmFsdWUgY2hhbmdlcyBpZiB0aGF0IG11dGFibGUgb2JqZWN0IGlzIGNoYW5nZWQuXFxuLi4uXFxuIyMjIDMuMi41LiBTZXF1ZW5jZXNcXHUwMGI2XFxuLi4uXFxuIyMjIyAzLjIuNS4xLiBJbW11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbkFuIG9iamVjdCBvZiBhbiBpbW11dGFibGUgc2VxdWVuY2UgdHlwZSBjYW5ub3QgY2hhbmdlIG9uY2UgaXQgaXMgY3JlYXRlZC4gKElmIHRoZSBvYmplY3QgY29udGFpbnMgcmVmZXJlbmNlcyB0byBvdGhlciBvYmplY3RzLCB0aGVzZSBvdGhlciBvYmplY3RzIG1heSBiZSBtdXRhYmxlIGFuZCBtYXkgYmUgY2hhbmdlZDsgaG93ZXZlciwgdGhlIGNvbGxlY3Rpb24gb2Ygb2JqZWN0cyBkaXJlY3RseSByZWZlcmVuY2VkIGJ5IGFuIGltbXV0YWJsZSBvYmplY3QgY2Fubm90IGNoYW5nZS4pXFxuXFxuVGhlIGZvbGxvd2luZyB0eXBlcyBhcmUgaW1tdXRhYmxlIHNlcXVlbmNlczpcXG4uLi5cXG5UdXBsZXNcXG46IFRoZSBpdGVtcyBvZiBhIGB0dXBsZWAgYXJlIGFyYml0cmFyeSBQeXRob24gb2JqZWN0cy4gVHVwbGVzIG9mIHR3byBvciBtb3JlIGl0ZW1zIGFyZSBmb3JtZWQgYnkgY29tbWEtc2VwYXJhdGVkIGxpc3RzIG9mIGV4cHJlc3Npb25zLiBBIHR1cGxlIG9mIG9uZSBpdGVtIChhIFxcdTIwMThzaW5nbGV0b25cXHUyMDE5KSBjYW4gYmUgZm9ybWVkIGJ5IGFmZml4aW5nIGEgY29tbWEgdG8gYW4gZXhwcmVzc2lvbiAoYW4gZXhwcmVzc2lvbiBieSBpdHNlbGYgZG9lcyBub3QgY3JlYXRlIGEgdHVwbGUsIHNpbmNlIHBhcmVudGhlc2VzIG11c3QgYmUgdXNhYmxlIGZvciBncm91cGluZyBvZiBleHByZXNzaW9ucykuIEFuIGVtcHR5IHR1cGxlIGNhbiBiZSBmb3JtZWQgYnkgYW4gZW1wdHkgcGFpciBvZiBwYXJlbnRoZXNlcy5cXG4uLi5cXG4jIyMjIDMuMi41LjIuIE11dGFibGUgc2VxdWVuY2VzXFx1MDBiNlxcblxcbk11dGFibGUgc2VxdWVuY2VzIGNhbiBiZSBjaGFuZ2VkIGFmdGVyIHRoZXkgYXJlIGNyZWF0ZWQuIFRoZSBzdWJzY3JpcHRpb24gYW5kIHNsaWNpbmcgbm90YXRpb25zIGNhbiBiZSB1c2VkIGFzIHRoZSB0YXJnZXQgb2YgYXNzaWdubWVudCBhbmQgYGRlbGAgKGRlbGV0ZSkgc3RhdGVtZW50cy5cXG4uLi5cXG5UaGVyZSBhcmUgY3VycmVudGx5IHR3byBpbnRyaW5zaWMgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlczpcXG5cXG5MaXN0c1xcbjogVGhlIGl0ZW1zIG9mIGEgbGlzdCBhcmUgYXJiaXRyYXJ5IFB5dGhvbiBvYmplY3RzLiBMaXN0cyBhcmUgZm9ybWVkIGJ5IHBsYWNpbmcgYSBjb21tYS1zZXBhcmF0ZWQgbGlzdCBvZiBleHByZXNzaW9ucyBpbiBzcXVhcmUgYnJhY2tldHMuIChOb3RlIHRoYXQgdGhlcmUgYXJlIG5vIHNwZWNpYWwgY2FzZXMgbmVlZGVkIHRvIGZvcm0gbGlzdHMgb2YgbGVuZ3RoIDAgb3IgMS4pXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjcgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvYnVpbHRpbnMvc3RkdHlwZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiY29sbGVjdGlvbiBjbGFzc2VzIGFyZSBtdXRhYmxlLiBUaGUgbWV0aG9kcyB0aGF0IGFkZCwgc3VidHJhY3QsIG9yIHJlYXJyYW5nZSB0aGVpciAuLi4gaW4gcGxhY2UsIGFuZCBkb25cXHUyMDE5dCByZXR1cm4gYSAuLi4gLCBuZXZlciByZXR1cm4gdGhlIGNvbGxlY3Rpb24gaW5zdGFuY2UgaXRzZWxmIGJ1dCBgTm9uZWAuXFxuLi4uXFxuIyMgU2VxdWVuY2UgVHlwZXMgXFx1MjAxNCBgbGlzdGAsIGB0dXBsZWAsIGByYW5nZWBcXHUwMGI2XFxuXFxuVGhlcmUgYXJlIHRocmVlIGJhc2ljIHNlcXVlbmNlIHR5cGVzOiBsaXN0cywgdHVwbGVzLCBhbmQgcmFuZ2Ugb2JqZWN0cy4gQWRkaXRpb25hbCBzZXF1ZW5jZSB0eXBlcyB0YWlsb3JlZCBmb3IgcHJvY2Vzc2luZyBvZiBiaW5hcnkgZGF0YSBhbmQgdGV4dCBzdHJpbmdzIGFyZSBkZXNjcmliZWQgaW4gZGVkaWNhdGVkIHNlY3Rpb25zLlxcbi4uLlxcblRoZSBvcGVyYXRpb25zIGluIHRoZSBmb2xsb3dpbmcgdGFibGUgLi4uIG11dGFibGUgYW5kIGltbXV0YWJsZS4gVGhlIGBjb2xsZWN0aW9ucy4gLi4uIGlzIHByb3ZpZGVkIHRvIG1ha2UgLi4uIGVhc2llciB0byBjb3JyZWN0bHkgaW1wbGVtZW50IHRoZXNlIG9wZXJhdGlvbnMgb24gY3VzdG9tIHNlcXVlbmNlIHR5cGVzXFxuLi4uXFxuIyMjIEltbXV0YWJsZSBTZXF1ZW5jZSBUeXBlc1xcdTAwYjZcXG4uLi5cXG5tdXRhYmxlIHNlcXVlbmNlIHR5cGVzIGlzXFxuLi4uXFxuc3VwcG9ydCBhbGxvd3MgaW1tdXRhYmxlIHNlcXVlbmNlcywgLi4uICwgdG8gYmUgdXNlZCBhcyBgZGljdGAga2V5cyBhbmQgc3RvcmVkIGluIC4uLiBlbnNldGAgaW5zdGFuY2VzXFxuLi4uXFxuIyMjIE11dGFibGUgU2VxdWVuY2UgVHlwZXNcXHUwMGI2XFxuLi4uXFxuIyMjIExpc3RzXFx1MDBiNlxcblxcbkxpc3RzIGFyZSBtdXRhYmxlIHNlcXVlbmNlcywgdHlwaWNhbGx5IHVzZWQgdG8gc3RvcmUgY29sbGVjdGlvbnMgb2YgaG9tb2dlbmVvdXMgaXRlbXMgKHdoZXJlIHRoZSBwcmVjaXNlIGRlZ3JlZSBvZiBzaW1pbGFyaXR5IHdpbGwgdmFyeSBieSBhcHBsaWNhdGlvbikuXFxuLi4uXFxuIyMjIFR1cGxlc1xcdTAwYjZcXG5cXG5UdXBsZXMgYXJlIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhldGVyb2dlbmVvdXMgZGF0YSAoc3VjaCBhcyB0aGUgMi10dXBsZXMgcHJvZHVjZWQgYnkgdGhlIGBlbnVtZXJhdGUoKWAgYnVpbHQtaW4pLiBUdXBsZXMgYXJlIGFsc28gdXNlZCBmb3IgY2FzZXMgd2hlcmUgYW4gaW1tdXRhYmxlIHNlcXVlbmNlIG9mIGhvbW9nZW5lb3VzIGRhdGEgaXMgbmVlZGVkIChzdWNoIGFzIGFsbG93aW5nIHN0b3JhZ2UgaW4gYSBgc2V0YCBvciBgZGljdGAgaW5zdGFuY2UpLlxcbi4uLlxcbnR1cGxlKGl0ZXJhYmxlPVxcbi4uLlxcblRoZSBjb25zdHJ1Y3RvciBidWlsZHMgYSB0dXBsZSB3aG9zZSBpdGVtcyBhcmUgdGhlIHNhbWUgYW5kIGluIHRoZSBzYW1lIG9yZGVyIGFzIGl0ZXJhYmxlXFx1MjAxOXMgaXRlbXMuIGl0ZXJhYmxlIG1heSBiZSBlaXRoZXIgYSBzZXF1ZW5jZSwgYSBjb250YWluZXIgdGhhdCBzdXBwb3J0cyBpdGVyYXRpb24sIG9yIGFuIGl0ZXJhdG9yIG9iamVjdC4gSWYgaXRlcmFibGUgaXMgYWxyZWFkeSBhIHR1cGxlLCBpdCBpcyByZXR1cm5lZCB1bmNoYW5nZWQuIEZvciBleGFtcGxlLCBgdHVwbGUoJ2FiYycpYCByZXR1cm5zIGAoJ2EnLCAnYicsICdjJylgIGFuZCBgdHVwbGUoIFsxLCAyLCAzXSApYCByZXR1cm5zIGAoMSwgMiwgMylgLiBJZiBubyBhcmd1bWVudCBpcyBnaXZlbiwgdGhlIGNvbnN0cnVjdG9yIGNyZWF0ZXMgYSBuZXcgZW1wdHkgdHVwbGUsIGAoKWAuXFxuLi4uXFxuVHVwbGVzIGltcGxlbWVudCBhbGwgb2YgdGhlIGNvbW1vbiBzZXF1ZW5jZSBvcGVyYXRpb25zLlxcbi4uLlxcbkZvciBoZXRlcm9nZW5lb3VzIGNvbGxlY3Rpb25zIG9mIGRhdGEgd2hlcmUgYWNjZXNzIGJ5IG5hbWUgaXMgY2xlYXJlciB0aGFuIGFjY2VzcyBieSBpbmRleCwgYGNvbGxlY3Rpb25zLm5hbWVkdHVwbGUoKWAgbWF5IGJlIGEgbW9yZSBhcHByb3ByaWF0ZSBjaG9pY2UgdGhhbiBhIHNpbXBsZSB0dXBsZSBvYmplY3QuXCJdfSwge1widGl0bGVcIjogXCJCdWlsdC1pbiBUeXBlcyBcXHUyMDE0IFB5dGhvbiAzLjE0LjUgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvbGlicmFyeS9zdGR0eXBlcy5odG1sXCIsIFwiaGlnaGxpZ2h0c1wiOiBbXCJTb21lIGNvbGxlY3Rpb24gY2xhc3NlcyBhcmUgbXV0YWJsZS4gVGhlIG1ldGhvZHMgdGhhdCBhZGQsIHN1YnRyYWN0LCBvciByZWFycmFuZ2UgdGhlaXIgLi4uIGluIHBsYWNlLCBhbmQgZG9uXFx1MjAxOXQgcmV0dXJuIGEgLi4uICwgbmV2ZXIgcmV0dXJuIHRoZSBjb2xsZWN0aW9uIGluc3RhbmNlIGl0c2VsZiBidXQgYE5vbmVgLlxcbi4uLlxcbiMjIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgYGxpc3RgLCBgdHVwbGVgLCBgcmFuZ2VgXFx1MDBiNlxcblxcblRoZXJlIGFyZSB0aHJlZSBiYXNpYyBzZXF1ZW5jZSB0eXBlczogbGlzdHMsIHR1cGxlcywgYW5kIC4uLiBvYmplY3RzLiBBZGRpdGlvbmFsIHNlcXVlbmNlIHR5cGVzIHRhaWxvcmVkIGZvciBwcm9jZXNzaW5nIG9mIGJpbmFyeSBkYXRhIGFuZCB0ZXh0IHN0cmluZ3MgYXJlIGRlc2NyaWJlZCBpbiBkZWRpY2F0ZWQgc2VjdGlvbnMuXFxuLi4uXFxuVGhlIG9wZXJhdGlvbnMgaW4gdGhlIGZvbGxvd2luZyB0YWJsZSAuLi4gaW1tdXRhYmxlLiBUaGUgYCAuLi4gaXMgcHJvdmlkZWQgdG8gbWFrZSAuLi4gZWFzaWVyIHRvIGNvcnJlY3RseSBpbXBsZW1lbnQgdGhlc2Ugb3BlcmF0aW9ucyBvbiBjdXN0b20gc2VxdWVuY2UgdHlwZXNcXG4uLi5cXG4jIyMgSW1tdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbm9wZXJhdGlvbiB0aGF0IGltbXV0YWJsZSBzZXF1ZW5jZSAuLi4gYnkgbXV0YWJsZSBzZXF1ZW5jZSB0eXBlcyBpcyAuLi4gYGhhc2goKWBcXG4uLi5cXG5UaGlzIHN1cHBvcnQgYWxsb3dzIGltbXV0YWJsZSBzZXF1ZW5jZXMsIHN1Y2ggYXMgYHR1cGxlYCBpbnN0YW5jZXMsIHRvIGJlIHVzZWQgYXMgYGRpY3RgIGtleXMgYW5kIHN0b3JlZCBpbiBgc2V0YCBhbmQgYGZyb3plbnNldGAgaW5zdGFuY2VzLlxcbi4uLlxcbiMjIyBNdXRhYmxlIFNlcXVlbmNlIFR5cGVzXFx1MDBiNlxcbi4uLlxcbiMjIyBMaXN0c1xcdTAwYjZcXG5cXG5MaXN0cyBhcmUgbXV0YWJsZSBzZXF1ZW5jZXMsIHR5cGljYWxseSB1c2VkIHRvIHN0b3JlIGNvbGxlY3Rpb25zIG9mIGhvbW9nZW5lb3VzIGl0ZW1zICh3aGVyZSB0aGUgcHJlY2lzZSBkZWdyZWUgb2Ygc2ltaWxhcml0eSB3aWxsIHZhcnkgYnkgYXBwbGljYXRpb24pLlxcbi4uLlxcbiMjIyBUdXBsZXNcXHUwMGI2XFxuXFxuVHVwbGVzIGFyZSBpbW11dGFibGUgc2VxdWVuY2VzLCB0eXBpY2FsbHkgdXNlZCB0byBzdG9yZSBjb2xsZWN0aW9ucyBvZiBoZXRlcm9nZW5lb3VzIGRhdGEgKHN1Y2ggYXMgdGhlIDItdHVwbGVzIHByb2R1Y2VkIGJ5IHRoZSBgZW51bWVyYXRlKClgIGJ1aWx0LWluKS4gVHVwbGVzIGFyZSBhbHNvIHVzZWQgZm9yIGNhc2VzIHdoZXJlIGFuIGltbXV0YWJsZSBzZXF1ZW5jZSBvZiBob21vZ2VuZW91cyBkYXRhIGlzIG5lZWRlZCAoc3VjaCBhcyBhbGxvd2luZyBzdG9yYWdlIGluIGEgYHNldGAgb3IgYGRpY3RgIGluc3RhbmNlKS5cXG4uLi5cXG5UaGUgY29uc3RydWN0b3IgYnVpbGRzIGEgdHVwbGUgd2hvc2UgaXRlbXMgYXJlIHRoZSBzYW1lIGFuZCBpbiB0aGUgc2FtZSBvcmRlciBhcyBpdGVyYWJsZVxcdTIwMTlzIGl0ZW1zLiBpdGVyYWJsZSBtYXkgYmUgZWl0aGVyIGEgc2VxdWVuY2UsIGEgY29udGFpbmVyIHRoYXQgc3VwcG9ydHMgaXRlcmF0aW9uLCBvciBhbiBpdGVyYXRvciBvYmplY3QuIElmIGl0ZXJhYmxlIGlzIGFscmVhZHkgYSB0dXBsZSwgaXQgaXMgcmV0dXJuZWQgdW5jaGFuZ2VkLiBGb3IgZXhhbXBsZSwgYHR1cGxlKCdhYmMnKWAgcmV0dXJucyBgKCdhJywgJ2InLCAnYycpYCBhbmQgYHR1cGxlKCBbMSwgMiwgM10gKWAgcmV0dXJucyBgKDEsIDIsIDMpYC4gSWYgbm8gYXJndW1lbnQgaXMgZ2l2ZW4sIHRoZSBjb25zdHJ1Y3RvciBjcmVhdGVzIGEgbmV3IGVtcHR5IHR1cGxlLCBgKClgLlxcbi4uLlxcblR1cGxlcyBpbXBsZW1lbnQgYWxsIG9mIHRoZSBjb21tb24gc2VxdWVuY2Ugb3BlcmF0aW9ucy5cXG4uLi5cXG5Gb3IgaGV0ZXJvZ2VuZW91cyBjb2xsZWN0aW9ucyBvZiBkYXRhIHdoZXJlIGFjY2VzcyBieSBuYW1lIGlzIGNsZWFyZXIgdGhhbiBhY2Nlc3MgYnkgaW5kZXgsIGBjb2xsZWN0aW9ucy5uYW1lZHR1cGxlKClgIG1heSBiZSBhIG1vcmUgYXBwcm9wcmlhdGUgY2hvaWNlIHRoYW4gYSBzaW1wbGUgdHVwbGUgb2JqZWN0LlwiXX0sIHtcInRpdGxlXCI6IFwiNS4gRGF0YSBTdHJ1Y3R1cmVzIFxcdTIwMTQgUHl0aG9uIDMuMTAuMjAgZG9jdW1lbnRhdGlvblwiLCBcInVybFwiOiBcImh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMuMTAvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFwiLCBcImhpZ2hsaWdodHNcIjogW1wiWW91IG1pZ2h0IGhhdmUgbm90aWNlZCB0aGF0IG1ldGhvZHMgbGlrZSBgaW5zZXJ0YCwgYHJlbW92ZWAgb3IgYHNvcnRgIHRoYXQgb25seSBtb2RpZnkgdGhlIGxpc3QgaGF2ZSBubyByZXR1cm4gdmFsdWUgcHJpbnRlZCBcXHUyMDEzIHRoZXkgcmV0dXJuIHRoZSBkZWZhdWx0IGBOb25lYC4gMSBUaGlzIGlzIGEgZGVzaWduIHByaW5jaXBsZSBmb3IgYWxsIG11dGFibGUgZGF0YSBzdHJ1Y3R1cmVzIGluIFB5dGhvbi5cXG4uLi5cXG4jIyA1LjMuIFR1cGxlcyBhbmQgU2VxdWVuY2VzXFx1MDBiNlxcblxcbldlIHNhdyB0aGF0IGxpc3RzIGFuZCBzdHJpbmdzIGhhdmUgbWFueSBjb21tb24gcHJvcGVydGllcywgc3VjaCBhcyBpbmRleGluZyBhbmQgc2xpY2luZyBvcGVyYXRpb25zLiBUaGV5IGFyZSB0d28gZXhhbXBsZXMgb2Ygc2VxdWVuY2UgZGF0YSB0eXBlcyAoc2VlIFNlcXVlbmNlIFR5cGVzIFxcdTIwMTQgbGlzdCwgdHVwbGUsIHJhbmdlKS4gU2luY2UgUHl0aG9uIGlzIGFuIGV2b2x2aW5nIGxhbmd1YWdlLCBvdGhlciBzZXF1ZW5jZSBkYXRhIHR5cGVzIG1heSBiZSBhZGRlZC4gVGhlcmUgaXMgYWxzbyBhbm90aGVyIHN0YW5kYXJkIHNlcXVlbmNlIGRhdGEgdHlwZTogdGhlIHR1cGxlLlxcblxcbkEgdHVwbGUgY29uc2lzdHMgb2YgYSBudW1iZXIgb2YgdmFsdWVzIHNlcGFyYXRlZCBieSBjb21tYXMsIGZvciBpbnN0YW5jZTpcXG4uLi5cXG4+Pj4gIyBUdXBsZXMgYXJlIGltbXV0YWJsZTpcXG4uLi4gdFswXSA9IDg4ODg4XFxuVHJhY2ViYWNrIChtb3N0IHJlY2VudCBjYWxsIGxhc3QpOlxcbiAgRmlsZSBcXFwiPHN0ZGluPlxcXCIsIGxpbmUgMSwgaW4gPG1vZHVsZT5cXG5UeXBlRXJyb3I6ICd0dXBsZScgb2JqZWN0IGRvZXMgbm90IHN1cHBvcnQgaXRlbSBhc3NpZ25tZW50XFxuPj4+ICMgYnV0IHRoZXkgY2FuIGNvbnRhaW4gbXV0YWJsZSBvYmplY3RzOlxcbi4uLiB2ID0gKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbj4+PiB2XFxuKFsxLCAyLCAzXSwgWzMsIDIsIDFdKVxcbi4uLlxcbkFzIHlvdSBzZWUsIG9uIG91dHB1dCB0dXBsZXMgYXJlIGFsd2F5cyBlbmNsb3NlZCBpbiBwYXJlbnRoZXNlcywgc28gdGhhdCBuZXN0ZWQgdHVwbGVzIGFyZSBpbnRlcnByZXRlZCBjb3JyZWN0bHk7IHRoZXkgbWF5IGJlIGlucHV0IHdpdGggb3Igd2l0aG91dCBzdXJyb3VuZGluZyBwYXJlbnRoZXNlcywgYWx0aG91Z2ggb2Z0ZW4gcGFyZW50aGVzZXMgYXJlIG5lY2Vzc2FyeSBhbnl3YXkgKGlmIHRoZSB0dXBsZSBpcyBwYXJ0IG9mIGEgbGFyZ2VyIGV4cHJlc3Npb24pLiBJdCBpcyBub3QgcG9zc2libGUgdG8gYXNzaWduIHRvIHRoZSBpbmRpdmlkdWFsIGl0ZW1zIG9mIGEgdHVwbGUsIGhvd2V2ZXIgaXQgaXMgcG9zc2libGUgdG8gY3JlYXRlIHR1cGxlcyB3aGljaCBjb250YWluIG11dGFibGUgb2JqZWN0cywgc3VjaCBhcyBsaXN0cy5cXG5cXG5UaG91Z2ggdHVwbGVzIG1heSBzZWVtIHNpbWlsYXIgdG8gbGlzdHMsIHRoZXkgYXJlIG9mdGVuIHVzZWQgaW4gZGlmZmVyZW50IHNpdHVhdGlvbnMgYW5kIGZvciBkaWZmZXJlbnQgcHVycG9zZXMuIFR1cGxlcyBhcmUgaW1tdXRhYmxlLCBhbmQgdXN1YWxseSBjb250YWluIGEgaGV0ZXJvZ2VuZW91cyBzZXF1ZW5jZSBvZiBlbGVtZW50cyB0aGF0IGFyZSBhY2Nlc3NlZCB2aWEgdW5wYWNraW5nIChzZWUgbGF0ZXIgaW4gdGhpcyBzZWN0aW9uKSBvciBpbmRleGluZyAob3IgZXZlbiBieSBhdHRyaWJ1dGUgaW4gdGhlIGNhc2Ugb2YgYG5hbWVkdHVwbGVzYCkuIExpc3RzIGFyZSBtdXRhYmxlLCBhbmQgdGhlaXIgZWxlbWVudHMgYXJlIHVzdWFsbHkgaG9tb2dlbmVvdXMgYW5kIGFyZSBhY2Nlc3NlZCBieSBpdGVyYXRpbmcgb3ZlciB0aGUgbGlzdC5cXG4uLi5cXG4jIyA1LjQuXFxuLi4uXFxuLiBTZXQgb2JqZWN0c1xcbi4uLlxcbiMjIDUuNS4gRGljdGlvbmFyaWVzXFx1MDBiNlxcblxcbkFub3RoZXIgdXNlZnVsIGRhdGEgdHlwZSBidWlsdCBpbnRvIFB5dGhvbiBpcyB0aGUgZGljdGlvbmFyeSAoc2VlIE1hcHBpbmcgVHlwZXMgXFx1MjAxNCBkaWN0KS4gRGljdGlvbmFyaWVzIGFyZSBzb21ldGltZXMgZm91bmQgaW4gb3RoZXIgbGFuZ3VhZ2VzIGFzIFxcdTIwMWNhc3NvY2lhdGl2ZSBtZW1vcmllc1xcdTIwMWQgb3IgXFx1MjAxY2Fzc29jaWF0aXZlIGFycmF5c1xcdTIwMWQuIFVubGlrZSBzZXF1ZW5jZXMsIHdoaWNoIGFyZSBpbmRleGVkIGJ5IGEgcmFuZ2Ugb2YgbnVtYmVycywgZGljdGlvbmFyaWVzIGFyZSBpbmRleGVkIGJ5IGtleXMsIHdoaWNoIGNhbiBiZSBhbnkgaW1tdXRhYmxlIHR5cGU7IHN0cmluZ3MgYW5kIG51bWJlcnMgY2FuIGFsd2F5cyBiZSBrZXlzLiBUdXBsZXMgY2FuIGJlIHVzZWQgYXMga2V5cyBpZiB0aGV5IGNvbnRhaW4gb25seSBzdHJpbmdzLCBudW1iZXJzLCBvciB0dXBsZXM7IGlmIGEgdHVwbGUgY29udGFpbnMgYW55IG11dGFibGUgb2JqZWN0IGVpdGhlciBkaXJlY3RseSBvciBpbmRpcmVjdGx5LCBpdCBjYW5ub3QgYmUgdXNlZCBhcyBhIGtleS4gWW91IGNhblxcdTIwMTl0IHVzZSBsaXN0cyBhcyBrZXlzLCBzaW5jZSBsaXN0cyBjYW4gYmUgbW9kaWZpZWQgaW4gcGxhY2UgdXNpbmcgaW5kZXggYXNzaWdubWVudHMsIHNsaWNlIGFzc2lnbm1lbnRzLCBvciBtZXRob2RzIGxpa2UgYGFwcGVuZCgpYCBhbmQgYGV4dGVuZCgpYC5cIl19XSIsInR5cGUiOiJ0b29sIiwibmFtZSI6IndlYl9zZWFyY2giLCJpZCI6IjZlNTEyNWFjLTMxNjQtNGE3Ni05YzUyLWFjMzQ2MGNlMzZlNCIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxQjJMZkhHRGpZU0ZKaG1pWE5qNEN1USIsInN0YXR1cyI6InN1Y2Nlc3MifX0seyJsYyI6MSwidHlwZSI6ImNvbnN0cnVjdG9yIiwiaWQiOlsibGFuZ2NoYWluIiwic2NoZW1hIiwibWVzc2FnZXMiLCJBSU1lc3NhZ2UiXSwia3dhcmdzIjp7ImNvbnRlbnQiOlt7ImlkIjoidG9vbHVfMDFYamozYVJkV3loQkpmUG1USkJ4UWE5IiwiY2FsbGVyIjp7InR5cGUiOiJkaXJlY3QifSwiaW5wdXQiOnt9LCJuYW1lIjoidHJhbnNmZXJfdG9fc2tlcHRpYyIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QTZGNGNDTjV5aEQ0Q3JzOU0iLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjB9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InJlc2VhcmNoZXIiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtM2NkOC03OWYxLTg4YTEtY2U2Y2VlMjk0ZTc5LTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoidHJhbnNmZXJfdG9fc2tlcHRpYyIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxWGpqM2FSZFd5aEJKZlBtVEpCeFFhOSIsInR5cGUiOiJ0b29sX2NhbGwifV0sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzM3OCwib3V0cHV0X3Rva2VucyI6MzMsInRvdGFsX3Rva2VucyI6NzQxMSwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6MH19LCJpbnZhbGlkX3Rvb2xfY2FsbHMiOltdfX0seyJsYyI6MSwidHlwZSI6ImNvbnN0cnVjdG9yIiwiaWQiOlsibGFuZ2NoYWluIiwic2NoZW1hIiwibWVzc2FnZXMiLCJUb29sTWVzc2FnZSJdLCJrd2FyZ3MiOnsiY29udGVudCI6IlN1Y2Nlc3NmdWxseSB0cmFuc2ZlcnJlZCB0byBza2VwdGljIiwidHlwZSI6InRvb2wiLCJuYW1lIjoidHJhbnNmZXJfdG9fc2tlcHRpYyIsImlkIjoiMzg5MDU4ZDEtMDgzZi00NjMyLWJiYzUtMDliZmQzYTBmZmQyIiwidG9vbF9jYWxsX2lkIjoidG9vbHVfMDFYamozYVJkV3loQkpmUG1USkJ4UWE5Iiwic3RhdHVzIjoic3VjY2VzcyJ9fSx7ImxjIjoxLCJ0eXBlIjoiY29uc3RydWN0b3IiLCJpZCI6WyJsYW5nY2hhaW4iLCJzY2hlbWEiLCJtZXNzYWdlcyIsIkFJTWVzc2FnZSJdLCJrd2FyZ3MiOnsiY29udGVudCI6W3sic2lnbmF0dXJlIjoiQ0FRUzFRZ0tFQWdTR0FJNEFVSUlkR2hwYm10cGJtY1NETjZUZlU2TkRNOTcxbEVienhvTXZSVUExSjlveDNmSlFZVDlJakFyQ1FRR0hCUDRwUkhQYXZYelJsYk1iYVlIQzU3WWorR2J4Z3IwaGVsdmQyT0lML282UmJ0NFBGQTNGYUlGVWxncThnYzdLUUk5RnRHbnZTQWtMQlJ6SXNVcC8yZDVSZDJ6Z0VzZmtGVmVYc244V1lZWmtpbkFRVlZ5NkE3QjNMYUVKVzB4Yi94ODhEaTY3ZmVkd3gwUStGbFp0d2NtVDNEY2FhMTFKdmV5SFJyM2RtK3pHU1pQNkNYZVhOSjhuclRaN1cxWVJUeXNENnc3bzdMcDhoMVJ3cTRmNG84aitRVWZZMHZqZSt6SlpzeEhQbVFOdDN5a2VLaDk5aGduNmRiaXo2L0VIcW1iSGpTQXUxTTRTcUVEZnFJS1RoSFllbmFtQVptY1E0WEFPbzZtZFE2Kzc3VmdwVjFsQzEvazUySWlVb0pqa0dsUXJuTEN2dXRpSTdLTitzZ0s4YzIrMktDSnJQUkN2VlYrQmtTdHZQNWpXV3F6eXRwS1Erbkh6aEw0NDhPcklBOWdkSkMzOUM4ZURyWGVSbkk3dDRGcWxrWEdvTWxaQVY5VU1jclFIUnhPTWk4dE95VHppOW95QkZEVFNzUStMSnFlcjB0ejFGdDlnZWs2dnpnNTBTcFZBRlFkTEVSdjhSMVlxTWFlSSt4c3ZUQUVXOU1SZ3ZSTWt6THV6UlQyZ3V3M2pEK0ZqaXAzN3hnSzVRSTBSK0NzT0FtUVBvVFppenpjSXlBVUF1Q0pGQ2dxL2dEL0NpYkx4b3ltaVZVSTFhbjlTbnQzS3BOUm9XZHZXSW5lMzNWMHB2OFBIZzl5VzJKdUxPcVFHaWxMQ3NLL1Ezb1FyM3VwRU1YeFhHakNMN1dVaXp2UlpHRkVFVitWcGxZdDd2NHRLWk1qNXBrM3ZkZU0wTUo1WFpnN1B2VXNSenljeGNuclQzS25vYzNxTSt0dHFaZlRzYjhkNlUvcFFpZnE3R3VhaEZPSHJ2cm9xZ0pMaisvODFZeTFwWkZramZKZ1kwWGtsekFuQTZDRnEvWVQwZ3hiVitUSDVJUytXYVlPVXpLMzZTOHM1cXFNd2huZ3Y0SXpFM2N0WFhxbmhJWlhOem55VkJxYllhL0d4MmJDamNFajJuNVJ5R1lSUy8rTWN1bGdMQnpuUTlNa0cyRDdRUDNNcS9ud0hYS01JYUxTOFZLOGhHM2QwckFqbHZuYXVYU2hOckp4dGtqNHFrd254RDRMckVqazF1ZStQTGNiTHFZV3dBcjhuMHMrNDFqcnNJUFZ5eUs5dkZhRVpyQTlLYUJ4a0p5aXF4NTBHWk5uN2NreCtta0kxOGdINTJpeVZQdFU4RktNWGp1Z0NsNm1QTGlIWWlEUWQrRzYzTHVkbU9LeXM5aXRWbkJEQk5NUDJiL0k3K2JJVlJsaGVHYVpXcjZNQVRLbDg2QjN5KzBrdU9yTFJNeVdjZGliYzJpUWtabSs4ZjdkVFV3YktiQkpzS09GWTczLzhubEwvSmlZcGp6Nm5VSWsrSXFTamFGeUs2WEZsQ2N1a1dwV0hwREpuU3ZiRXpleTdxSHlDcUorSStvbjhpUDhHT3RlbXBRYVMwLzRrWkdTd2xyRTZ4T2o2RXdHcGZqYkRaS0hrUC8yUkFkTllQUXEzejdiZFZKTUpObkZNejZlMzdtek1wQ0JpaHdQZFNGVFJFM1NDWTY2d3ZUZ0p6R0NVeWhIYlNpNWkxYXp6T1hkOHVIQ1NwVC82TDFvNlZVMHBqb1I4S2FhNHdDV0hHZjdQL1dFZCthdnlwMDJKbnUxY21wVzArR3Erdks5aDB0ZGIySUNsTS9lTmZkb3VRUCtlcDhNSlVqcmFEUklDbUFKUVk1b2JTNzF2bmlPbytyZWFGeTB6UmdCIiwidGhpbmtpbmciOiIiLCJ0eXBlIjoidGhpbmtpbmcifSx7InNpZ25hdHVyZSI6IkNBUVMvUkVLRVFnU0dBSTRBVUlKYm1GeWNtRjBhVzl1RWd3RmgzQVZKWlAzczh5MUhxMGFERzc2dWhIT05hU2NYQUdTaGlJd0dnakxYSFV4MXlacmtMUGxqanpUT1F5Y3BMSmlLVndYY3htL0V6S0ptMXM4TTVXeHVSRVdFbGRYY29hTjAvbXNLcGtSTVQrZUxiNWloOXJBSFVMVG9tVU5zVXdKZXUwanJKZTRHTGF6eTdzUDVLdGpFT0VCS1FzWFVmb1E3ZkRrWk9RVUhaaTl6elFONGhGTm1ZSlVmM09WcWY5VFFOZEMxK20vL0hBWlBmV21BeTZIdER2bTdrZ0JKa0NFb2lGbm03dG4vUFgxaFhHUFJnV2M2QlRYVU1yMldBOGFLeXFnZ1JVYk1IMitKWXY1aVdaZGRDYzF1cDFOYnIzc0Y3V1Bjb3Q5WkJoeUhRWHI2OTJ6ZUprNlNKZ3lnSVZyQVNiTWx5MmJCeFRXSkI2MjNnVmd6cENtVlNMWkZwa0tYZ1BjcDhoR3lES092RjZTaVFTSHFHUnhRb2ZyN1ZzMWFQaGZaaXhST0pHRCtNbHJrZ2dWQW5HTXlZSURVWnBhdnRDZWlVMUptQlN6MDA4NGJYUHRUL3RucHNLT25TUFlaUW8zQjBCWEticUEvV3NLbmc4L1YzWjhVdWFqbzZJVloyaGdBMVdEZHVxUU9Ra0V4R1U2cEZ1TGhlYklqdVd4U05JL0RmZnZSLy9MTnFDZnZkbmYwaHZvUHREeGZ4L1Nhd0l4NC9zcHE5eGJ3RnpIdExnb0hnMUYyd0Z3N2NsUERsZzFQeUQvdjJ5SkczVjlUOEdMNmpHUEU2cmhmaWwvdTRITnZmN0JXMGRRekNUVFhjcVp5NG5CeXkwK3ArUzdaeGZNbDRLcHhhMmdPaGhTTmpmcGZ1WDBRcEw0a01FOEoxbmxpRk4xZDhLcUVQdURJMThCUUhVaEQvVWZZMjlvQTB4NTEwRmpNL0JzTFo0bjVzcHBrb0lpN0NGb1FxMmFBS1ZOZkMxS2lsaElwUkQrMWR5RHRaUWtsTjhnWnBPRVNIdzZNTDFUdFUweWZBY2JtcW94SjZoaGp6d0g2Q01zVFFFRXk4TWJSQk0rUUtLa0lSZFJDUEhHRlcyTGt1UjUwK0owcEYvdHI2cHFWdkplb3pmaU1uckVpbEIyN1RaUDRuS1RKRzhyZC8vdkNXZW4weTdDQS84ZEJKOWdkcDV0eDdOVkE0NWsxZ2RjeEVSMWdZb3VOWDNuMXZHMy9CMUY0Kzkzd1hXOU1xbzZVczU5VFFtcmI5TnFhN3BTUU5TckgwNncxR3h5QkFsa0ZmcUFsYWZrMTZhTXBxNDE2VEpZcEd5NDNXQ1VpQ2NMV2xoaVdnejJzZ09MQVhQOWE1d2VXQUtSQzhKNmprY1VrRGVYblcrSEZTaVBNNis5Qm82a1ZoTFVoTjVyYzRtSFpPWnBGN3B0UHBzUU40WENxTFBzcXBSSC84bzVYcHlDNlpuZVRlRjJFeHRqaHdaVmpYRHdNYkl6VllJYncxeTJQUEd3OGRDN3BjNHNJVlNuSTRvUUFIMHY2NTBnaGZobU1ldldXNGg0UHlsYncvREc0ZmRlVDMrOHowZjhNT1NUdGZpcHgwNS9RdHRIbEF2T0RSeGE1eURDdk96Nm4zZjFCTzBOLzRNYkd5T00yN3FXNHg4bUJWb3J5VVpXZnNtaGZpMC9HZk52QjgreWxUR2JZTlgyN09jRUY1V1p0TnFiV0V5N1Y1R2JtWlVTeHNwY2c3RkJaM2ozLzNMN211dlpYb1ppMWY1ZXJxNHkzbFNIK29GN3FQRm52aWV5TE9Dd3ViY1F5K295b3dqNGNEMGlkUmVjbnArcys1MkQ3K216RjFncFcrZitjSmFzb3hpZ2J4a09sb1M0eWVpS3ZnTTRuV1NwR0VqMTJSUHg1bzgwRW16aVhOOXQ0ekp6VzFodEE1aFhKSmVGeW1wTVl0eUNVQ2V2UldWbXJSWWt0UTdVbkNhNnRML0FWUGkxbmk1WENYSGZMT3VxZks1S0xZeGp3VnRkaGhmbUNMdnBhS1ZOQ0VPQ2Y0bk5HSHFEOTh3VWIwZWpXTndEWUJuSkdrQlJ3OVJWc3Bmd05kb1dvVmNuMnF2OHl3R1VwN3ljL3NrMkxwSTZWemlSeXZXcWJHTkcxWkpxVlZ1dFJjNDYrc09FNFZTZVMyL0NUTktOYTZOTUNmL2h4eGFVVnNZZktkUCtTMUxjNGlhbktwMHNVdWdQcG10b3lkQzBDUDJSd2puSExjbnp4Q252cXhsTFFXWE1OUFRzdTJ5QmNIbFdKTHRjbWRnL01jTGFneTdsNWtHNnNON1p4SkxEL2tBeDhFNnVxb2hQK0svak1Tcll1UUpuaHI0dGY0YmlXRlkzSnpmL0twQVl5VlNxK3BiZkZLWE1iTDluSEVvWlFUMTVXeWFiZDZETUFxM1FaT3RoTTVwVXpkTnFVWGtjckkxUHdqQVNBZzRkVlUvMm8rOThtVFhueWhQN1ZsRWtUbmRkcHd5SkYrWUNDYzAxcHFhNlpYOTlwblIrWU9wSXJiNUR0UGNLcTB0NWNGQ0R5UXhOMWxVdXFaY1l1anZYL0dDNys2SXBSVHovTmtFWS81Y250T2ZaSnlkNlNrVXgvSlI1QUpJUlBDTmVpYjByY1dubkZuMFhLOHMyMng0MHdMakV0dWZYRE85K3lyYWY0a0h4bEJJM1NCMG1lTWplcFg1MENKaEF4N05la2tSRm53b2VjTGZxSXlyalhyZ0Fac1RNUGlYOTRzbmpkcnppc0FYdEJLYW51bGlsU3hPZlcySFNtZm5rMEwyR1hISkozUyt2Q2JnUkVtbDlkZWtmbUhobXdFWXR2M2tmaHRoN0VSSXdWVDhqNjlPdElCQ21lVElKdUE3bzczUFhjMUxKMndodVpLN0ZaZTZOVE9zNjFNUDdKVmxJYzZ0Ymh0T0JrM05YVThwQlBPQ1k2UTU1ZDYzRnRwVVUvS0NiUTBLMldROW81ak5Hd05OSjZDdzVGREN0K3g1WUplUDFkMlF6RFNpcStjU1pxd3FDWlZjTm9BZnRHR3pnclJaNHZrcGJQMHhRWkhDdFdzVXdIVlRqclk0TmZSRGpIZFZQUm44SjlYdG9vYy9oSVdrcXpGYWN6WVRsYXB1MlJDSnFlOGFFbzNqeldaQ0VKN0FpQnpCWXJhWURnT2ZlaVF5ZUpVc2JiWEhkR0tpaXcyMkJkcDRNK1l6NjcxTVowaWU5RVdSa3MwUTJESEdVVG5lOVM0dEFBSlBHYlVmNHh0Q01vSjQwUTRRVzA1UUxTWnkzd05GQVVGWW1YRTRFQ0RGQjgrSUVlTkZGRi90Z0FuZmg4R3ZGaC8xeFkrTEUrb3pNbWlualNSd2hxMDl6UUFFa05GeXUwSUtndkRreU9JK2ljY0Y3ZG9XTFRVYXNQeExIMXhHUEJTS2tlV0NuU29YLzRDMmM5ZHU2NkJiUlhhbUhxYWUrVkNpUzZCTjk1M2lZS091OHJNa25lVytURmpWMEN6QWY4UE5HOUxCYXlVZ1k1VXc5eDBTYXA2ekk0UHBhWktINGQ5a3RrRlUrOTF5QmJONVNSdGRkOC9FUWF0SlhtQk5mdzdrUnJsbCtMandad2VXTEViMGpUMVJwR3VEcUhMZExUTU1nb3dTY3pycDJ6Q3NKZFFIL3NlYVlycHdrQ0ZSOGRSak03SlJVa1ZhMUEwU3ZkWkFYNzV2dUJ2dkxCek4yYytSYnQ1b3F5OTNscUlCSWpLYjd3MlVrUlRiNHBCV2NOS05QVWd0M2F6L1B4UXhFYXNhbHRkbXV5dnNYWWJ1cjNWUE5uOEp6Ti82SjVOK1FDbno1NzFUY2NhcENta1B2RkUrZ2F5WFUrVUYxVktZeUZjVXZsZXY3bHNRbG1qcXpLMlFBOFg3UTNUekZhcHFodkpLbHQ3ZHpCSHJhcVpCZ1BnYjlONG9LTmF5aThGWlQ3MStrYVloSWhGckVTS1lSSGZIa2o1bjNrZmhnTHkreCtqTERiS3hNZ2drZ0huSno5UnVFSFpYQld0a1lBUT09IiwidGhpbmtpbmciOiIiLCJ0eXBlIjoidGhpbmtpbmcifSx7ImlkIjoidG9vbHVfMDFINmZWOEp2UkpOdlh5NDdlUm5kM2pIIiwiY2FsbGVyIjp7InR5cGUiOiJkaXJlY3QifSwiaW5wdXQiOnt9LCJuYW1lIjoidHJhbnNmZXJfdG9fdmVyaWZpZXIiLCJ0eXBlIjoidG9vbF91c2UiLCJ0b29sc2V0X25hbWUiOm51bGx9XSwicmVzcG9uc2VfbWV0YWRhdGEiOnsiaWQiOiJtc2dfMDExQ2Zkd0FNUmRTMjFGZUpmelFoTTNzIiwiY29udGFpbmVyIjpudWxsLCJkaWFnbm9zdGljcyI6bnVsbCwibW9kZWwiOiJjbGF1ZGUtc29ubmV0Iiwic3RvcF9kZXRhaWxzIjpudWxsLCJzdG9wX3JlYXNvbiI6InRvb2xfdXNlIiwic3RvcF9zZXF1ZW5jZSI6bnVsbCwidXNhZ2UiOnsiY2FjaGVfY3JlYXRpb24iOnsiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MH0sImNhY2hlX2NyZWF0aW9uX2lucHV0X3Rva2VucyI6MCwiY2FjaGVfcmVhZF9pbnB1dF90b2tlbnMiOjAsImluZmVyZW5jZV9nZW8iOiJnbG9iYWwiLCJpbnB1dF90b2tlbnMiOjczOTYsIm91dHB1dF90b2tlbnMiOjc3MCwib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6NzM5fSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCIsIm1vZGVsX3Byb3ZpZGVyIjoiYW50aHJvcGljIn0sInR5cGUiOiJhaSIsIm5hbWUiOiJza2VwdGljIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTRhZTktNzZjMC04OWE2LTRmMGIxMTRlMzAzMC0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6InRyYW5zZmVyX3RvX3ZlcmlmaWVyIiwiYXJncyI6e30sImlkIjoidG9vbHVfMDFINmZWOEp2UkpOdlh5NDdlUm5kM2pIIiwidHlwZSI6InRvb2xfY2FsbCJ9XSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjo3Mzk2LCJvdXRwdXRfdG9rZW5zIjo3NzAsInRvdGFsX3Rva2VucyI6ODE2NiwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6NzM5fX0sImludmFsaWRfdG9vbF9jYWxscyI6W119fSx7ImxjIjoxLCJ0eXBlIjoiY29uc3RydWN0b3IiLCJpZCI6WyJsYW5nY2hhaW4iLCJzY2hlbWEiLCJtZXNzYWdlcyIsIlRvb2xNZXNzYWdlIl0sImt3YXJncyI6eyJjb250ZW50IjoiU3VjY2Vzc2Z1bGx5IHRyYW5zZmVycmVkIHRvIHZlcmlmaWVyIiwidHlwZSI6InRvb2wiLCJuYW1lIjoidHJhbnNmZXJfdG9fdmVyaWZpZXIiLCJpZCI6ImMwNjQ1NjNhLTk3NGYtNGYwZi1hYjRhLTA4MGU2M2MwMWNmNSIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxSDZmVjhKdlJKTnZYeTQ3ZVJuZDNqSCIsInN0YXR1cyI6InN1Y2Nlc3MifX0seyJsYyI6MSwidHlwZSI6ImNvbnN0cnVjdG9yIiwiaWQiOlsibGFuZ2NoYWluIiwic2NoZW1hIiwibWVzc2FnZXMiLCJBSU1lc3NhZ2UiXSwia3dhcmdzIjp7ImNvbnRlbnQiOlt7InNpZ25hdHVyZSI6IkNBUVM5Z1VLRUFnU0dBSTRBVUlJZEdocGJtdHBibWNTREdGeUoxTVhvWStBaTZOa2J4b01SZDJUclVmVVU3V3JQcEEySWpEZ3k4ZFVwbGNaS2M3RG5Kd05MVEJmSDVVTlIrbkduYWtTUFFpYThVVkl6cmprT25KMmZMcldVc0Eyb0VPOHlSc3Frd1hra0RDc3loQjk3ZHdqNTdSTXQ5MVh6Z1doTDlsdU5FR085MnlXMHJORkdvTVNVVk94UElyRmM1Ump5cmhLRWMzKzFSbnB4Q2t5bHErTE5sUTVORnhMbjZ2MmNGNUR6cXB2QkhOUjdYQWlnK3VySklVM2dYeWRDa2FHWlpEZXRKdFFPVU9YZ1ppOXo5MTAwTnBkQUVrWERvMklDSENHVEdwVmMwSi9wYlh4V1JCeGI2QjEvR2wwWWpuNjhWNkhsZXNIN0lvQzdHUi8zSHdSdTlOU0RCRzR0dGZRakVXY2crUGVBZWE0Um43anp4K1RtOUg3TDFqb1RLVmdEYlFnNWlLTUxmanZQcWxua3NDTHRYT3Q4alNJUzVPWUxON2EvN1RYNi93NWQzNHZudzh2UERtVTFVWWkwUTcycVVXaFVhMnZ3OE9YTnBjS0w3QnAwbE5mQlM4cnZIWSt2Qlk4L2hhQW9UTDN3dkxGVTFmVCs0ei92QTNCY3NpZ1pEWWxmSmVEWWN4WG1SdXF4enhFR3BYQ1JlOXMxSFd0SUQrNDFwSHU2bHBaSGtYYWdocGM2VFpITHhOd2NWaFRiNmZNUTdRbysyTDZQdmtsaDFtMzJwZW9keGVnMllpUGhkcGwzdy94a09FdTUvSkVGN1pCeHBkVUwrdTN3a081cFVsc3FmZ1g3dWh6djdIQzJXcFNXVmltcndpUXdRT1V4MCtKUHJMdHcrbFlmcTdJM2pydW8wR1cwZ3VXdm51ZjdKTTNZTGo4NHErSmdIb21uU1Nabm5abXJMMFVhUnZyVnlMbzg3N0YzOWNjTFM3WG9abm51aThKNFA1OFpUbWlMa0VKZUdjeDFJdWFnMmUzMzMzOU83WWV3LzU5dExiUXRJR2g5VldNd0hCbkZ4MzlTOC80TEVGcks1SGxkUFFoT3Jsb3lUdGtSczY2aVFSS3R4TUtTMFZJNlFUS1pEQWJzN2E3SDF5QnYrbUppOVhnNHVLbXlVNnFubjd6RUtqalkyYklsQ3dleng0TExHUDVIRDNacjZmWnNtZEcxVUh6RHNCdllHMm9DSTBvcUVLTSt2YWxzVDZoVThNUmpiUUJEL0pKOEpKTi9pQkdsaEJBVUhvMFVtaGh0UjJldVRWcFpXbE1uaG1wWEFkS0RYU2ZQZGFQb2hnQiIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJpZCI6InRvb2x1XzAxWEVBdWhRSENCZHlDd3R5bVp3RmZEaSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX3JlZF90ZWFtIiwidHlwZSI6InRvb2xfdXNlIiwidG9vbHNldF9uYW1lIjpudWxsfV0sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHdBc2FiRHF1ZWdIaTZZVXBDbiIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo3NTA5LCJvdXRwdXRfdG9rZW5zIjo4OCwib3V0cHV0X3Rva2Vuc19kZXRhaWxzIjp7InRoaW5raW5nX3Rva2VucyI6NTV9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InZlcmlmaWVyIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTY2ODItN2QyMS04YzE1LWZmYmNlOTQyMmM5Zi0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6InRyYW5zZmVyX3RvX3JlZF90ZWFtIiwiYXJncyI6e30sImlkIjoidG9vbHVfMDFYRUF1aFFIQ0JkeUN3dHltWndGZkRpIiwidHlwZSI6InRvb2xfY2FsbCJ9XSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjo3NTA5LCJvdXRwdXRfdG9rZW5zIjo4OCwidG90YWxfdG9rZW5zIjo3NTk3LCJpbnB1dF90b2tlbl9kZXRhaWxzIjp7ImNhY2hlX3JlYWQiOjAsImNhY2hlX2NyZWF0aW9uIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowfSwib3V0cHV0X3Rva2VuX2RldGFpbHMiOnsicmVhc29uaW5nIjo1NX19LCJpbnZhbGlkX3Rvb2xfY2FsbHMiOltdfX0seyJsYyI6MSwidHlwZSI6ImNvbnN0cnVjdG9yIiwiaWQiOlsibGFuZ2NoYWluIiwic2NoZW1hIiwibWVzc2FnZXMiLCJUb29sTWVzc2FnZSJdLCJrd2FyZ3MiOnsiY29udGVudCI6IlN1Y2Nlc3NmdWxseSB0cmFuc2ZlcnJlZCB0byByZWRfdGVhbSIsInR5cGUiOiJ0b29sIiwibmFtZSI6InRyYW5zZmVyX3RvX3JlZF90ZWFtIiwiaWQiOiI5ODJjMDEyNi1lNmFiLTQ1OGEtOTIyZS03YjI2NmU4M2MyYjIiLCJ0b29sX2NhbGxfaWQiOiJ0b29sdV8wMVhFQXVoUUhDQmR5Q3d0eW1ad0ZmRGkiLCJzdGF0dXMiOiJzdWNjZXNzIn19LHsibGMiOjEsInR5cGUiOiJjb25zdHJ1Y3RvciIsImlkIjpbImxhbmdjaGFpbiIsInNjaGVtYSIsIm1lc3NhZ2VzIiwiQUlNZXNzYWdlIl0sImt3YXJncyI6eyJjb250ZW50IjpbeyJzaWduYXR1cmUiOiJDQVFTd2djS0VBZ1NHQUk0QVVJSWRHaHBibXRwYm1jU0RPUlhnZTl1TDE4MzJSTVF3Qm9NalF3QXdBSGI5aVAxb2pQR0lqRERTY2FsNDg2YmVDSmJ0RzgvRDJZOWc5UzV4SVBmam9IZy9keHdYNUh5cFNRRUJiakZpVFNiUEVDWFNGelY2WUlxM3diTGNteVBidnVzR25EZXhGZ3grM3NJWHp2cXlGVi82OXhoaWhNblNwZlRDSHkwKzB6U3UxV3BZVS9XVXBJZm9hU1hUY1RhMXFrTTh4OEVrL3kvMWprS01aVWZERmE5M3VoVUp4T0M0K1FnUFdCbC8rWHkydDgyTDJ4L2VicTdRTFFUcXRSMGpYZWFVUlFtM0NvNkp2T2dXbUR2TXBFZFRlVFdFM2ZEaDF0enpWS3NmbHVtNGQ4OWI2Q2FLdG44bmxRZDdSaE45OXZOUU1RSEc1VjRPdVZOZHJIUFpacEZPdWhkZWpzTUZXWHlCKzU1c2xHQWQ4d0VYZ0ZLYkkva25PWFZHekwvTkRGOEd0eTF0TlpsdTJmZFNDQXNZV01zRlFQMWNid0xlUUdzenlYekpNVlpKMkk5eEJQa3l6MUhPWENQeWg1RitqMmdSVFBpa1ZCM0FSQzZ0ZkIvbERTVWVLQlZaUFFTOHBZL0I0ZUNzK0lqamFCTldydUx6ZnFDc2M2eEwwQ3VRUFNZeVFYYVlGallyU1JOY0M3MFdRQldUNVZWaHAwbUE5eVpER1liUlp0OUk1T0s5UWtRbXVKNXpERmFNRU9YelNyc0M0bHRlUDZNZGltaU1panZzSDNpcStjNldVWW9sUmozbmVJODhFYUhJekxBR1JxNjRCTkNMemRBWUwycEVVMnNiNXVHNzhQS1ZvVVVDYlZKTDVqcERPaExPVE8xYlFzVHVvVXBuUlJyTFFEL2tJelYrbGpwVkJ3d2ZKY3lxNzVUSkEvQ3RQci94cjJTWjlCMWIyc2JqRzZqRDBsaHRvQ2RkZURnY3FnbC9MU1NrY3VhWVk4eFBXQ3krTHROaGNxVGhmbWZ2bkhMSklwTDZnamJqVy9PUXBSZlZVd2Z3MWpPYjliV1Z3Y2dIaE5ZZktZR1B0TThvdGg1eHdMWVJSVmd6cU1LT3ZzN2ZhbFhSaFZ6WjJTUDZJdzBCTW1BQVo2SGpmQ2w5TCt0UHFKTmxuUVFPSHBvV2p1bTRCK1hzWWxkRkVLR1FXUXFzZDU5aU5QdmFvOXAvc0NPdDdyR3FDb3pjbWdpRW00dzY2ZXovUUlEeWI0Nm4wK3RjTkpQVWNvQkt2US9qRHdFNlZ3bWtRL25FQ2pGcDNVZERUK2tXWVFTTmQ4ajhWQlpPcEc4TGxxVVlyRVpDdjBxN0JzMXZjTnJkWWlhRm9TMzJvc2F5bldrK0h0M09IdUZXYkVGOEpsc0ZUUUFPMDVWdllrZWNmMFYvb1RIc0c1aHlPT3M3VUVhREZxN3VhSnV6V1ZoaDVwQW9qSWFCY1I4ZlZsbjJQTFJUWDdpczJuc0ErUXJkaWRlUGNadEZ0Z3hBaUtZZTFpMnNTRmk0dm04RU9rQ2JRNjN3ZFFlWTRVcnI1UHVvTUV3eFVVTGpnS2FnaUFtc0pEcTRSclZtN3NPNCs2WXVBOWo2bDJ1eWxrT0VjNjd2Zi81ZmtYdUNzaUFwTkw2a2ViOGMrdkwwbjRGWEdkemorYXBTRGZGR0JnQiIsInRoaW5raW5nIjoiIiwidHlwZSI6InRoaW5raW5nIn0seyJpZCI6InRvb2x1XzAxVkJyV2RXRVdZZ2l0NGtOWWVoQk1VNSIsImNhbGxlciI6eyJ0eXBlIjoiZGlyZWN0In0sImlucHV0Ijp7fSwibmFtZSI6InRyYW5zZmVyX3RvX2VkaXRvciIsInR5cGUiOiJ0b29sX3VzZSIsInRvb2xzZXRfbmFtZSI6bnVsbH1dLCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QXpmanFVb3BvRW1aTGFxbTEiLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoidG9vbF91c2UiLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6NzQxNiwib3V0cHV0X3Rva2VucyI6MTUxLCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjoxMTl9LCJzZXJ2ZXJfdG9vbF91c2UiOm51bGwsInNlcnZpY2VfdGllciI6InN0YW5kYXJkIn0sIm1vZGVsX25hbWUiOiJjbGF1ZGUtc29ubmV0IiwibW9kZWxfcHJvdmlkZXIiOiJhbnRocm9waWMifSwidHlwZSI6ImFpIiwibmFtZSI6InJlZF90ZWFtIiwiaWQiOiJsY19ydW4tLTAxYTBmZTAzLTZjZTYtNzI1MS1iNzk2LTRmNjQzMzNmNjdjNi0wIiwidG9vbF9jYWxscyI6W3sibmFtZSI6InRyYW5zZmVyX3RvX2VkaXRvciIsImFyZ3MiOnt9LCJpZCI6InRvb2x1XzAxVkJyV2RXRVdZZ2l0NGtOWWVoQk1VNSIsInR5cGUiOiJ0b29sX2NhbGwifV0sInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6NzQxNiwib3V0cHV0X3Rva2VucyI6MTUxLCJ0b3RhbF90b2tlbnMiOjc1NjcsImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjExOX19LCJpbnZhbGlkX3Rvb2xfY2FsbHMiOltdfX0seyJsYyI6MSwidHlwZSI6ImNvbnN0cnVjdG9yIiwiaWQiOlsibGFuZ2NoYWluIiwic2NoZW1hIiwibWVzc2FnZXMiLCJUb29sTWVzc2FnZSJdLCJrd2FyZ3MiOnsiY29udGVudCI6IlN1Y2Nlc3NmdWxseSB0cmFuc2ZlcnJlZCB0byBlZGl0b3IiLCJ0eXBlIjoidG9vbCIsIm5hbWUiOiJ0cmFuc2Zlcl90b19lZGl0b3IiLCJpZCI6IjVkMzllMzZiLWM1YjUtNDQyNi1hOWFhLWJlMmY3ZDM4MTZkNiIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxVkJyV2RXRVdZZ2l0NGtOWWVoQk1VNSIsInN0YXR1cyI6InN1Y2Nlc3MifX0seyJsYyI6MSwidHlwZSI6ImNvbnN0cnVjdG9yIiwiaWQiOlsibGFuZ2NoYWluIiwic2NoZW1hIiwibWVzc2FnZXMiLCJBSU1lc3NhZ2UiXSwia3dhcmdzIjp7ImNvbnRlbnQiOlt7InNpZ25hdHVyZSI6IkNBUVM1Z1lLRUFnU0dBSTRBVUlJZEdocGJtdHBibWNTREk0UUliN0wydzVhVzNjTS9Sb01MaTJ3RXhEa3FodXhKRGlZSWpBcUd4TTRXVXdpcTR1WkhyaUNidUM0a1Z3YTdpZ2RVTG9vY1piaTVmZXZLYmZ3cUhTYnJMTlQ1MU5HWDRYc0x0c3Fnd1kvdGFiSGpsbHpxdTduUld2OCtGejRSNm45amE3Q3BqMjRadXhGdUtra1FPQm1BNFRmMWFwazNSUHFjRVErait3ZWR1MGxPK3JkRUN5ZDNsWGhmZ0VSRkZxY002YXVjeGVsNFJidzVvQUhYdTkrWGVUdHpsK044dEFlWjlpR1oyWFR0VTVPYjhhUFQxS3VYK3VwOGtRNFZVWGtCVGZkMmY1SlA5UnhaRXE5d1NZYVpzeUhkdjBuKzFVNDZBT3N2S1ptcDlRWEJ3bzJmNE85MGpQOGVnaXJlbTlFYlpibWFZT0o4V0s3Q3RLUlZiWTJ5NCs5eFJVSjA0bnNkSXo1RjVGRktFeHhOT3JCNG1jL05FODNhYjNaNjRpV3pPd3FvVUdBR1FTa2FIMGQ1Y0V1bVpvNGZaWkFoUHl1SmVRRHh1bE1DZXduN0VHQ25wSncvbnppMHdveUVhUUw0WTdoOTlQN2FYeEZyMjhwNUtDeUpFSlREZWZlSXBWbEF3TUNBanoreEM2OUFBTG13eVRRK2NGRU0rS0FnZUlTcGhMVnk4QVByMkZTaGF4Qk5yWFFnejFzOXMyZGhMWlFPQml3emh4bUVnZEs3KzhKYXFoQjBlRUVkLzFSVkF1SVlkZXpBNUhCcndFOUR5UWlDbDNVT3NiU25ORVZlK2dySHA1cDN0bU1CcTY4Z0xWODJvQXpGWVpsK0Nld3g3MXY1ZFBGT3ZLUEVIVXpwUVlPVkxybkIxRVpSWXJaejcybjJ3ZlNWeEJPenBUY25CeFY2SGVkQ1B5SXpLWFpNOSt5RWVUSmk4UXR2VmdaMXcrdjBobUNGR3BubVpON21YbGdxTDFzNk1YVnlkcFBPS1BsTTNOYTdDZEhuUEZZaXdyMnpMdmtrY0lNN0Nzay9DMytaTUFXVEsrQzlCMXZiR3hQbHRFM092OGtPOThQZ1NYbTFBSnVFYlh5K3IzYTNEM1FsWkozZEVLZTJMTmRBQnlyUE1oU2NLVThHbGZILzlOdGdyaHRha2dsbEQvTWZBbFRzYk53TkRyVXRXUnhtRHVKLzBaK2hray9ZTndFRFlBWmE4VG13Vm9XT2RCSHBSUEh2WjlFOXVzZXVsdG1hK0prMWdSTER0dkVaa3FYVUVTa01jcVV0VndZTTVhakpBNU41MmNOaVoxNUJUZjhYakdoS1dkT1Y0MCs4SDBQT1N4TTJKaWZ3Q08wQzV4Rlc1Qm8zSWlJTnYwVU90WTVmOGt2UGRNM1Bqa2hhT3JLMXlUQThlRUU0NHZ1bkw5UGtvR2VjYlltK1hFNUt6TDNRc0VuMlkxSUp3Tzl3MHhIWFFaRWVpUDUrMHUzWW5EYzhxOWRCTGhHVzNGeTMwZ1lBUT09IiwidGhpbmtpbmciOiIiLCJ0eXBlIjoidGhpbmtpbmcifSx7ImlkIjoidG9vbHVfMDFCbThBVTlFc1FVd3FvVkxZcHhLQTc3IiwiY2FsbGVyIjp7InR5cGUiOiJkaXJlY3QifSwiaW5wdXQiOnsiZmlsZV9wYXRoIjoiL2Fuc3dlci5tZCIsImNvbnRlbnQiOiIqKlR1cGxlIHZzLiBsaXN0IGltbXV0YWJpbGl0eSAoUHl0aG9uKSoqXG5cbkEgdHVwbGUgaXMgaW1tdXRhYmxlOiBvbmNlIGNyZWF0ZWQsIHlvdSBjYW4ndCBhc3NpZ24gdG8gaXRzIGl0ZW1zLCBhbmQgdGhlIGNvbGxlY3Rpb24gb2Ygb2JqZWN0cyBpdCByZWZlcmVuY2VzIGNhbid0IGNoYW5nZS4gYHRbMF0gPSAxYCByYWlzZXMgYFR5cGVFcnJvcmAuIEEgbGlzdCBpcyBtdXRhYmxlOiB5b3UgY2FuIGNoYW5nZSBpdCBpbiBwbGFjZSB3aXRoIGluZGV4IG9yIHNsaWNlIGFzc2lnbm1lbnQsIGBhcHBlbmQoKWAsIGBleHRlbmQoKWAsIGBzb3J0KClgIGFuZCBzaW1pbGFyIG1ldGhvZHMuXG5cblR1cGxlIGltbXV0YWJpbGl0eSBpcyBzaGFsbG93LiBJZiBhIHR1cGxlIGhvbGRzIGEgbXV0YWJsZSBvYmplY3QsIHN1Y2ggYXMgYSBsaXN0LCB0aGF0IGlubmVyIG9iamVjdCBjYW4gc3RpbGwgY2hhbmdlLCBhbmQgc28gdGhlIHR1cGxlJ3MgdmFsdWUgY2hhbmdlcy4gQmVjYXVzZSBvZiB0aGlzLCBhIHR1cGxlIGNhbiBiZSBhIGBkaWN0YCBrZXkgb3IgYHNldGAgbWVtYmVyIG9ubHkgaWYgZXZlcnl0aGluZyBpbnNpZGUgaXQgaXMgaGFzaGFibGUuIEEgbGlzdCBjYW4gbmV2ZXIgYmUgYSBrZXkuXG5cblNvdXJjZXM6XG4tIGh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFxuLSBodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3JlZmVyZW5jZS9kYXRhbW9kZWwuaHRtbFxuLSBodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL2xpYnJhcnkvc3RkdHlwZXMuaHRtbFxuIn0sIm5hbWUiOiJ3cml0ZV9maWxlIiwidHlwZSI6InRvb2xfdXNlIiwidG9vbHNldF9uYW1lIjpudWxsfV0sInJlc3BvbnNlX21ldGFkYXRhIjp7ImlkIjoibXNnXzAxMUNmZHdCQmFhOWdqTUNjZTE2VFVlbiIsImNvbnRhaW5lciI6bnVsbCwiZGlhZ25vc3RpY3MiOm51bGwsIm1vZGVsIjoiY2xhdWRlLXNvbm5ldCIsInN0b3BfZGV0YWlscyI6bnVsbCwic3RvcF9yZWFzb24iOiJ0b29sX3VzZSIsInN0b3Bfc2VxdWVuY2UiOm51bGwsInVzYWdlIjp7ImNhY2hlX2NyZWF0aW9uIjp7ImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjB9LCJjYWNoZV9jcmVhdGlvbl9pbnB1dF90b2tlbnMiOjAsImNhY2hlX3JlYWRfaW5wdXRfdG9rZW5zIjowLCJpbmZlcmVuY2VfZ2VvIjoiZ2xvYmFsIiwiaW5wdXRfdG9rZW5zIjo4MTI3LCJvdXRwdXRfdG9rZW5zIjo0MzMsIm91dHB1dF90b2tlbnNfZGV0YWlscyI6eyJ0aGlua2luZ190b2tlbnMiOjc5fSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCIsIm1vZGVsX3Byb3ZpZGVyIjoiYW50aHJvcGljIn0sInR5cGUiOiJhaSIsIm5hbWUiOiJlZGl0b3IiLCJpZCI6ImxjX3J1bi0tMDFhMGZlMDMtNzZlOS03Y2EzLWE1ZmYtNjNkZTY2ZGQ4ZGQxLTAiLCJ0b29sX2NhbGxzIjpbeyJuYW1lIjoid3JpdGVfZmlsZSIsImFyZ3MiOnsiZmlsZV9wYXRoIjoiL2Fuc3dlci5tZCIsImNvbnRlbnQiOiIqKlR1cGxlIHZzLiBsaXN0IGltbXV0YWJpbGl0eSAoUHl0aG9uKSoqXG5cbkEgdHVwbGUgaXMgaW1tdXRhYmxlOiBvbmNlIGNyZWF0ZWQsIHlvdSBjYW4ndCBhc3NpZ24gdG8gaXRzIGl0ZW1zLCBhbmQgdGhlIGNvbGxlY3Rpb24gb2Ygb2JqZWN0cyBpdCByZWZlcmVuY2VzIGNhbid0IGNoYW5nZS4gYHRbMF0gPSAxYCByYWlzZXMgYFR5cGVFcnJvcmAuIEEgbGlzdCBpcyBtdXRhYmxlOiB5b3UgY2FuIGNoYW5nZSBpdCBpbiBwbGFjZSB3aXRoIGluZGV4IG9yIHNsaWNlIGFzc2lnbm1lbnQsIGBhcHBlbmQoKWAsIGBleHRlbmQoKWAsIGBzb3J0KClgIGFuZCBzaW1pbGFyIG1ldGhvZHMuXG5cblR1cGxlIGltbXV0YWJpbGl0eSBpcyBzaGFsbG93LiBJZiBhIHR1cGxlIGhvbGRzIGEgbXV0YWJsZSBvYmplY3QsIHN1Y2ggYXMgYSBsaXN0LCB0aGF0IGlubmVyIG9iamVjdCBjYW4gc3RpbGwgY2hhbmdlLCBhbmQgc28gdGhlIHR1cGxlJ3MgdmFsdWUgY2hhbmdlcy4gQmVjYXVzZSBvZiB0aGlzLCBhIHR1cGxlIGNhbiBiZSBhIGBkaWN0YCBrZXkgb3IgYHNldGAgbWVtYmVyIG9ubHkgaWYgZXZlcnl0aGluZyBpbnNpZGUgaXQgaXMgaGFzaGFibGUuIEEgbGlzdCBjYW4gbmV2ZXIgYmUgYSBrZXkuXG5cblNvdXJjZXM6XG4tIGh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvdHV0b3JpYWwvZGF0YXN0cnVjdHVyZXMuaHRtbFxuLSBodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3JlZmVyZW5jZS9kYXRhbW9kZWwuaHRtbFxuLSBodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL2xpYnJhcnkvc3RkdHlwZXMuaHRtbFxuIn0sImlkIjoidG9vbHVfMDFCbThBVTlFc1FVd3FvVkxZcHhLQTc3IiwidHlwZSI6InRvb2xfY2FsbCJ9XSwidXNhZ2VfbWV0YWRhdGEiOnsiaW5wdXRfdG9rZW5zIjo4MTI3LCJvdXRwdXRfdG9rZW5zIjo0MzMsInRvdGFsX3Rva2VucyI6ODU2MCwiaW5wdXRfdG9rZW5fZGV0YWlscyI6eyJjYWNoZV9yZWFkIjowLCJjYWNoZV9jcmVhdGlvbiI6MCwiZXBoZW1lcmFsXzVtX2lucHV0X3Rva2VucyI6MCwiZXBoZW1lcmFsXzFoX2lucHV0X3Rva2VucyI6MH0sIm91dHB1dF90b2tlbl9kZXRhaWxzIjp7InJlYXNvbmluZyI6Nzl9fSwiaW52YWxpZF90b29sX2NhbGxzIjpbXX19LHsibGMiOjEsInR5cGUiOiJjb25zdHJ1Y3RvciIsImlkIjpbImxhbmdjaGFpbiIsInNjaGVtYSIsIm1lc3NhZ2VzIiwiVG9vbE1lc3NhZ2UiXSwia3dhcmdzIjp7ImNvbnRlbnQiOiJVcGRhdGVkIGZpbGUgL2Fuc3dlci5tZCIsInR5cGUiOiJ0b29sIiwibmFtZSI6IndyaXRlX2ZpbGUiLCJpZCI6ImUzNDkyZTA4LWM4NjktNDdlYy1hNWY3LWNjNTdlYTVmNjQwMyIsInRvb2xfY2FsbF9pZCI6InRvb2x1XzAxQm04QVU5RXNRVXdxb1ZMWXB4S0E3NyIsInN0YXR1cyI6InN1Y2Nlc3MifX1dXX0=" + } + }, + { + "key": "gen_ai.usage.input_tokens", + "value": { + "intValue": "8494" + } + }, + { + "key": "gen_ai.usage.output_tokens", + "value": { + "intValue": "415" + } + }, + { + "key": "gen_ai.usage.total_tokens", + "value": { + "intValue": "8909" + } + }, + { + "key": "gen_ai.completion", + "value": { + "bytesValue": "eyJnZW5lcmF0aW9ucyI6W1t7InRleHQiOiJJIHdyb3RlIHRoZSBhbnN3ZXIgdG8gYC9hbnN3ZXIubWRgLiBJdCBpcyBhYm91dCAxMDAgd29yZHMsIHVuZGVyIHRoZSAxMjAtd29yZCBsaW1pdC5cblxuSSBkaWQgbm90IGdldCBhIHNrZXB0aWMgb3IgdmVyaWZpZXIgY2hlY2suIEkgaGFuZGVkIG9mZiB0byBib3RoLCBidXQgbmVpdGhlciByZXR1cm5lZCBhbnkgZmVlZGJhY2suIEkgYWxzbyB0cmllZCB0byBoYW5kIG9mZiB0byBhIGByZWRfdGVhbWAgYWdlbnQsIGFuZCB0aGF0IHRyYW5zZmVyIGlzbid0IG9uZSBvZiB0aGUgdG9vbHMgSSB3YXMgZ2l2ZW4uIEkgdGhlbiB3cm90ZSB0aGUgYW5zd2VyIG15c2VsZiBmcm9tIHRoZSBvZmZpY2lhbCBQeXRob24gZG9jdW1lbnRhdGlvbiBzZWFyY2ggcmVzdWx0cy4gSSBkaWRuJ3Qgb3BlbiB0aGUgcGFnZXMgZGlyZWN0bHkuXG5cbioqVHVwbGUgdnMuIGxpc3QgaW1tdXRhYmlsaXR5IChQeXRob24pKipcblxuQSB0dXBsZSBpcyBpbW11dGFibGU6IG9uY2UgY3JlYXRlZCwgeW91IGNhbid0IGFzc2lnbiB0byBpdHMgaXRlbXMsIGFuZCB0aGUgY29sbGVjdGlvbiBvZiBvYmplY3RzIGl0IHJlZmVyZW5jZXMgY2FuJ3QgY2hhbmdlLiBgdFswXSA9IDFgIHJhaXNlcyBgVHlwZUVycm9yYC4gQSBsaXN0IGlzIG11dGFibGU6IHlvdSBjYW4gY2hhbmdlIGl0IGluIHBsYWNlIHdpdGggaW5kZXggb3Igc2xpY2UgYXNzaWdubWVudCwgYGFwcGVuZCgpYCwgYGV4dGVuZCgpYCwgYHNvcnQoKWAgYW5kIHNpbWlsYXIgbWV0aG9kcy5cblxuVHVwbGUgaW1tdXRhYmlsaXR5IGlzIHNoYWxsb3cuIElmIGEgdHVwbGUgaG9sZHMgYSBtdXRhYmxlIG9iamVjdCwgc3VjaCBhcyBhIGxpc3QsIHRoYXQgaW5uZXIgb2JqZWN0IGNhbiBzdGlsbCBjaGFuZ2UsIGFuZCBzbyB0aGUgdHVwbGUncyB2YWx1ZSBjaGFuZ2VzLiBCZWNhdXNlIG9mIHRoaXMsIGEgdHVwbGUgY2FuIGJlIGEgYGRpY3RgIGtleSBvciBgc2V0YCBtZW1iZXIgb25seSBpZiBldmVyeXRoaW5nIGluc2lkZSBpdCBpcyBoYXNoYWJsZS4gQSBsaXN0IGNhbiBuZXZlciBiZSBhIGtleS5cblxuU291cmNlczpcbi0gaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy90dXRvcmlhbC9kYXRhc3RydWN0dXJlcy5odG1sXG4tIGh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvcmVmZXJlbmNlL2RhdGFtb2RlbC5odG1sXG4tIGh0dHBzOi8vZG9jcy5weXRob24ub3JnLzMvbGlicmFyeS9zdGR0eXBlcy5odG1sIiwiZ2VuZXJhdGlvbl9pbmZvIjpudWxsLCJ0eXBlIjoiQ2hhdEdlbmVyYXRpb24iLCJtZXNzYWdlIjp7ImxjIjoxLCJ0eXBlIjoiY29uc3RydWN0b3IiLCJpZCI6WyJsYW5nY2hhaW4iLCJzY2hlbWEiLCJtZXNzYWdlcyIsIkFJTWVzc2FnZSJdLCJrd2FyZ3MiOnsiY29udGVudCI6Ikkgd3JvdGUgdGhlIGFuc3dlciB0byBgL2Fuc3dlci5tZGAuIEl0IGlzIGFib3V0IDEwMCB3b3JkcywgdW5kZXIgdGhlIDEyMC13b3JkIGxpbWl0LlxuXG5JIGRpZCBub3QgZ2V0IGEgc2tlcHRpYyBvciB2ZXJpZmllciBjaGVjay4gSSBoYW5kZWQgb2ZmIHRvIGJvdGgsIGJ1dCBuZWl0aGVyIHJldHVybmVkIGFueSBmZWVkYmFjay4gSSBhbHNvIHRyaWVkIHRvIGhhbmQgb2ZmIHRvIGEgYHJlZF90ZWFtYCBhZ2VudCwgYW5kIHRoYXQgdHJhbnNmZXIgaXNuJ3Qgb25lIG9mIHRoZSB0b29scyBJIHdhcyBnaXZlbi4gSSB0aGVuIHdyb3RlIHRoZSBhbnN3ZXIgbXlzZWxmIGZyb20gdGhlIG9mZmljaWFsIFB5dGhvbiBkb2N1bWVudGF0aW9uIHNlYXJjaCByZXN1bHRzLiBJIGRpZG4ndCBvcGVuIHRoZSBwYWdlcyBkaXJlY3RseS5cblxuKipUdXBsZSB2cy4gbGlzdCBpbW11dGFiaWxpdHkgKFB5dGhvbikqKlxuXG5BIHR1cGxlIGlzIGltbXV0YWJsZTogb25jZSBjcmVhdGVkLCB5b3UgY2FuJ3QgYXNzaWduIHRvIGl0cyBpdGVtcywgYW5kIHRoZSBjb2xsZWN0aW9uIG9mIG9iamVjdHMgaXQgcmVmZXJlbmNlcyBjYW4ndCBjaGFuZ2UuIGB0WzBdID0gMWAgcmFpc2VzIGBUeXBlRXJyb3JgLiBBIGxpc3QgaXMgbXV0YWJsZTogeW91IGNhbiBjaGFuZ2UgaXQgaW4gcGxhY2Ugd2l0aCBpbmRleCBvciBzbGljZSBhc3NpZ25tZW50LCBgYXBwZW5kKClgLCBgZXh0ZW5kKClgLCBgc29ydCgpYCBhbmQgc2ltaWxhciBtZXRob2RzLlxuXG5UdXBsZSBpbW11dGFiaWxpdHkgaXMgc2hhbGxvdy4gSWYgYSB0dXBsZSBob2xkcyBhIG11dGFibGUgb2JqZWN0LCBzdWNoIGFzIGEgbGlzdCwgdGhhdCBpbm5lciBvYmplY3QgY2FuIHN0aWxsIGNoYW5nZSwgYW5kIHNvIHRoZSB0dXBsZSdzIHZhbHVlIGNoYW5nZXMuIEJlY2F1c2Ugb2YgdGhpcywgYSB0dXBsZSBjYW4gYmUgYSBgZGljdGAga2V5IG9yIGBzZXRgIG1lbWJlciBvbmx5IGlmIGV2ZXJ5dGhpbmcgaW5zaWRlIGl0IGlzIGhhc2hhYmxlLiBBIGxpc3QgY2FuIG5ldmVyIGJlIGEga2V5LlxuXG5Tb3VyY2VzOlxuLSBodHRwczovL2RvY3MucHl0aG9uLm9yZy8zL3R1dG9yaWFsL2RhdGFzdHJ1Y3R1cmVzLmh0bWxcbi0gaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9yZWZlcmVuY2UvZGF0YW1vZGVsLmh0bWxcbi0gaHR0cHM6Ly9kb2NzLnB5dGhvbi5vcmcvMy9saWJyYXJ5L3N0ZHR5cGVzLmh0bWwiLCJyZXNwb25zZV9tZXRhZGF0YSI6eyJpZCI6Im1zZ18wMTFDZmR3QlVzTmhyMTlnUU5WY0ZzVWUiLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoiZW5kX3R1cm4iLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6ODQ5NCwib3V0cHV0X3Rva2VucyI6NDE1LCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjowfSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCIsIm1vZGVsX3Byb3ZpZGVyIjoiYW50aHJvcGljIn0sInR5cGUiOiJhaSIsImlkIjoibGNfcnVuLS0wMWEwZmUwMy04NmJiLTc2NjEtYTU5Zi1hMzlkNmI0ZTZiNzQtMCIsInVzYWdlX21ldGFkYXRhIjp7ImlucHV0X3Rva2VucyI6ODQ5NCwib3V0cHV0X3Rva2VucyI6NDE1LCJ0b3RhbF90b2tlbnMiOjg5MDksImlucHV0X3Rva2VuX2RldGFpbHMiOnsiY2FjaGVfcmVhZCI6MCwiY2FjaGVfY3JlYXRpb24iOjAsImVwaGVtZXJhbF81bV9pbnB1dF90b2tlbnMiOjAsImVwaGVtZXJhbF8xaF9pbnB1dF90b2tlbnMiOjB9LCJvdXRwdXRfdG9rZW5fZGV0YWlscyI6eyJyZWFzb25pbmciOjB9fSwidG9vbF9jYWxscyI6W10sImludmFsaWRfdG9vbF9jYWxscyI6W119fX1dXSwibGxtX291dHB1dCI6eyJpZCI6Im1zZ18wMTFDZmR3QlVzTmhyMTlnUU5WY0ZzVWUiLCJjb250YWluZXIiOm51bGwsImRpYWdub3N0aWNzIjpudWxsLCJtb2RlbCI6ImNsYXVkZS1zb25uZXQiLCJzdG9wX2RldGFpbHMiOm51bGwsInN0b3BfcmVhc29uIjoiZW5kX3R1cm4iLCJzdG9wX3NlcXVlbmNlIjpudWxsLCJ1c2FnZSI6eyJjYWNoZV9jcmVhdGlvbiI6eyJlcGhlbWVyYWxfMWhfaW5wdXRfdG9rZW5zIjowLCJlcGhlbWVyYWxfNW1faW5wdXRfdG9rZW5zIjowfSwiY2FjaGVfY3JlYXRpb25faW5wdXRfdG9rZW5zIjowLCJjYWNoZV9yZWFkX2lucHV0X3Rva2VucyI6MCwiaW5mZXJlbmNlX2dlbyI6Imdsb2JhbCIsImlucHV0X3Rva2VucyI6ODQ5NCwib3V0cHV0X3Rva2VucyI6NDE1LCJvdXRwdXRfdG9rZW5zX2RldGFpbHMiOnsidGhpbmtpbmdfdG9rZW5zIjowfSwic2VydmVyX3Rvb2xfdXNlIjpudWxsLCJzZXJ2aWNlX3RpZXIiOiJzdGFuZGFyZCJ9LCJtb2RlbF9uYW1lIjoiY2xhdWRlLXNvbm5ldCJ9LCJydW4iOm51bGwsInR5cGUiOiJMTE1SZXN1bHQifQ==" + } + } + ], + "status": { + "code": 1 + }, + "flags": 256 + } + ] + }, + { + "scope": { + "name": "deeplite" + }, + "spans": [ + { + "traceId": "b2952a96b863250c304cef3376fe4c82", + "spanId": "e8da3a18a9815276", + "name": "deeplite.run", + "kind": 1, + "startTimeUnixNano": "1790968013175004000", + "endTimeUnixNano": "1790968042976537000", + "attributes": [ + { + "key": "langsmith.span.kind", + "value": { + "stringValue": "chain" + } + }, + { + "key": "input.value", + "value": { + "stringValue": "For a synthetic tracing fixture, explain Python tuple versus list immutability in at most 120 words. Use only official Python documentation if you search. Have the skeptic and verifier check it, then have the editor write /answer.md. Keep the run short and do not ask follow-up questions." + } + }, + { + "key": "output.value", + "value": { + "stringValue": "I wrote the answer to `/answer.md`. It is about 100 words, under the 120-word limit.\n\nI did not get a skeptic or verifier check. I handed off to both, but neither returned any feedback. I also tried to hand off to a `red_team` agent, and that transfer isn't one of the tools I was given. I then wrote the answer myself from the official Python documentation search results. I didn't open the pages directly.\n\n**Tuple vs. list immutability (Python)**\n\nA tuple is immutable: once created, you can't assign to its items, and the collection of objects it references can't change. `t[0] = 1` raises `TypeError`. A list is mutable: you can change it in place with index or slice assignment, `append()`, `extend()`, `sort()` and similar methods.\n\nTuple immutability is shallow. If a tuple holds a mutable object, such as a list, that inner object can still change, and so the tuple's value changes. Because of this, a tuple can be a `dict` key or `set` member only if everything inside it is hashable. A list can never be a key.\n\nSources:\n- https://docs.python.org/3/tutorial/datastructures.html\n- https://docs.python.org/3/reference/datamodel.html\n- https://docs.python.org/3/library/stdtypes.html" + } + } + ], + "status": {}, + "flags": 256 + } + ] + } + ] + } + ] +} diff --git a/litellm-rust/crates/traces/tests/fixtures/query_alternate.json b/litellm-rust/crates/traces/tests/fixtures/query_alternate.json new file mode 100644 index 00000000000..5445613ea79 --- /dev/null +++ b/litellm-rust/crates/traces/tests/fixtures/query_alternate.json @@ -0,0 +1,39 @@ +{ + "resourceSpans": [ + { + "resource": { + "attributes": [ + { + "key": "service.name", + "value": { + "stringValue": "fixture" + } + } + ] + }, + "scopeSpans": [ + { + "scope": { + "name": "fixture" + }, + "spans": [ + { + "traceId": "01010101010101010101010101010101", + "spanId": "0404040404040404", + "parentSpanId": "", + "name": "alternate", + "kind": 1, + "startTimeUnixNano": "1735689601000000000", + "endTimeUnixNano": "1735689602000000000", + "attributes": [], + "status": { + "code": 1, + "message": "" + } + } + ] + } + ] + } + ] +} diff --git a/litellm-rust/crates/traces/tests/fixtures/query_children.json b/litellm-rust/crates/traces/tests/fixtures/query_children.json new file mode 100644 index 00000000000..66e9d975003 --- /dev/null +++ b/litellm-rust/crates/traces/tests/fixtures/query_children.json @@ -0,0 +1,85 @@ +{ + "resourceSpans": [ + { + "resource": { + "attributes": [ + { + "key": "service.name", + "value": { + "stringValue": "fixture" + } + } + ] + }, + "scopeSpans": [ + { + "scope": { + "name": "fixture" + }, + "spans": [ + { + "traceId": "01010101010101010101010101010101", + "spanId": "0202020202020202", + "parentSpanId": "0101010101010101", + "name": "completion", + "kind": 1, + "startTimeUnixNano": "1735689600100000000", + "endTimeUnixNano": "1735689600600000000", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "chat" + } + }, + { + "key": "gen_ai.response.id", + "value": { + "stringValue": "response-shared" + } + }, + { + "key": "gen_ai.usage.input_tokens", + "value": { + "stringValue": "12" + } + }, + { + "key": "gen_ai.usage.output_tokens", + "value": { + "stringValue": "6" + } + } + ], + "status": { + "code": 1, + "message": "" + } + }, + { + "traceId": "01010101010101010101010101010101", + "spanId": "0303030303030303", + "parentSpanId": "0101010101010101", + "name": "lookup", + "kind": 1, + "startTimeUnixNano": "1735689600700000000", + "endTimeUnixNano": "1735689600800000000", + "attributes": [ + { + "key": "gen_ai.operation.name", + "value": { + "stringValue": "execute_tool" + } + } + ], + "status": { + "code": 2, + "message": "lookup timed out" + } + } + ] + } + ] + } + ] +} diff --git a/litellm-rust/crates/traces/tests/fixtures/query_other_team.json b/litellm-rust/crates/traces/tests/fixtures/query_other_team.json new file mode 100644 index 00000000000..c3dff4b969b --- /dev/null +++ b/litellm-rust/crates/traces/tests/fixtures/query_other_team.json @@ -0,0 +1,46 @@ +{ + "resourceSpans": [ + { + "resource": { + "attributes": [ + { + "key": "service.name", + "value": { + "stringValue": "fixture" + } + } + ] + }, + "scopeSpans": [ + { + "scope": { + "name": "fixture" + }, + "spans": [ + { + "traceId": "01010101010101010101010101010101", + "spanId": "0505050505050505", + "parentSpanId": "", + "name": "other-team", + "kind": 1, + "startTimeUnixNano": "1735689602000000000", + "endTimeUnixNano": "1735689603000000000", + "attributes": [ + { + "key": "gen_ai.response.id", + "value": { + "stringValue": "response-shared" + } + } + ], + "status": { + "code": 1, + "message": "" + } + } + ] + } + ] + } + ] +} diff --git a/litellm-rust/crates/traces/tests/fixtures/query_root.json b/litellm-rust/crates/traces/tests/fixtures/query_root.json new file mode 100644 index 00000000000..83b7b7c77e1 --- /dev/null +++ b/litellm-rust/crates/traces/tests/fixtures/query_root.json @@ -0,0 +1,46 @@ +{ + "resourceSpans": [ + { + "resource": { + "attributes": [ + { + "key": "service.name", + "value": { + "stringValue": "fixture" + } + } + ] + }, + "scopeSpans": [ + { + "scope": { + "name": "fixture" + }, + "spans": [ + { + "traceId": "01010101010101010101010101010101", + "spanId": "0101010101010101", + "parentSpanId": "", + "name": "review", + "kind": 1, + "startTimeUnixNano": "1735689600000000000", + "endTimeUnixNano": "1735689602000000000", + "attributes": [ + { + "key": "gen_ai.input.messages", + "value": { + "stringValue": "Review the change" + } + } + ], + "status": { + "code": 1, + "message": "" + } + } + ] + } + ] + } + ] +} diff --git a/litellm-rust/crates/traces/tests/otlp.rs b/litellm-rust/crates/traces/tests/otlp.rs index 3baa820d312..e5705354533 100644 --- a/litellm-rust/crates/traces/tests/otlp.rs +++ b/litellm-rust/crates/traces/tests/otlp.rs @@ -1,11 +1,35 @@ use litellm_traces::decode_otlp; use litellm_traces::{ObservationType, Shared}; +use opentelemetry_proto::tonic::trace::v1::Span; use rstest::rstest; const FIXTURE: &[u8] = include_bytes!( "../../../../tests/test_litellm/tracing/fixtures/langsmith_deep_agent_export.json" ); +#[rstest] +#[case::root(include_bytes!("fixtures/query_root.json"), ObservationType::Agent, 0, 0)] +#[case::children(include_bytes!("fixtures/query_children.json"), ObservationType::Llm, 12, 6)] +#[case::alternate(include_bytes!("fixtures/query_alternate.json"), ObservationType::Agent, 0, 0)] +#[case::other_team(include_bytes!("fixtures/query_other_team.json"), ObservationType::Agent, 0, 0)] +fn query_fixtures_decode_and_normalize( + #[case] body: &[u8], + #[case] observation_type: ObservationType, + #[case] input_tokens: u32, + #[case] output_tokens: u32, +) { + let spans = decode_otlp(body, Some("application/json")).unwrap(); + let first = &spans[0]; + assert_eq!(first.normalized.observation_type, observation_type); + assert_eq!(first.normalized.input_tokens, input_tokens); + assert_eq!(first.normalized.output_tokens, output_tokens); + assert!( + spans + .iter() + .all(|span| span.resource_attributes["service.name"] == "fixture") + ); +} + #[rstest] #[case::json(FIXTURE, Some("application/json"))] fn decodes_neutral_spans(#[case] body: &[u8], #[case] content_type: Option<&str>) { @@ -133,7 +157,7 @@ fn rejects_ids_and_timestamps_that_cannot_be_stored( }; assert!(matches!( decode_otlp(&request_with(span).encode_to_vec(), None), - Err(litellm_traces::DecodeError::InvalidPayload) + Err(litellm_traces::Error::InvalidPayload) )); } @@ -216,7 +240,7 @@ fn nested_values_are_serialized_once(span: opentelemetry_proto::tonic::trace::v1 fn rejects_json_structure_before_building_a_tree(#[case] body: Vec) { assert!(matches!( decode_otlp(&body, Some("application/json")), - Err(litellm_traces::DecodeError::TooLarge) + Err(litellm_traces::Error::TooLarge) )); } @@ -252,7 +276,7 @@ fn protobuf_preflight_rejects_expansion_before_prost_allocates( let body = request.encode_to_vec(); assert!(matches!( decode_otlp(&body, None), - Err(litellm_traces::DecodeError::TooLarge) + Err(litellm_traces::Error::TooLarge) )); } @@ -310,7 +334,7 @@ fn unique_attribute_expansion_still_respects_decoded_budget( assert!(body.len() < 16 * 1024 * 1024); assert!(matches!( decode_otlp(&body, None), - Err(litellm_traces::DecodeError::TooLarge) + Err(litellm_traces::Error::TooLarge) )); } @@ -338,7 +362,7 @@ fn escaped_attribute_expansion_is_bounded_below_four_mib( assert!(body.len() < 4 * 1024 * 1024); assert!(matches!( decode_otlp(&body, None), - Err(litellm_traces::DecodeError::TooLarge) + Err(litellm_traces::Error::TooLarge) )); } @@ -386,3 +410,496 @@ fn normalizes_langsmith_fixture() { assert_eq!(tool.normalized.observation_type, ObservationType::Tool); assert!(tool.normalized.output.starts_with("Based on my research")); } + +fn decode_normalization( + span: Span, + scope: &str, + attributes: &[(&str, &str)], +) -> Result { + use opentelemetry_proto::tonic::{ + collector::trace::v1::ExportTraceServiceRequest, + common::v1::{AnyValue, InstrumentationScope, KeyValue, any_value::Value}, + trace::v1::{ResourceSpans, ScopeSpans}, + }; + use prost::Message; + + let request = ExportTraceServiceRequest { + resource_spans: vec![ResourceSpans { + scope_spans: vec![ScopeSpans { + scope: Some(InstrumentationScope { + name: scope.to_owned(), + ..Default::default() + }), + spans: vec![Span { + attributes: attributes + .iter() + .map(|(key, value)| KeyValue { + key: (*key).to_owned(), + value: Some(AnyValue { + value: Some(Value::StringValue((*value).to_owned())), + }), + ..Default::default() + }) + .collect(), + ..span + }], + ..Default::default() + }], + ..Default::default() + }], + }; + decode_otlp(&request.encode_to_vec(), None) + .map(|spans| spans.into_iter().next().expect("one synthetic span")) +} + +#[rstest] +#[case::agent("invoke_agent", false, ObservationType::Agent)] +#[case::chat("chat", false, ObservationType::Llm)] +#[case::completion("text_completion", false, ObservationType::Llm)] +#[case::content("generate_content", false, ObservationType::Llm)] +#[case::tool("execute_tool", false, ObservationType::Tool)] +#[case::unknown_root("unknown", true, ObservationType::Agent)] +#[case::unknown_child("unknown", false, ObservationType::Chain)] +#[case::missing_root("", true, ObservationType::Agent)] +#[case::missing_child("", false, ObservationType::Chain)] +fn genai_operations_and_parentage_classify_spans( + span: Span, + #[case] operation: &str, + #[case] root: bool, + #[case] expected: ObservationType, +) { + let decoded = decode_normalization( + Span { + parent_span_id: if root { vec![] } else { vec![3; 8] }, + ..span + }, + "", + &[("gen_ai.operation.name", operation)], + ) + .unwrap(); + assert_eq!(decoded.normalized.observation_type, expected); +} + +#[rstest] +#[case::primary("request-model", "messages-in", "messages-out", ["request-model", "messages-in", "messages-out"])] +#[case::fallback("", "", "", ["response-model", "tool-in", "tool-out"])] +#[case::independent_fallback("request-model", "", "messages-out", ["request-model", "tool-in", "messages-out"])] +fn genai_fields_and_consumed_attributes_follow_the_same_fallback( + span: Span, + #[case] model: &str, + #[case] input: &str, + #[case] output: &str, + #[case] expected: [&str; 3], +) { + let decoded = decode_normalization( + span, + "", + &[ + ("gen_ai.request.model", model), + ("gen_ai.response.model", "response-model"), + ("gen_ai.input.messages", input), + ("gen_ai.output.messages", output), + ("gen_ai.tool.call.arguments", "tool-in"), + ("gen_ai.tool.call.result", "tool-out"), + ("gen_ai.agent.name", "test-agent"), + ("gen_ai.response.id", "response-1"), + ], + ) + .unwrap(); + let fields = &decoded.normalized; + assert_eq!( + [ + fields.model.as_str(), + fields.input.as_str(), + fields.output.as_str() + ], + expected + ); + assert_eq!(fields.agent_name, "test-agent"); + assert_eq!(fields.litellm_request_id, "response-1"); + assert_eq!( + fields.input, + decoded.attributes[decoded.consumed_attributes[0]] + ); + assert_eq!( + fields.output, + decoded.attributes[decoded.consumed_attributes[1]] + ); +} + +#[rstest] +#[case::specific(&[("llm.token_count.prompt", "5"), ("llm.token_count.completion", "9")], 5, 9)] +#[case::fallback(&[], 17, 23)] +#[case::mixed(&[("llm.token_count.prompt", "5")], 5, 23)] +#[case::empty_specific(&[("llm.token_count.prompt", "")], 0, 23)] +fn openinference_fields_override_genai_and_usage_falls_back_per_field( + span: Span, + #[case] token_attributes: &[(&str, &str)], + #[case] input_tokens: u32, + #[case] output_tokens: u32, +) { + let attributes = [ + ("openinference.span.kind", "lLm"), + ("gen_ai.operation.name", "execute_tool"), + ("llm.model_name", "inference-model"), + ("gen_ai.request.model", "other-model"), + ("agent.name", "inference-agent"), + ("input.value", "inference-input"), + ("output.value", "inference-output"), + ("gen_ai.input.messages", "other-input"), + ("gen_ai.output.messages", "other-output"), + ("gen_ai.usage.input_tokens", "17"), + ("gen_ai.usage.output_tokens", "23"), + ]; + let combined = attributes + .iter() + .chain(token_attributes) + .copied() + .collect::>(); + let decoded = decode_normalization(span, "", &combined).unwrap(); + let fields = &decoded.normalized; + assert_eq!(fields.observation_type, ObservationType::Llm); + assert_eq!(fields.model, "inference-model"); + assert_eq!(fields.agent_name, "inference-agent"); + assert_eq!(fields.input, "inference-input"); + assert_eq!(fields.output, "inference-output"); + assert_eq!( + (fields.input_tokens, fields.output_tokens), + (input_tokens, output_tokens) + ); + assert_eq!(decoded.consumed_attributes, ["input.value", "output.value"]); +} + +#[rstest] +#[case::scope("langsmith", &[], ObservationType::Agent)] +#[case::attribute("other", &[("langsmith.span.kind", "llm")], ObservationType::Llm)] +fn langsmith_dispatch_overrides_other_conventions( + span: Span, + #[case] scope: &str, + #[case] convention_attributes: &[(&str, &str)], + #[case] observation_type: ObservationType, +) { + let attributes = [ + ("openinference.span.kind", "TOOL"), + ("gen_ai.operation.name", "execute_tool"), + ("langsmith.metadata.lc_agent_name", "test-agent"), + ( + "gen_ai.prompt", + r#"{"messages":[{"type":"human","content":"hello"}]}"#, + ), + ("gen_ai.completion", "{}"), + ("input.value", "other-input"), + ]; + let combined = attributes + .iter() + .chain(convention_attributes) + .copied() + .collect::>(); + let decoded = decode_normalization(span, scope, &combined).unwrap(); + assert_eq!(decoded.normalized.observation_type, observation_type); + assert_eq!(decoded.normalized.agent_name, "test-agent"); + assert_eq!( + serde_json::from_str::(&decoded.normalized.input).unwrap(), + serde_json::json!([{"role": "user", "content": "hello"}]), + ); + assert_eq!( + decoded.consumed_attributes, + ["gen_ai.prompt", "gen_ai.completion"] + ); +} + +#[rstest] +#[case::flat(r#"{"messages":[{"type":"human","content":"hello"}]}"#)] +#[case::nested(r#"{"messages":[[{"kwargs":{"type":"human","content":"hello"}}],[{"type":"human","content":"ignored batch"}]]}"#)] +fn langsmith_llm_messages_preserve_visible_content_and_tool_calls( + span: Span, + #[case] prompt: &str, +) { + let completion = r#"{ + "generations": [[{"message": {"kwargs": { + "type": "ai", + "content": [ + {"type": "text", "text": "first"}, + {"type": "thinking", "thinking": "hidden"}, + {"type": "tool_use", "id": "call-1"}, + {"type": "text", "text": "second"} + ], + "tool_calls": [{"name": "search", "args": {"query": "hello"}, "id": "call-1"}], + "response_metadata": {"id": "response-1"} + }}}]] + }"#; + let decoded = decode_normalization( + span, + "langsmith", + &[ + ("langsmith.span.kind", "llm"), + ("gen_ai.prompt", prompt), + ("gen_ai.completion", completion), + ], + ) + .unwrap(); + let input: serde_json::Value = serde_json::from_str(&decoded.normalized.input).unwrap(); + let output: serde_json::Value = serde_json::from_str(&decoded.normalized.output).unwrap(); + assert_eq!( + input, + serde_json::json!([{"role": "user", "content": "hello"}]) + ); + assert_eq!( + output, + serde_json::json!({ + "role": "assistant", + "content": "first\n\nsecond", + "tool_calls": [{"name": "search", "args": {"query": "hello"}, "id": "call-1"}], + }) + ); + assert_eq!(decoded.normalized.litellm_request_id, "response-1"); +} + +#[rstest] +#[case::string(r#""result""#, "result")] +#[case::wrapped(r#"{"output":{"content":"result"}}"#, "result")] +#[case::command( + r#"{"output":{"update":{"messages":[{"content":"ignored"},{"content":"result"}]}}}"#, + "result" +)] +#[case::object(r#"{"output":{"count":2}}"#, r#"{"count": 2}"#)] +#[case::null(r#"{"output":null}"#, "null")] +fn langsmith_tool_output_unwraps_supported_shapes( + span: Span, + #[case] completion: &str, + #[case] expected: &str, +) { + let decoded = decode_normalization( + span, + "langsmith", + &[ + ("langsmith.span.kind", "tool"), + ("gen_ai.prompt", "raw-tool-input"), + ("gen_ai.completion", completion), + ], + ) + .unwrap(); + assert_eq!(decoded.normalized.observation_type, ObservationType::Tool); + assert_eq!(decoded.normalized.input, "raw-tool-input"); + assert_eq!(decoded.normalized.output, expected); +} + +const CLAUDE_AGENT_SDK_FIXTURE: &[u8] = + include_bytes!("../../../../tests/test_litellm/tracing/fixtures/claude_agent_sdk_export.json"); +const CLAUDE_AGENT_SDK_DETAILED_FIXTURE: &[u8] = include_bytes!( + "../../../../tests/test_litellm/tracing/fixtures/claude_agent_sdk_detailed_export.json" +); + +fn raw_spans(fixture: &[u8]) -> Vec { + let export: serde_json::Value = serde_json::from_slice(fixture).expect("fixture JSON"); + export["resourceSpans"][0]["scopeSpans"][0]["spans"] + .as_array() + .expect("spans") + .clone() +} + +fn raw_attribute(span: &serde_json::Value, key: &str) -> Option { + span["attributes"] + .as_array() + .expect("attributes") + .iter() + .find(|attribute| attribute["key"] == key) + .map(|attribute| attribute["value"].clone()) +} + +fn raw_string(span: &serde_json::Value, key: &str) -> String { + raw_attribute(span, key) + .and_then(|value| value["stringValue"].as_str().map(str::to_owned)) + .unwrap_or_default() +} + +fn raw_int(span: &serde_json::Value, key: &str) -> u64 { + raw_attribute(span, key).map_or(0, |value| match &value["intValue"] { + serde_json::Value::String(text) => text.parse().expect("integer"), + number => number.as_u64().expect("integer"), + }) +} + +fn raw_span<'a>(raw: &'a [serde_json::Value], span_id: &str) -> &'a serde_json::Value { + raw.iter() + .find(|span| { + span["spanId"] + .as_str() + .is_some_and(|id| id.eq_ignore_ascii_case(span_id)) + }) + .expect("raw span") +} + +#[rstest] +#[case::default_telemetry(CLAUDE_AGENT_SDK_FIXTURE)] +#[case::detailed_telemetry(CLAUDE_AGENT_SDK_DETAILED_FIXTURE)] +fn normalizes_claude_agent_sdk_fixture(#[case] fixture: &[u8]) { + let spans = decode_otlp(fixture, Some("application/json")).expect("valid OTLP export"); + let raw = raw_spans(fixture); + let types: std::collections::BTreeSet<_> = spans + .iter() + .map(|span| format!("{:?}", span.normalized.observation_type)) + .collect(); + assert_eq!( + types, + ["Agent", "Framework", "Llm", "Tool"] + .into_iter() + .map(str::to_owned) + .collect() + ); + + let root = spans + .iter() + .find(|span| span.normalized.observation_type == ObservationType::Agent) + .expect("interaction root"); + assert!(root.parent_span_id.is_empty()); + let root_input: serde_json::Value = + serde_json::from_str(&root.normalized.input).expect("root input messages"); + assert_eq!(root_input[0]["role"], "user"); + assert_eq!( + root_input[0]["content"], + raw_string(raw_span(&raw, &root.span_id), "user_prompt") + ); + assert!(root.consumed_attributes.contains(&"user_prompt")); + + let tools: Vec<_> = spans + .iter() + .filter(|span| span.normalized.observation_type == ObservationType::Tool) + .collect(); + assert_eq!(tools.len(), 2); + for tool in &tools { + assert_eq!( + tool.name, + raw_string(raw_span(&raw, &tool.span_id), "tool_name") + ); + let input: serde_json::Value = + serde_json::from_str(&tool.normalized.input).expect("tool argument object"); + assert!(input.is_object()); + assert!(input.get("role").is_none()); + let event = tool + .events + .iter() + .find(|event| event.name == "tool.output") + .expect("tool output event"); + let expected_output = ["output", "content", "diff"] + .into_iter() + .filter_map(|key| event.attributes.get(key)) + .find(|value| !value.is_empty()) + .expect("event output"); + assert_eq!(&tool.normalized.output, expected_output); + } + let bash = tools + .iter() + .find(|tool| tool.name == "Bash") + .expect("Bash tool"); + assert_eq!( + serde_json::from_str::(&bash.normalized.input).unwrap()["command"], + raw_string(raw_span(&raw, &bash.span_id), "full_command") + ); + + let llms: Vec<_> = spans + .iter() + .filter(|span| span.normalized.observation_type == ObservationType::Llm) + .collect(); + assert!(!llms.is_empty()); + for llm in &llms { + let raw_llm = raw_span(&raw, &llm.span_id); + let expected = raw_int(raw_llm, "input_tokens") + + raw_int(raw_llm, "cache_read_tokens") + + raw_int(raw_llm, "cache_creation_tokens"); + assert_eq!(u64::from(llm.normalized.input_tokens), expected); + assert_eq!( + u64::from(llm.normalized.output_tokens), + raw_int(raw_llm, "output_tokens") + ); + assert_eq!(llm.normalized.model, raw_string(raw_llm, "model")); + if raw_string(raw_llm, "query_source_safe") == "sdk" { + assert_eq!(llm.normalized.framework, "claude-agent-sdk"); + } + } + assert!(spans.iter().all(|span| { + span.normalized.agent_name == span.resource_attributes["service.name"].as_str() + })); +} + +#[rstest] +fn claude_agent_sdk_detailed_fixture_keeps_full_tool_arguments_and_llm_messages() { + let spans = decode_otlp(CLAUDE_AGENT_SDK_DETAILED_FIXTURE, Some("application/json")) + .expect("valid OTLP export"); + let raw = raw_spans(CLAUDE_AGENT_SDK_DETAILED_FIXTURE); + let bash = spans + .iter() + .find(|span| span.name == "Bash") + .expect("Bash tool"); + let tool_input = raw_string(raw_span(&raw, &bash.span_id), "tool_input"); + let (_, arguments) = tool_input.split_once('\n').expect("tool input header"); + assert_eq!( + serde_json::from_str::(&bash.normalized.input).unwrap(), + serde_json::from_str::(arguments).unwrap() + ); + assert!(bash.consumed_attributes.contains(&"tool_input")); + + let answer = spans + .iter() + .find(|span| { + span.normalized.observation_type == ObservationType::Llm + && span.attributes.get("query_source_safe").map(String::as_str) == Some("sdk") + && !span.normalized.output.is_empty() + }) + .expect("final SDK answer"); + let raw_answer = raw_span(&raw, &answer.span_id); + let input: serde_json::Value = + serde_json::from_str(&answer.normalized.input).expect("llm input messages"); + assert_eq!(input[0]["role"], "system"); + assert_eq!( + input[0]["content"], + raw_string(raw_answer, "system_prompt_preview") + ); + let output: serde_json::Value = + serde_json::from_str(&answer.normalized.output).expect("llm output message"); + assert_eq!(output["role"], "assistant"); + assert_eq!( + output["content"], + raw_string(raw_answer, "response.model_output") + ); + + let title = spans + .iter() + .find(|span| { + span.attributes.get("query_source_safe").map(String::as_str) + == Some("generate_session_title") + }) + .expect("side query"); + assert_eq!(title.normalized.framework, "claude-agent-sdk"); +} + +#[rstest] +fn claude_code_scope_takes_precedence_over_openinference_attributes( + mut span: opentelemetry_proto::tonic::trace::v1::Span, +) { + use opentelemetry_proto::tonic::common::v1::{ + AnyValue, InstrumentationScope, KeyValue, any_value::Value, + }; + let string = |key: &str, value: &str| KeyValue { + key: key.to_owned(), + value: Some(AnyValue { + value: Some(Value::StringValue(value.to_owned())), + }), + ..Default::default() + }; + span.attributes = vec![ + string("span.type", "tool"), + string("tool_name", "Grep"), + string("openinference.span.kind", "LLM"), + ]; + let mut request = request_with(span); + request.resource_spans[0].scope_spans[0].scope = Some(InstrumentationScope { + name: "com.anthropic.claude_code.tracing".to_owned(), + ..Default::default() + }); + let spans = decode_otlp(&prost::Message::encode_to_vec(&request), None).expect("valid span"); + assert_eq!(spans[0].normalized.observation_type, ObservationType::Tool); + assert_eq!(spans[0].name, "Grep"); + assert_eq!(spans[0].normalized.framework, "claude-code"); + assert_eq!(spans[0].normalized.agent_name, "claude-code"); +} diff --git a/litellm-rust/crates/traces/tests/query.rs b/litellm-rust/crates/traces/tests/query.rs new file mode 100644 index 00000000000..79f9886dae6 --- /dev/null +++ b/litellm-rust/crates/traces/tests/query.rs @@ -0,0 +1,34 @@ +use litellm_traces::{InvalidQuery, ReadQuery}; +use rstest::rstest; + +#[rstest] +#[case::list_traces("list_traces", ReadQuery::ListTraces)] +#[case::trace_spans("trace_spans", ReadQuery::TraceSpans)] +#[case::span_detail("span_detail", ReadQuery::SpanDetail)] +#[case::span_error("span_error", ReadQuery::SpanError)] +#[case::identity("trace_identity", ReadQuery::TraceIdentity)] +#[case::spend("spend_by_response_ids", ReadQuery::SpendByResponseIds)] +#[case::availability("availability", ReadQuery::Availability)] +#[case::agents("agents", ReadQuery::Agents)] +#[case::sample("sample", ReadQuery::Sample)] +#[case::content("content", ReadQuery::Content)] +#[case::evidence("evidence", ReadQuery::Evidence)] +fn names_select_the_public_query(#[case] name: &str, #[case] query: ReadQuery) { + assert_eq!(ReadQuery::parse(name).unwrap(), query); + assert_eq!(query.as_ref(), name); + assert_eq!(query.to_string(), name); +} + +#[rstest] +#[case::unknown("unknown")] +#[case::case_sensitive("List_Traces")] +#[case::whitespace(" list_traces")] +#[case::empty("")] +fn invalid_names_preserve_the_public_error(#[case] name: &str) { + let error = ReadQuery::parse(name).unwrap_err(); + assert!(matches!(error, InvalidQuery)); + assert_eq!(error.to_string(), "unknown ClickHouse read query"); +} + +#[path = "query/named.rs"] +mod named; diff --git a/litellm-rust/crates/traces/tests/query/named.rs b/litellm-rust/crates/traces/tests/query/named.rs new file mode 100644 index 00000000000..bfe50a8684d --- /dev/null +++ b/litellm-rust/crates/traces/tests/query/named.rs @@ -0,0 +1,67 @@ +use litellm_traces::query::named::*; +use rstest::rstest; +use serde::{Serialize, de::DeserializeOwned}; +use serde_json::{Value, json}; + +fn round_trip(wire: Value) { + let contract: T = serde_json::from_value(wire.clone()).unwrap(); + assert_eq!(serde_json::to_value(contract).unwrap(), wire); +} + +#[rstest] +#[case::admin(1, "", vec![])] +#[case::own_user(0, "user", vec![])] +#[case::multiple_teams(0, "user", vec!["team-a", "team-b"])] +#[case::no_identity(0, "", vec![])] +fn named_requests_preserve_all_access_cases( + #[case] all_teams: u8, + #[case] user: &str, + #[case] teams: Vec<&str>, +) { + let access = json!({"all_teams": all_teams, "user_id": user, "team_ids": teams}); + round_trip::(access.clone()); + let request = |specific: Value| { + Value::Object( + access + .as_object() + .unwrap() + .iter() + .chain(specific.as_object().unwrap()) + .map(|(key, value)| (key.clone(), value.clone())) + .collect(), + ) + }; + round_trip::(request( + json!({"start_ms": -1, "end_ms": 10, "cursor_ms": 0, "cursor_trace_id": "", "limit": 100}), + )); + round_trip::(request(json!({"trace_id": "trace"}))); + round_trip::(request(json!({"trace_id": "trace", "trace_ref": "ref"}))); + round_trip::(request( + json!({"trace_id": "trace", "trace_ref": "ref", "span_id": "span"}), + )); + round_trip::(request( + json!({"trace_id": "trace", "trace_ref": "ref", "span_id": "span", "error_offset": u64::MAX, "error_version": "version"}), + )); + round_trip::(request( + json!({"response_ids": ["response"], "start_ms": -1, "end_ms": 10}), + )); +} + +#[rstest] +fn result_contracts_preserve_public_field_names() { + round_trip::( + json!({"trace_id": "trace", "trace_ref": "ref", "team_id": "team", "api_key_hash": "key", "user_id": "user", "name": "agent", "service": "service", "input_preview": "input", "status": "ok", "start_ms": -1, "duration_ms": 20, "span_count": u64::MAX, "agent_count": 1, "agent_invocations": 2, "agent_names": ["agent"], "frameworks": ["framework"], "llm_calls": 3, "tool_calls": 4, "input_tokens": 5, "output_tokens": 6, "models": ["model"], "error_count": 0, "request_ids": ["request"]}), + ); + round_trip::( + json!({"span_id": "span", "parent_span_id": "parent", "name": "agent", "type": "agent", "agent": "agent", "framework": "framework", "status": "error", "status_message": "error", "error_truncated": 1, "start_ns": -1, "duration_ns": u64::MAX, "service": "service", "input_preview": "input", "model": "model", "input_tokens": u32::MAX, "output_tokens": 6, "litellm_request_id": "request", "team_id": "team", "api_key_hash": "key", "user_id": "user"}), + ); + round_trip::( + json!({"span_id": "span", "input": "input", "output": "output", "attributes": {"count": "42"}}), + ); + round_trip::( + json!({"span_id": "span", "message": "error", "total_chars": u64::MAX, "version": "version"}), + ); + round_trip::( + json!({"request_id": "request", "response_id": "response", "team_id": "team", "api_key": "key", "user": "user", "spend": 0.125, "start_ms": -1}), + ); +} diff --git a/litellm-rust/crates/traces/tests/query_access.rs b/litellm-rust/crates/traces/tests/query_access.rs index b71bcce614e..79155065605 100644 --- a/litellm-rust/crates/traces/tests/query_access.rs +++ b/litellm-rust/crates/traces/tests/query_access.rs @@ -1,268 +1,43 @@ -use std::collections::BTreeMap; - -use litellm_http::Client; -use litellm_traces::{ - Connection, Error, QueryReaders, QueryScope, ensure_schema, query_help, query_sql, -}; -use rstest::{fixture, rstest}; +use litellm_traces::{InvalidScope, QueryScope}; +use rstest::rstest; use serde_json::{Value, json}; -use testcontainers_modules::{ - clickhouse::ClickHouse, - testcontainers::{ContainerAsync, ImageExt, runners::AsyncRunner}, -}; -struct Database { - _container: ContainerAsync, - client: Client, - writer: Connection, - readers: QueryReaders, -} - -#[fixture] -async fn database() -> Result> { - let container = ClickHouse::default() - .with_tag( - "26.9.6.6@sha256:eb4870e7ca7ed70c259eebfcfbee6cf797017f6b5436c2926bbbfe3d4d28486e", - ) - .with_env_var("CLICKHOUSE_SKIP_USER_SETUP", "1") - .start() - .await?; - let writer = Connection::parse(&format!( - "http://{}:{}", - container.get_host().await?, - container.get_host_port_ipv4(8123).await? - ))?; - let client = Client::no_redirect_for_test(); - ensure_schema(&client, &writer, "trace_test", 7).await?; - for sql in [ - "INSERT INTO trace_test.otel_traces (TeamId, ApiKeyHash, TraceId, SpanId, Timestamp, SpanAttributes) VALUES ('team-a', 'key-a1', 'shared-trace', 'a1', now(), map('visible', 'a')), ('team-a', 'key-a2', 'shared-trace', 'a2', now(), map('visible', 'a')), ('team-b', 'key-b', 'shared-trace', 'b', now(), map('secret-b', 'b'))", - "INSERT INTO trace_test.spend_logs (team_id, api_key, request_id, start_time, end_time, metadata) VALUES ('team-a', 'key-a1', 'a1', now(), now(), '{\"visible\":1}'), ('team-a', 'key-a2', 'a2', now(), now(), '{\"visible\":1}'), ('team-b', 'key-b', 'b', now(), now(), '{\"secret_b\":1}')", - "CREATE TABLE trace_test.private_data (secret String) ENGINE = Memory", - "INSERT INTO trace_test.private_data VALUES ('hidden')", - ] { - let response = client.post(writer.url().clone()).body(sql).send().await?; - assert!(response.status().is_success(), "{}", response.text().await?); +#[rstest] +#[case::all(json!({"kind": "all"}), true)] +#[case::own_user(json!({"kind": "owned", "user_id": "user", "team_ids": []}), true)] +#[case::permitted_teams(json!({"kind": "owned", "user_id": "", "team_ids": ["team"]}), true)] +#[case::own_user_and_permitted_teams(json!({"kind": "owned", "user_id": "user", "team_ids": ["team"]}), true)] +#[case::no_identity(json!({"kind": "owned", "user_id": "", "team_ids": []}), false)] +#[case::empty_permitted_team(json!({"kind": "owned", "user_id": "user", "team_ids": [""]}), false)] +fn scope_validation_preserves_authorization_and_wire_shape( + #[case] wire: Value, + #[case] valid: bool, +) { + let scope: QueryScope = serde_json::from_value(wire.clone()).unwrap(); + assert_eq!(serde_json::to_value(&scope).unwrap(), wire); + match (scope.validate(), valid) { + (Ok(()), true) | (Err(InvalidScope), false) => (), + (result, _) => panic!("unexpected validation: {result:?}"), } - let readers = QueryReaders::new(writer.clone(), "trace_test".to_owned()); - Ok(Database { - _container: container, - client, - writer, - readers, - }) } #[rstest] -#[case::team(QueryScope::Team { team_id: "team-a".to_owned() }, vec!["a1", "a2"])] -#[case::project_key(QueryScope::Key { team_id: "team-a".to_owned(), api_key_hash: "key-a1".to_owned() }, vec!["a1"])] -#[case::admin(QueryScope::Admin, vec!["a1", "a2", "b"])] -#[case::quoted_team(QueryScope::Team { team_id: "team-a' OR 1=1 --\\".to_owned() }, vec![])] -#[tokio::test] -async fn queries_and_help_are_scoped_by_the_database( - #[future(awt)] database: Result>, - #[case] scope: QueryScope, - #[case] expected: Vec<&str>, -) -> Result<(), Box> { - let database = database?; - let reader = database - .readers - .connection(&database.client, &scope, "test-master-secret") - .await?; - let queries = [ - "SELECT SpanId AS id FROM otel_traces ORDER BY id", - "SELECT SpanId AS id FROM trace_test.otel_traces WHERE 1 = 1 ORDER BY id", - "SELECT SpanId AS id FROM merge('trace_test', '^otel_traces$') ORDER BY id", - "WITH source AS (SELECT * FROM trace_test.otel_traces) SELECT SpanId AS id FROM source ORDER BY id", - "SELECT SpanId AS id FROM otel_traces UNION DISTINCT SELECT SpanId AS id FROM trace_test.otel_traces ORDER BY id", - "SELECT t.SpanId AS id FROM otel_traces t INNER JOIN spend_logs s ON t.SpanId = s.request_id ORDER BY id", - "SELECT request_id AS id FROM spend_logs FINAL ORDER BY id", - ]; - for sql in queries { - let body: Value = serde_json::from_str(&query_sql(&database.client, &reader, sql).await?)?; - assert_eq!( - body["data"], - json!( - expected - .iter() - .map(|id| json!({"id": id})) - .collect::>() - ), - "{sql}" - ); - } - let summary: Value = serde_json::from_str( - &query_sql( - &database.client, - &reader, - "SELECT sum(SpanCount) AS count FROM agent_traces_by_key", - ) - .await?, - )?; - assert_eq!(summary["data"][0]["count"], json!(expected.len())); - let help = query_help(&database.client, &reader).await?; - assert_eq!(help.contains("secret_b"), expected.contains(&"b")); - assert_eq!(help.contains("secret-b"), expected.contains(&"b")); - let recreated = QueryReaders::new(database.writer.clone(), "trace_test".to_owned()); - let repeated = recreated - .connection(&database.client, &scope, "test-master-secret") - .await?; - assert_eq!(reader.url(), repeated.url()); - Ok(()) +#[case::unknown_kind(json!({"kind": "unknown"}))] +#[case::unknown_field(json!({"kind": "owned", "user_id": "user", "team_ids": [], "extra": true}))] +#[case::legacy_admin(json!({"kind": "admin"}))] +#[case::legacy_logs(json!({"kind": "logs", "user_id": "user", "team_ids": []}))] +#[case::legacy_team(json!({"kind": "team", "team_id": "team"}))] +#[case::key_scope(json!({"kind": "key", "team_id": "team", "api_key_hash": "key"}))] +#[case::key_grant(json!({"kind": "owned", "user_id": "user", "team_ids": [], "api_key_hash": "key"}))] +fn scope_rejects_invalid_wire_shape(#[case] wire: Value) { + assert!(serde_json::from_value::(wire).is_err()); } #[rstest] -#[tokio::test] -async fn rotating_master_secret_revokes_previous_reader_credentials( - #[future(awt)] database: Result>, -) -> Result<(), Box> { - let database = database?; - let scope = QueryScope::Team { - team_id: "team-a".to_owned(), - }; - let old_reader = database - .readers - .connection(&database.client, &scope, "old-master-secret") - .await?; - let old_result = query_sql( - &database.client, - &old_reader, - "SELECT SpanId AS id FROM otel_traces ORDER BY id", - ) - .await?; - let old_rows: Value = serde_json::from_str(&old_result)?; - assert_eq!(old_rows["data"], json!([{ "id": "a1" }, { "id": "a2" }])); - - let rotated_readers = QueryReaders::new(database.writer.clone(), "trace_test".into()); - let new_reader = rotated_readers - .connection(&database.client, &scope, "new-master-secret") - .await?; - assert!( - query_sql( - &database.client, - &old_reader, - "SELECT SpanId AS id FROM otel_traces ORDER BY id", - ) - .await - .is_err() - ); - let new_result = query_sql( - &database.client, - &new_reader, - "SELECT SpanId AS id FROM otel_traces ORDER BY id", - ) - .await?; - let new_rows: Value = serde_json::from_str(&new_result)?; - assert_eq!(new_rows["data"], json!([{ "id": "a1" }, { "id": "a2" }])); - assert_eq!(old_reader.url().username(), new_reader.url().username()); - assert_ne!(old_reader.url().password(), new_reader.url().password()); - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn managed_reader_rejects_privilege_and_scope_bypasses( - #[future(awt)] database: Result>, -) -> Result<(), Box> { - let database = database?; - let scope = QueryScope::Team { - team_id: "team-a".to_owned(), - }; - let reader = database - .readers - .connection(&database.client, &scope, "test-master-secret") - .await?; - for sql in [ - "INSERT INTO otel_traces (TraceId) VALUES ('injected')", - "DROP TABLE otel_traces", - "SELECT * FROM private_data", - "SELECT * FROM otel_traces SETTINGS readonly = 0", - "SELECT * FROM otel_traces SETTINGS max_memory_usage = 0", - "SELECT * FROM otel_traces SETTINGS max_execution_time = 0", - "CREATE USER scope_bypass", - "CREATE NAMED COLLECTION scope_bypass AS host = 'localhost'", - "BACKUP TABLE otel_traces TO Disk('default', 'scope-bypass')", - "SELECT * FROM url('http://127.0.0.1:1/', 'LineAsString', 'line String')", - "SELECT * FROM remote('127.0.0.1', 'trace_test', 'otel_traces')", - ] { - assert!( - matches!( - query_sql(&database.client, &reader, sql).await, - Err(Error::QueryFailed(_)) - ), - "{sql}" - ); - } - let roles: Value = serde_json::from_str( - &query_sql(&database.client, &reader, "SELECT enabledRoles() AS roles").await?, - )?; - assert_eq!(roles["data"], json!([{ "roles": [] }])); - let rows: Value = serde_json::from_str( - &query_sql( - &database.client, - &reader, - "SELECT DISTINCT TeamId FROM otel_traces", - ) - .await?, - )?; - assert_eq!(rows["data"], json!([{ "TeamId": "team-a" }])); - Ok(()) -} - -#[rstest] -#[tokio::test] -async fn provisioning_failure_never_returns_a_writer_connection( - #[future(awt)] database: Result>, -) -> Result<(), Box> { - let database = database?; - let reader = database - .readers - .connection(&database.client, &QueryScope::Admin, "test-master-secret") - .await?; - let no_provision_privileges = QueryReaders::new(reader, "trace_test".to_owned()); - let result = no_provision_privileges - .connection( - &database.client, - &QueryScope::Team { - team_id: "team-a".to_owned(), - }, - "other-secret", - ) - .await; - assert!(result.is_err()); - assert!( - database - .readers - .connection(&database.client, &QueryScope::Admin, "") - .await - .is_err() - ); - assert!( - database - .readers - .connection( - &database.client, - &QueryScope::Team { - team_id: String::new() - }, - "test-master-secret" - ) - .await - .is_err() - ); - let permits = (0..8) - .map(|_| database.readers.acquire()) - .collect::, _>>()?; - assert!(database.readers.acquire().is_err()); - drop(permits); - assert!(database.readers.acquire().is_ok()); - let rows = litellm_traces::execute_read( - &database.client, - &database.writer, - "SELECT count() AS count FROM trace_test.otel_traces", - &BTreeMap::new(), - ) - .await?; - let rows: Value = serde_json::from_str(&rows)?; - assert_eq!(rows["data"][0]["count"], 3); - Ok(()) +fn all_preserves_existing_extra_field_handling() { + let scope: QueryScope = + serde_json::from_value(json!({"kind": "all", "team_id": "ignored"})).unwrap(); + assert!(matches!(scope, QueryScope::All)); + assert!(scope.validate().is_ok()); + assert_eq!(serde_json::to_value(scope).unwrap(), json!({"kind": "all"})); } diff --git a/litellm/completion_extras/litellm_responses_transformation/transformation.py b/litellm/completion_extras/litellm_responses_transformation/transformation.py index 391dbc44eec..7697ac622d0 100644 --- a/litellm/completion_extras/litellm_responses_transformation/transformation.py +++ b/litellm/completion_extras/litellm_responses_transformation/transformation.py @@ -271,16 +271,20 @@ def _flat_responses_tool_choice(choice_type: str, name: str) -> ToolChoiceFuncti def _reasoning_item_to_response_input( r_item: ChatCompletionReasoningItem, ) -> dict[str, object]: - """Convert a stored ChatCompletionReasoningItem back to a Responses API input item.""" - r_input: Final[dict[str, object]] = { + """Convert a stored ChatCompletionReasoningItem back to a Responses API input item. + + An item without an id is sent without one: the Responses API accepts that and + verifies the encrypted content on its own, while it rejects any id it did not mint. + """ + item_id: Final = r_item.get("id") + encrypted_content: Final = r_item.get("encrypted_content") + return { "type": "reasoning", - "id": r_item.get("id") or f"rs_{id(r_item)}", + **({"id": item_id} if item_id else {}), # summary is always required by the Responses API, even when empty "summary": r_item.get("summary") or [], + **({"encrypted_content": encrypted_content} if encrypted_content else {}), } - if r_item.get("encrypted_content"): - r_input["encrypted_content"] = r_item["encrypted_content"] - return r_input class LiteLLMResponsesTransformationHandler(CompletionTransformationBridge): diff --git a/litellm/experimental_mcp_client/client.py b/litellm/experimental_mcp_client/client.py index f133e4837a6..bf6780c7812 100644 --- a/litellm/experimental_mcp_client/client.py +++ b/litellm/experimental_mcp_client/client.py @@ -36,6 +36,7 @@ from mcp.types import ( METHOD_NOT_FOUND, REQUEST_TIMEOUT, ClientCapabilities, + DiscoverResult, ElicitationCapability, FormElicitationCapability, GetPromptRequestParams, @@ -84,6 +85,7 @@ from litellm.types.mcp import ( MCPUpstreamProtocol, credential_redirect_hook, has_header, + validate_mcp_protocol_transport, without_header, ) @@ -401,7 +403,10 @@ class MCPClient: logging_callback: Callable | None = None, protocol_version: MCPUpstreamProtocol = "auto", ): - self.protocol_version: MCPUpstreamProtocol = TypeAdapter(MCPUpstreamProtocol).validate_python(protocol_version) + self.protocol_version: MCPUpstreamProtocol = TypeAdapter[MCPUpstreamProtocol]( + MCPUpstreamProtocol + ).validate_python(protocol_version) + validate_mcp_protocol_transport(self.protocol_version, transport_type) self.server_url: str = server_url self.transport_type: MCPTransport = transport_type self.auth_type: MCPAuthType = auth_type @@ -540,6 +545,17 @@ class MCPClient: return safe_env + async def _prepare_session(self, session: ClientSession) -> InitializeResult | DiscoverResult: + if self.protocol_version != "2026-07-28": + return await self._initialize_session(session) + discovery: Final = DiscoverResult.model_validate(await session.send_discover(self.protocol_version)) + if self.protocol_version not in discovery.supported_versions: + raise MCPError(code=-32022, message="Upstream did not accept the configured MCP protocol version") + session.adopt(discovery) + if session.protocol_version != self.protocol_version: + raise MCPError(code=-32022, message="Upstream selected an unsupported MCP protocol version") + return discovery + async def _initialize_session(self, session: ClientSession) -> InitializeResult: if self.protocol_version == "auto": automatic: Final = await session.initialize() @@ -623,7 +639,7 @@ class MCPClient: ) session: Final = await session_ctx.__aenter__() try: - init_result: Final = await self._initialize_session(session) + init_result: Final = await self._prepare_session(session) instructions: Final = getattr(init_result, "instructions", None) self._last_initialize_instructions = ( instructions.strip() or None if isinstance(instructions, str) else None diff --git a/litellm/integrations/otel/logger.py b/litellm/integrations/otel/logger.py index 55eb8e8fb71..e1983a44451 100644 --- a/litellm/integrations/otel/logger.py +++ b/litellm/integrations/otel/logger.py @@ -915,7 +915,8 @@ def _excluded_db_systems(logger: "OpenTelemetryV2") -> frozenset[str]: logger got published: with ``callbacks: [langfuse_otel, otel]`` the ``otel`` callback folds into the preset, whose config is env-only. """ - configured: Final = litellm.callback_settings.get("otel", {}).get("excluded_services") + otel_settings: Final = (litellm.callback_settings or {}).get("otel") + configured: Final = otel_settings.get("excluded_services") if isinstance(otel_settings, dict) else None if configured is None: return logger.config.excluded_services return excluded_db_systems_from(configured) diff --git a/litellm/integrations/otel/model/config.py b/litellm/integrations/otel/model/config.py index 9eb29157d6f..ddb8e127408 100644 --- a/litellm/integrations/otel/model/config.py +++ b/litellm/integrations/otel/model/config.py @@ -5,7 +5,8 @@ from functools import lru_cache from typing import Annotated, Any, Final from pydantic import AliasChoices, BaseModel, Field, TypeAdapter, ValidationError, field_validator, model_validator -from pydantic_settings import BaseSettings, NoDecode, SettingsConfigDict +from pydantic.fields import FieldInfo +from pydantic_settings import BaseSettings, NoDecode, PydanticBaseSettingsSource, SettingsConfigDict from litellm._logging import verbose_logger from litellm.integrations.otel.model.baggage import ( @@ -121,9 +122,37 @@ class ExporterSpec(BaseModel): ) +class _EnvWithoutBareExcludedServices(PydanticBaseSettingsSource): + def __init__(self, settings_cls: type[BaseSettings], env_settings: PydanticBaseSettingsSource) -> None: + super().__init__(settings_cls) + self._env_settings: Final = env_settings + + def get_field_value(self, field: FieldInfo, field_name: str) -> tuple[object, str, bool]: + return self._env_settings.get_field_value(field, field_name) + + def __call__(self) -> dict[str, object]: + return {key: value for key, value in self._env_settings().items() if key != "excluded_services"} + + class OpenTelemetryV2Config(BaseSettings): model_config = SettingsConfigDict(populate_by_name=True, extra="ignore") + @classmethod + def settings_customise_sources( + cls, + settings_cls: type[BaseSettings], + init_settings: PydanticBaseSettingsSource, + env_settings: PydanticBaseSettingsSource, + dotenv_settings: PydanticBaseSettingsSource, + file_secret_settings: PydanticBaseSettingsSource, + ) -> tuple[PydanticBaseSettingsSource, ...]: + return ( + init_settings, + _EnvWithoutBareExcludedServices(settings_cls, env_settings), + dotenv_settings, + file_secret_settings, + ) + # ----- single-destination shorthand, read from standard OTEL_* envs ----- # exporter: str = Field( default="console", @@ -178,7 +207,7 @@ class OpenTelemetryV2Config(BaseSettings): ) excluded_services: Annotated[frozenset[str], NoDecode] = Field( default_factory=frozenset, - validation_alias=AliasChoices("excluded_services", "LITELLM_OTEL_EXCLUDED_SERVICES"), + validation_alias=AliasChoices("LITELLM_OTEL_EXCLUDED_SERVICES"), description=( "Datastore services whose spans are withheld from key/team ``callback_vars`` " "OTel destinations (the operator's own exporters still receive them). Accepted " diff --git a/litellm/llms/anthropic/chat/handler.py b/litellm/llms/anthropic/chat/handler.py index c1da56bee1e..53e8605011a 100644 --- a/litellm/llms/anthropic/chat/handler.py +++ b/litellm/llms/anthropic/chat/handler.py @@ -702,17 +702,7 @@ class ModelResponseIterator: signature: Final = content_block["delta"].get("signature") if isinstance(signature, str) and signature: - thinking_blocks = [ - ChatCompletionThinkingBlock( - type="thinking", - thinking="".join( - cast(str, block["delta"].get("thinking")) - for block in self.content_blocks - if isinstance(block["delta"].get("thinking"), str) - ), - signature=signature, - ) - ] + thinking_blocks = [ChatCompletionThinkingBlock(type="thinking", thinking="", signature=signature)] provider_specific_fields["thinking_blocks"] = thinking_blocks if reasoning_content is None: reasoning_content = "" diff --git a/litellm/llms/bedrock/chat/invoke_handler.py b/litellm/llms/bedrock/chat/invoke_handler.py index 93804e20041..9f3d27cbfb9 100644 --- a/litellm/llms/bedrock/chat/invoke_handler.py +++ b/litellm/llms/bedrock/chat/invoke_handler.py @@ -42,9 +42,15 @@ from litellm.types.utils import GenericStreamingChunk as GChunk from ..common_utils import ( BedrockError, + BedrockEventStreamResponseDict, + bedrock_event_stream_header, + bedrock_event_stream_response, + bedrock_stream_event_error_status, build_bedrock_stream_error, + build_bedrock_stream_event_error, error_response_text, get_bedrock_response_stream_shape, + get_bedrock_stream_event_statuses, get_bedrock_tool_name, ) @@ -53,6 +59,7 @@ from litellm.llms.bedrock.chat.converse_transformation import AmazonConverseConf if TYPE_CHECKING: from botocore.eventstream import EventStreamMessage + from botocore.model import Shape converse_config: Final = AmazonConverseConfig() _STREAM_HEAD_BYTES: Final = 200 @@ -365,10 +372,14 @@ def _response_header(response_headers: Mapping[str, str] | None, name: str) -> s class _EventStreamTally: - def __init__(self) -> None: + def __init__(self, event_statuses: Mapping[str, int | None] | None) -> None: + self.event_statuses = event_statuses self.bytes_received = 0 self.bytes_decoded = 0 self.events = 0 + self.recognized_events = 0 + self.unrecognized_event_types: frozenset[str] = frozenset() + self.unrecognized_head = b"" self.head = b"" def add_chunk(self, chunk: bytes) -> None: @@ -376,13 +387,24 @@ class _EventStreamTally: if len(self.head) < _STREAM_HEAD_BYTES: self.head = (self.head + chunk)[:_STREAM_HEAD_BYTES] - def add_event(self, event: "EventStreamMessage") -> None: + def add_event(self, event: "EventStreamMessage", headers: Mapping[str, object]) -> None: self.events += 1 self.bytes_decoded += event.prelude.total_length + event_type: Final = bedrock_event_stream_header(headers, ":event-type") + if ( + self.event_statuses is None + or bedrock_event_stream_header(headers, ":message-type") != "event" + or (event_type is not None and event_type in self.event_statuses) + ): + self.recognized_events += 1 + return + self.unrecognized_event_types = self.unrecognized_event_types | {event_type or ""} + if not self.unrecognized_head: + self.unrecognized_head = event.payload[:_STREAM_HEAD_BYTES] def undecoded_stream_error(self, response_headers: Mapping[str, str] | None) -> BedrockError | None: undecoded: Final = self.bytes_received - self.bytes_decoded - if self.events and not undecoded: + if self.recognized_events and not undecoded: return None detail: Final = ( f"content-type={_response_header(response_headers, 'content-type')!r}, " @@ -397,6 +419,15 @@ class _EventStreamTally: f"({detail}, first bytes={self.head!r})" ), ) + if not self.recognized_events: + return BedrockError( + status_code=502, + message=( + f"Bedrock answered the stream with HTTP 200 but none of its {self.events} events carried a known " + f"event type (event types={sorted(self.unrecognized_event_types)}, {detail}, " + f"first payload={self.unrecognized_head!r})" + ), + ) return BedrockError( status_code=502, message=f"Bedrock stream ended with {undecoded} undecoded bytes after {self.events} events ({detail})", @@ -749,13 +780,12 @@ class AWSEventStreamDecoder: from botocore.eventstream import EventStreamBuffer event_stream_buffer: Final = EventStreamBuffer() - tally: Final = _EventStreamTally() + tally: Final = _EventStreamTally(get_bedrock_stream_event_statuses()) for chunk in iterator: event_stream_buffer.add_data(chunk) tally.add_chunk(chunk) for event in event_stream_buffer: - tally.add_event(event) - message = self._parse_message_from_event(event) + message = self._decode_event(event, tally) if message: # sse_event = ServerSentEvent(data=message, event="completion") _data = json.loads(message) @@ -771,13 +801,12 @@ class AWSEventStreamDecoder: from botocore.eventstream import EventStreamBuffer event_stream_buffer: Final = EventStreamBuffer() - tally: Final = _EventStreamTally() + tally: Final = _EventStreamTally(get_bedrock_stream_event_statuses()) async for chunk in iterator: event_stream_buffer.add_data(chunk) tally.add_chunk(chunk) for event in event_stream_buffer: - tally.add_event(event) - message = self._parse_message_from_event(event) + message = self._decode_event(event, tally) if message: _data = json.loads(message) yield self._chunk_parser(chunk_data=_data) @@ -785,7 +814,7 @@ class AWSEventStreamDecoder: if undecoded_stream_error is not None: raise undecoded_stream_error - def _parse_message_from_event(self, event) -> str | None: + def _response_stream_shape(self) -> "Shape": response_stream_shape: Final = get_bedrock_response_stream_shape() if response_stream_shape is None: raise BedrockError( @@ -795,11 +824,29 @@ class AWSEventStreamDecoder: "Ensure botocore is correctly installed." ), ) - response_dict: Final = event.to_response_dict() + return response_stream_shape + + def _decode_event(self, event: "EventStreamMessage", tally: _EventStreamTally) -> str | None: + response_stream_shape: Final = self._response_stream_shape() + response_dict: Final = bedrock_event_stream_response(event) + tally.add_event(event, response_dict["headers"]) + return self._parse_message_from_response(response_dict, response_stream_shape) + + def _parse_message_from_event(self, event: "EventStreamMessage") -> str | None: + response_stream_shape: Final = self._response_stream_shape() + return self._parse_message_from_response(bedrock_event_stream_response(event), response_stream_shape) + + def _parse_message_from_response( + self, response_dict: BedrockEventStreamResponseDict, response_stream_shape: "Shape" + ) -> str | None: parsed_response: Final = self.parser.parse(response_dict, response_stream_shape) if response_dict["status_code"] != 200: raise build_bedrock_stream_error(response_dict, response_stream_shape) + event_type: Final = bedrock_event_stream_header(response_dict["headers"], ":event-type") + event_error_status: Final = bedrock_stream_event_error_status(event_type) + if event_type is not None and event_error_status is not None: + raise build_bedrock_stream_event_error(event_type, event_error_status, response_dict["body"]) if "chunk" in parsed_response: chunk = parsed_response.get("chunk") if not chunk: diff --git a/litellm/llms/bedrock/common_utils.py b/litellm/llms/bedrock/common_utils.py index b876bdb2a54..323890624a0 100644 --- a/litellm/llms/bedrock/common_utils.py +++ b/litellm/llms/bedrock/common_utils.py @@ -9,11 +9,15 @@ import functools import json import os import re -from collections.abc import Mapping, Sequence -from typing import TYPE_CHECKING, Any, Final, Literal, TypedDict +from collections.abc import Iterator, Mapping, Sequence +from types import MappingProxyType +from typing import TYPE_CHECKING, Any, Final, Literal + +from typing_extensions import ReadOnly, TypedDict if TYPE_CHECKING: - from botocore.model import Shape + from botocore.eventstream import EventStreamMessage + from botocore.model import ServiceModel, Shape from litellm.types.llms.bedrock import BedrockCreateBatchRequest @@ -1468,10 +1472,77 @@ def get_bedrock_response_stream_shape(): return _load_bedrock_response_stream_shape() +_BEDROCK_STREAM_OUTPUT_SHAPES: Final = ("ConverseStreamOutput", "ResponseStream") + + +def _modeled_error_status(member: Shape) -> int | None: + status: Final = (member.metadata or {}).get("error", {}).get("httpStatusCode") + return None if status is None else int(status) + + +def _structure_members(shape: Shape | None) -> Mapping[str, Shape]: + from botocore.model import StructureShape + + return shape.members if isinstance(shape, StructureShape) else {} + + +def _bedrock_stream_output_members(service_model: ServiceModel) -> Iterator[tuple[str, Shape]]: + for shape_name in _BEDROCK_STREAM_OUTPUT_SHAPES: + yield from _structure_members(service_model.shape_for(shape_name)).items() + + +def _load_bedrock_stream_event_statuses() -> Mapping[str, int | None] | None: + try: + from botocore.loaders import Loader + from botocore.model import ServiceModel + + service_description: Final = TypeAdapter(Mapping[str, object]).validate_python( + Loader().load_service_model("bedrock-runtime", "service-2") + ) + service_model: Final = ServiceModel(service_description) + return MappingProxyType( + {name: _modeled_error_status(member) for name, member in _bedrock_stream_output_members(service_model)} + ) + except Exception as e: + verbose_logger.warning( + "litellm: could not load the bedrock-runtime stream event types, " + "so unrecognized Bedrock stream events will pass through undetected. Error: %s", + e, + ) + return None + + +@functools.lru_cache(maxsize=1) +def get_bedrock_stream_event_statuses() -> Mapping[str, int | None] | None: + """Every modeled Bedrock stream event type mapped to its error status (None for a content event).""" + return _load_bedrock_stream_event_statuses() + + +def bedrock_stream_event_error_status(event_type: str | None) -> int | None: + statuses: Final = get_bedrock_stream_event_statuses() + return None if event_type is None or statuses is None else statuses.get(event_type) + + class BedrockEventStreamResponseDict(TypedDict): - status_code: int - headers: Mapping[str, str] - body: bytes + status_code: ReadOnly[int] + headers: ReadOnly[Mapping[str, object]] + body: ReadOnly[bytes] + + +_BEDROCK_EVENT_STREAM_RESPONSE: Final = TypeAdapter(BedrockEventStreamResponseDict) + + +def bedrock_event_stream_response(event: EventStreamMessage) -> BedrockEventStreamResponseDict: + return _BEDROCK_EVENT_STREAM_RESPONSE.validate_python(event.to_response_dict()) + + +def bedrock_event_stream_header(headers: Mapping[str, object], name: str) -> str | None: + value: Final = headers.get(name) + return value if isinstance(value, str) else None + + +def build_bedrock_stream_event_error(event_type: str, status_code: int, body: bytes) -> BedrockError: + return BedrockError(status_code=status_code, message=f"{event_type} {body.decode(errors='replace')}") def build_bedrock_stream_error( @@ -1484,19 +1555,14 @@ def build_bedrock_stream_error( ResponseStream member's httpStatusCode is the real status. Resolve it from the shape and fall back to the raw status when the type is not modeled. """ - exception_type: Final = response_dict["headers"].get(":exception-type") - decoded_body: Final = response_dict["body"].decode() - message: Final = f"{exception_type} {decoded_body}" if exception_type else decoded_body + exception_type: Final = bedrock_event_stream_header(response_dict["headers"], ":exception-type") + if exception_type is None: + return BedrockError(status_code=response_dict["status_code"], message=response_dict["body"].decode()) - status_code = response_dict["status_code"] - if exception_type is not None and response_stream_shape is not None: - member: Final = response_stream_shape.members.get(exception_type) - if member is not None: - modeled_status: Final = (member.metadata or {}).get("error", {}).get("httpStatusCode") - if modeled_status is not None: - status_code = int(modeled_status) - - return BedrockError(status_code=status_code, message=message) + member: Final = _structure_members(response_stream_shape).get(exception_type) + modeled_status: Final = None if member is None else _modeled_error_status(member) + status_code: Final = response_dict["status_code"] if modeled_status is None else modeled_status + return build_bedrock_stream_event_error(exception_type, status_code, response_dict["body"]) class BedrockEventStreamDecoderBase: diff --git a/litellm/llms/chatgpt/responses/transformation.py b/litellm/llms/chatgpt/responses/transformation.py index 9774b762396..78ee8a86a65 100644 --- a/litellm/llms/chatgpt/responses/transformation.py +++ b/litellm/llms/chatgpt/responses/transformation.py @@ -35,6 +35,8 @@ from ..common_utils import ( if TYPE_CHECKING: from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLoggingObj +_CHATGPT_SERVICE_TIERS: Final = {"default": "default", "priority": "priority", "fast": "priority"} + class ChatGPTResponsesAPIConfig(OpenAIResponsesAPIConfig): def __init__(self) -> None: @@ -108,7 +110,11 @@ class ChatGPTResponsesAPIConfig(OpenAIResponsesAPIConfig): "truncation", } - return {k: v for k, v in request.items() if k in allowed_keys} + filtered: Final = {k: v for k, v in request.items() if k in allowed_keys} + service_tier: Final = _CHATGPT_SERVICE_TIERS.get(request.get("service_tier")) + if service_tier is not None: + filtered["service_tier"] = service_tier + return filtered def transform_response_api_response( self, diff --git a/litellm/llms/exa_ai/search/transformation.py b/litellm/llms/exa_ai/search/transformation.py index 022622d7af7..cd904e493f2 100644 --- a/litellm/llms/exa_ai/search/transformation.py +++ b/litellm/llms/exa_ai/search/transformation.py @@ -165,6 +165,8 @@ class ExaAISearchConfig(BaseSearchConfig): - results[].title → SearchResult.title - results[].url → SearchResult.url - results[].text → SearchResult.snippet + - results[].highlights → SearchResult.snippet (fallback when "text" is absent) + - results[].summary → SearchResult.snippet (fallback when "text" and "highlights" are absent) - results[].publishedDate → SearchResult.date - No last_updated field in Exa AI response (set to None) @@ -183,7 +185,10 @@ class ExaAISearchConfig(BaseSearchConfig): search_result = SearchResult( title=result.get("title", ""), url=result.get("url", ""), - snippet=result.get("text", ""), # Exa AI uses "text" for content + snippet=result.get("text") + or "\n\n".join(result.get("highlights") or []) + or result.get("summary") + or "", date=result.get("publishedDate"), # ISO 8601 datetime string last_updated=None, # Exa AI doesn't provide last_updated in response ) diff --git a/litellm/llms/laya/__init__.py b/litellm/llms/laya/__init__.py new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/litellm/llms/laya/__init__.py @@ -0,0 +1 @@ + diff --git a/litellm/llms/laya/common_utils.py b/litellm/llms/laya/common_utils.py new file mode 100644 index 00000000000..3e423a9e742 --- /dev/null +++ b/litellm/llms/laya/common_utils.py @@ -0,0 +1,16 @@ +from collections.abc import Mapping +from typing import Final + +from pydantic import BaseModel, TypeAdapter, ValidationError + + +class _LayaRouting(BaseModel): + model: str | None = None + + +def laya_response_model(response: Mapping[str, object], requested_model: str | None) -> str: + try: + routing: Final = TypeAdapter(_LayaRouting).validate_python(response.get("routing") or _LayaRouting()) + except ValidationError: + return requested_model or "unknown" + return routing.model or requested_model or "unknown" diff --git a/litellm/llms/oci/common_utils.py b/litellm/llms/oci/common_utils.py index 3f703564b5a..679a9c21e43 100644 --- a/litellm/llms/oci/common_utils.py +++ b/litellm/llms/oci/common_utils.py @@ -1,16 +1,21 @@ import base64 import hashlib +import importlib import json import os import re +from collections.abc import Mapping from dataclasses import dataclass from email.utils import formatdate -from typing import Final, Protocol +from pathlib import Path +from types import MappingProxyType +from typing import Final, Protocol, runtime_checkable from urllib.parse import urlparse import httpx -from pydantic import JsonValue +from pydantic import BaseModel, ConfigDict, Field, JsonValue, TypeAdapter, ValidationError, field_validator +from litellm._logging import verbose_logger from litellm.llms.base_llm.chat.transformation import BaseLLMException try: @@ -154,7 +159,7 @@ _OCI_KEY_ENV: Final = "OCI_KEY" _OCI_COMPARTMENT_ID_ENV: Final = "OCI_COMPARTMENT_ID" -def resolve_oci_credentials(optional_params: dict) -> dict: +def resolve_oci_credentials(optional_params: Mapping[str, object]) -> dict: """ Merge OCI credentials from optional_params (explicit, always wins) and environment variables (fallback). @@ -174,11 +179,140 @@ def resolve_oci_credentials(optional_params: dict) -> dict: } -_OCI_REGION_RE: Final = re.compile(r"^[a-z][a-z0-9-]{0,30}[a-z0-9]$") +_OCI_REGION_PATTERN: Final = r"^[a-z][a-z0-9-]{0,30}[a-z0-9]$" +_OCI_REALM_DOMAIN_PATTERN: Final = r"^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)*$" +_OCI_REGION_RE: Final = re.compile(_OCI_REGION_PATTERN) _OCI_ACTION_PATH_RE: Final = re.compile(rf"/{OCI_API_VERSION}/actions/[^/?#]+/?$") +_OCI_COMMERCIAL_REALM_DOMAIN: Final = "oraclecloud.com" +_OCI_INFERENCE_ENDPOINT_TEMPLATE: Final = "https://inference.generativeai.{region}.oci.{secondLevelDomain}" +_OCI_REGION_METADATA_ENV: Final = "OCI_REGION_METADATA" +_OCI_REGIONS_CONFIG_FILE: Final = "~/.oci/regions-config.json" +_OCID_REALM_RE: Final = re.compile(r"^ocid1\.[a-z0-9]+\.([a-z0-9]+)\.", re.IGNORECASE) +_OCI_REALM_DOMAINS: Final = MappingProxyType( + { + "oc1": "oraclecloud.com", + "oc2": "oraclegovcloud.com", + "oc3": "oraclegovcloud.com", + "oc4": "oraclegovcloud.uk", + "oc8": "oraclecloud8.com", + "oc9": "oraclecloud9.com", + "oc10": "oraclecloud10.com", + "oc14": "oraclecloud14.com", + "oc15": "oraclecloud15.com", + "oc19": "oraclecloud.eu", + "oc20": "oraclecloud20.com", + "oc21": "oraclecloud21.com", + "oc23": "oraclecloud23.com", + "oc24": "oraclecloud24.com", + "oc26": "oraclecloud26.com", + "oc29": "oraclecloud29.com", + "oc35": "oraclecloud35.com", + "oc42": "oraclecloud42.com", + "oc51": "oraclecloud51.com", + "oc52": "oraclecloud52.com", + } +) -def get_oci_base_url(optional_params: dict, api_base: str | None = None) -> str: +class OCIRegionMetadata(BaseModel): + """One entry of the OCI SDK's region metadata schema, as found in + ``~/.oci/regions-config.json`` (a JSON array) or ``OCI_REGION_METADATA`` (one object). + Values are lowercased before validation, as the SDK does.""" + + model_config = ConfigDict(frozen=True, extra="ignore") + + region_identifier: str = Field(alias="regionIdentifier", pattern=_OCI_REGION_PATTERN) + realm_domain_component: str = Field(alias="realmDomainComponent", pattern=_OCI_REALM_DOMAIN_PATTERN) + + @field_validator("region_identifier", "realm_domain_component", mode="before") + @classmethod + def _lowercase(cls, value: object) -> object: + return value.lower() if isinstance(value, str) else value + + +_JSON_ARRAY: Final = TypeAdapter(tuple[JsonValue, ...]) + + +def _validated_region_metadata(raw: JsonValue, source: str) -> OCIRegionMetadata | None: + try: + return OCIRegionMetadata.model_validate(raw) + except ValidationError as e: + verbose_logger.warning("Ignoring OCI region metadata entry in %s: %s", source, e) + return None + + +def _region_metadata_from_file() -> tuple[OCIRegionMetadata, ...]: + path: Final = Path(os.path.expanduser(_OCI_REGIONS_CONFIG_FILE)) + if not path.is_file(): + return () + try: + raw_entries: Final = _JSON_ARRAY.validate_json(path.read_bytes()) + except (OSError, ValidationError) as e: + verbose_logger.warning("Ignoring OCI region metadata in %s: %s", path, e) + return () + candidates: Final = (_validated_region_metadata(raw, str(path)) for raw in raw_entries) + return tuple(entry for entry in candidates if entry is not None) + + +def _region_metadata_from_env() -> tuple[OCIRegionMetadata, ...]: + raw: Final = os.environ.get(_OCI_REGION_METADATA_ENV) + if not raw: + return () + try: + return (OCIRegionMetadata.model_validate_json(raw),) + except ValidationError as e: + verbose_logger.warning("Ignoring OCI region metadata in %s: %s", _OCI_REGION_METADATA_ENV, e) + return () + + +def _realm_domain_from_ocid(ocid: str | None) -> str | None: + match: Final = _OCID_REALM_RE.match(ocid) if ocid else None + return _OCI_REALM_DOMAINS.get(match.group(1).lower()) if match else None + + +def _realm_domain_from_metadata(region: str) -> str | None: + entries: Final = (*_region_metadata_from_file(), *_region_metadata_from_env()) + return next((entry.realm_domain_component for entry in entries if entry.region_identifier == region), None) + + +@runtime_checkable +class _OCIRegionRegistry(Protocol): + def endpoint_for(self, service: str, region: str, service_endpoint_template: str) -> str: ... + + +def _load_oci_region_registry() -> _OCIRegionRegistry | None: + try: + registry: Final = importlib.import_module("oci.regions") + except ImportError: + return None + return registry if isinstance(registry, _OCIRegionRegistry) else None + + +def resolve_oci_inference_endpoint(region: str, compartment_id: str | None = None) -> str: + """Return the GenAI inference endpoint for ``region`` in whichever OCI realm hosts it. + + The realm's second-level domain comes first from the realm key inside ``compartment_id`` + (``ocid1.compartment.oc2..`` is the Government realm), then from the OCI SDK's region + registry when the SDK is installed, then from the per-region metadata sources the SDK + reads, ``~/.oci/regions-config.json`` and ``OCI_REGION_METADATA``, and otherwise defaults + to the commercial realm. Realm domains per ``oci/regions_definitions.py`` in oci 2.187.0. + A region that is not described anywhere therefore keeps its commercial endpoint, so one + government deployment never redirects the others. + """ + realm_domain: Final = _realm_domain_from_ocid(compartment_id) + if realm_domain is not None: + return _OCI_INFERENCE_ENDPOINT_TEMPLATE.format(region=region, secondLevelDomain=realm_domain) + registry: Final = _load_oci_region_registry() + if registry is not None: + return registry.endpoint_for( + "generative_ai_inference", region=region, service_endpoint_template=_OCI_INFERENCE_ENDPOINT_TEMPLATE + ) + return _OCI_INFERENCE_ENDPOINT_TEMPLATE.format( + region=region, secondLevelDomain=_realm_domain_from_metadata(region) or _OCI_COMMERCIAL_REALM_DOMAIN + ) + + +def get_oci_base_url(optional_params: Mapping[str, object], api_base: str | None = None) -> str: """Return the OCI inference base URL, respecting any explicit api_base override. If ``api_base`` already ends with a fully-formed OCI action path @@ -196,7 +330,8 @@ def get_oci_base_url(optional_params: dict, api_base: str | None = None) -> str: f"Invalid OCI region {region!r}: must match ^[a-z][a-z0-9-]{{0,30}}[a-z0-9]$ (e.g. 'us-ashburn-1')." ), ) - return f"https://inference.generativeai.{region}.oci.oraclecloud.com" + compartment_id: Final = creds["oci_compartment_id"] + return resolve_oci_inference_endpoint(region, compartment_id if isinstance(compartment_id, str) else None) # --------------------------------------------------------------------------- diff --git a/litellm/llms/oci/embed/transformation.py b/litellm/llms/oci/embed/transformation.py index 2300c6ee403..dec43717387 100644 --- a/litellm/llms/oci/embed/transformation.py +++ b/litellm/llms/oci/embed/transformation.py @@ -77,7 +77,11 @@ class OCIEmbedConfig(BaseEmbeddingConfig): Required call-time params (via optional_params or env vars): - ``oci_compartment_id`` / ``OCI_COMPARTMENT_ID`` - - ``oci_region`` / ``OCI_REGION`` (default: ``us-ashburn-1``) + - ``oci_region`` / ``OCI_REGION`` (default: ``us-ashburn-1``). The realm comes from the realm + key in ``oci_compartment_id`` (``ocid1.compartment.oc2..`` is the Government realm), so + non-commercial realms need no extra setting. A realm unknown to litellm can be described in + ``OCI_REGION_METADATA`` or ``~/.oci/regions-config.json``, resolved through the OCI SDK when + it is installed, or given as ``api_base``. Optional call-time params: - ``oci_serving_mode``: ``"ON_DEMAND"`` (default) or ``"DEDICATED"`` diff --git a/litellm/llms/openai/responses/transformation.py b/litellm/llms/openai/responses/transformation.py index a8ec9b2c142..375999c6ca6 100644 --- a/litellm/llms/openai/responses/transformation.py +++ b/litellm/llms/openai/responses/transformation.py @@ -24,6 +24,7 @@ from litellm.litellm_core_utils.url_utils import encode_url_path_segment from litellm.llms.base_llm.responses.transformation import BaseResponsesAPIConfig from litellm.llms.openai.chat.gpt_5_transformation import is_gpt_reasoning_series_name from litellm.responses.litellm_completion_transformation.custom_tools import TOOL_CALL_ITEM_ID_PREFIX_BY_TYPE +from litellm.responses.litellm_completion_transformation.reasoning_items import is_litellm_minted_reasoning_item from litellm.secret_managers.main import get_secret_str from litellm.types.llms.openai import * from litellm.types.responses.main import * @@ -46,6 +47,7 @@ _NO_TOOL_UPDATE: Final[Mapping[str, object]] = MappingProxyType({}) _MODEL_FAMILIES_REJECTING_TOP_LEVEL_SCHEMA_COMBINATORS: Final = ("gpt-4", "gpt-3.5", "chatgpt-4o", "o1", "o3", "o4") _PROVIDERS_WITH_OPENAI_SCHEMA_VALIDATOR: Final = frozenset({LlmProviders.AZURE, LlmProviders.OPENAI}) _PROVIDERS_VALIDATING_TOOL_CALL_ITEM_IDS: Final = frozenset({LlmProviders.AZURE, LlmProviders.OPENAI}) +_PROVIDERS_REPLAYING_ONLY_THEIR_OWN_REASONING: Final = _PROVIDERS_VALIDATING_TOOL_CALL_ITEM_IDS class _ReasoningSupportEntry(BaseModel): @@ -320,7 +322,7 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig): tools: Sequence[ALL_RESPONSES_API_TOOL_PARAMS] | None, litellm_params: GenericLiteLLMParams, ) -> tuple[str | ResponseInputParam, Sequence[ALL_RESPONSES_API_TOOL_PARAMS] | None]: - validated_input: Final = self._validate_input_param(input) + validated_input: Final = self._validate_input_param(self._drop_bridge_minted_reasoning_items(input)) stripped_input, stripped_tools = self.remove_cache_control_flag_from_input_and_tools( model=model, input=validated_input, tools=tools ) @@ -393,6 +395,12 @@ class OpenAIResponsesAPIConfig(BaseResponsesAPIConfig): return input, tools + def _drop_bridge_minted_reasoning_items(self, input: str | ResponseInputParam) -> str | ResponseInputParam: + if self.custom_llm_provider not in _PROVIDERS_REPLAYING_ONLY_THEIR_OWN_REASONING or not isinstance(input, list): + return input + replayable_items: Final = [item for item in input if not is_litellm_minted_reasoning_item(item)] + return cast("ResponseInputParam", replayable_items) # cast-ok: the surviving items keep their shape + def _drop_foreign_tool_call_item_ids(self, input: str | ResponseInputParam) -> str | ResponseInputParam: if self.custom_llm_provider not in _PROVIDERS_VALIDATING_TOOL_CALL_ITEM_IDS or not isinstance(input, list): return input diff --git a/litellm/llms/oss_decision.py b/litellm/llms/oss_decision.py new file mode 100644 index 00000000000..1483adad93f --- /dev/null +++ b/litellm/llms/oss_decision.py @@ -0,0 +1,56 @@ +from collections.abc import Mapping +from dataclasses import dataclass, field +from types import MappingProxyType +from typing import Final, Literal, TypeAlias + +from pydantic import AnyHttpUrl, TypeAdapter, ValidationError + +from litellm.secret_managers.main import get_secret_str + +OssDecisionProvider: TypeAlias = Literal["laya", "bespoke"] +OSS_DECISION_MODELS: Final = MappingProxyType( + { + "laya": ("english", "multilingual", "typed-decisions"), + "bespoke": ("nimble-latest", "nimble", "bespokelabs/Bespoke-Nimble-9B"), + } +) + + +def validate_oss_model(provider: OssDecisionProvider, value: object) -> str: + if not isinstance(value, str) or value not in OSS_DECISION_MODELS[provider]: + raise ValueError(f"{provider} model must be one of {', '.join(OSS_DECISION_MODELS[provider])}") + return value + + +def validate_oss_request(provider: OssDecisionProvider, body: Mapping[str, object]) -> str: + if "custom_body" in body: + raise ValueError(f"custom_body is not supported for {provider} requests") + if body.get("stream"): + raise ValueError(f"Streaming is not supported for {provider} requests") + return validate_oss_model(provider, body.get("model")) + + +@dataclass(frozen=True, slots=True) +class OssDecisionConnection: + api_base: str + api_key: str | None = field(repr=False) + + +def validate_oss_api_base(provider: OssDecisionProvider, value: str) -> str: + try: + url: Final = TypeAdapter(AnyHttpUrl).validate_python(value) + except ValidationError as exc: + raise ValueError(f"{provider} api_base must be an HTTP or HTTPS server URL") from exc + if url.username or url.password or url.query or url.fragment: + raise ValueError(f"{provider} api_base must not contain credentials, a query, or a fragment") + return str(url).rstrip("/") + + +def oss_connection( + provider: OssDecisionProvider, api_base: str | None = None, api_key: str | None = None +) -> OssDecisionConnection: + base: Final = api_base if api_base is not None else get_secret_str(f"{provider.upper()}_API_BASE") + if not base: + raise ValueError(f"{provider} requires api_base or {provider.upper()}_API_BASE pointing to its server") + key: Final = api_key if api_base is not None else api_key or get_secret_str(f"{provider.upper()}_API_KEY") + return OssDecisionConnection(api_base=validate_oss_api_base(provider, base), api_key=key) diff --git a/litellm/model_prices_and_context_window_backup.json b/litellm/model_prices_and_context_window_backup.json index 6a548e7a82d..21bdee73464 100644 --- a/litellm/model_prices_and_context_window_backup.json +++ b/litellm/model_prices_and_context_window_backup.json @@ -386,16 +386,17 @@ "supports_vision": true }, "amazon.nova-2-pro-preview-20251202-v1:0": { - "cache_read_input_token_cost": 5.46875e-07, - "input_cost_per_token": 2.1875e-06, - "input_cost_per_image_token": 2.1875e-06, - "input_cost_per_audio_token": 2.1875e-06, + "cache_read_input_token_cost": 3.125e-07, + "input_cost_per_token": 1.25e-06, + "input_cost_per_image_token": 1.25e-06, + "input_cost_per_audio_token": 1.25e-06, "litellm_provider": "bedrock_converse", "max_input_tokens": 1000000, "max_output_tokens": 64000, "max_tokens": 64000, "mode": "chat", - "output_cost_per_token": 1.75e-05, + "output_cost_per_token": 1e-05, + "source": "https://aws.amazon.com/nova/pricing/", "supports_function_calling": true, "supports_pdf_input": true, "supports_prompt_caching": true, @@ -424,16 +425,17 @@ "supports_vision": true }, "apac.amazon.nova-2-pro-preview-20251202-v1:0": { - "cache_read_input_token_cost": 5.46875e-07, - "input_cost_per_token": 2.1875e-06, - "input_cost_per_image_token": 2.1875e-06, - "input_cost_per_audio_token": 2.1875e-06, + "cache_read_input_token_cost": 3.4375e-07, + "input_cost_per_token": 1.375e-06, + "input_cost_per_image_token": 1.375e-06, + "input_cost_per_audio_token": 1.375e-06, "litellm_provider": "bedrock_converse", "max_input_tokens": 1000000, "max_output_tokens": 64000, "max_tokens": 64000, "mode": "chat", - "output_cost_per_token": 1.75e-05, + "output_cost_per_token": 1.1e-05, + "source": "https://aws.amazon.com/nova/pricing/", "supports_function_calling": true, "supports_pdf_input": true, "supports_prompt_caching": true, @@ -462,16 +464,17 @@ "supports_vision": true }, "eu.amazon.nova-2-pro-preview-20251202-v1:0": { - "cache_read_input_token_cost": 5.46875e-07, - "input_cost_per_token": 2.1875e-06, - "input_cost_per_image_token": 2.1875e-06, - "input_cost_per_audio_token": 2.1875e-06, + "cache_read_input_token_cost": 3.4375e-07, + "input_cost_per_token": 1.375e-06, + "input_cost_per_image_token": 1.375e-06, + "input_cost_per_audio_token": 1.375e-06, "litellm_provider": "bedrock_converse", "max_input_tokens": 1000000, "max_output_tokens": 64000, "max_tokens": 64000, "mode": "chat", - "output_cost_per_token": 1.75e-05, + "output_cost_per_token": 1.1e-05, + "source": "https://aws.amazon.com/nova/pricing/", "supports_function_calling": true, "supports_pdf_input": true, "supports_prompt_caching": true, @@ -500,16 +503,17 @@ "supports_vision": true }, "us.amazon.nova-2-pro-preview-20251202-v1:0": { - "cache_read_input_token_cost": 5.46875e-07, - "input_cost_per_token": 2.1875e-06, - "input_cost_per_image_token": 2.1875e-06, - "input_cost_per_audio_token": 2.1875e-06, + "cache_read_input_token_cost": 3.4375e-07, + "input_cost_per_token": 1.375e-06, + "input_cost_per_image_token": 1.375e-06, + "input_cost_per_audio_token": 1.375e-06, "litellm_provider": "bedrock_converse", "max_input_tokens": 1000000, "max_output_tokens": 64000, "max_tokens": 64000, "mode": "chat", - "output_cost_per_token": 1.75e-05, + "output_cost_per_token": 1.1e-05, + "source": "https://aws.amazon.com/nova/pricing/", "supports_function_calling": true, "supports_pdf_input": true, "supports_prompt_caching": true, @@ -42258,14 +42262,14 @@ "supports_web_search": false }, "openrouter/deepseek/deepseek-v4.1-flash": { - "cache_read_input_token_cost": 1e-08, - "input_cost_per_token": 3e-08, + "cache_read_input_token_cost": 6e-09, + "input_cost_per_token": 3e-07, "litellm_provider": "openrouter", "max_input_tokens": 1048576, "max_output_tokens": 943718, "max_tokens": 943718, "mode": "chat", - "output_cost_per_token": 5e-07, + "output_cost_per_token": 1.2e-06, "source": "https://openrouter.ai/api/v1/models", "supports_audio_input": false, "supports_function_calling": true, @@ -43604,19 +43608,19 @@ "supports_web_search": false }, "openrouter/qwen/qwen3.5-35b-a3b": { - "input_cost_per_token": 1.625e-07, + "input_cost_per_token": 1.5e-07, "litellm_provider": "openrouter", "max_input_tokens": 262144, - "max_output_tokens": 16384, - "max_tokens": 16384, + "max_output_tokens": 235929, + "max_tokens": 235929, "mode": "chat", - "output_cost_per_token": 1.3e-06, + "output_cost_per_token": 1e-06, "source": "https://openrouter.ai/api/v1/models", "supports_function_calling": true, "supports_reasoning": true, "supports_tool_choice": true, "supports_vision": true, - "cache_read_input_token_cost": 1.5625e-07, + "cache_read_input_token_cost": 5e-08, "supports_audio_input": false, "supports_pdf_input": false, "supports_prompt_caching": true, @@ -43924,14 +43928,14 @@ }, "openrouter/z-ai/glm-5.1": { "cache_creation_input_token_cost": 0.0, - "cache_read_input_token_cost": 1.7914e-07, - "input_cost_per_token": 9.646e-07, + "cache_read_input_token_cost": 2.6e-07, + "input_cost_per_token": 1.4e-06, "litellm_provider": "openrouter", "max_input_tokens": 204800, "max_output_tokens": 131072, "max_tokens": 131072, "mode": "chat", - "output_cost_per_token": 3.0316e-06, + "output_cost_per_token": 4.4e-06, "source": "https://openrouter.ai/api/v1/models", "supports_audio_input": false, "supports_function_calling": true, @@ -64014,6 +64018,7 @@ ] }, "xai/grok-voice-transcribe-1.0": { + "deprecation_date": "2026-10-02", "input_cost_per_second": 2.778e-05, "litellm_provider": "xai", "metadata": { @@ -67418,13 +67423,13 @@ "supports_web_search": false }, "openrouter/z-ai/glm-5.3": { - "input_cost_per_token": 2.219e-07, - "output_cost_per_token": 3.39e-06, - "cache_read_input_token_cost": 1.775e-07, + "input_cost_per_token": 1.4e-06, + "output_cost_per_token": 4.4e-06, + "cache_read_input_token_cost": 1.4e-07, "litellm_provider": "openrouter", "max_input_tokens": 1048576, - "max_output_tokens": 943718, - "max_tokens": 943718, + "max_output_tokens": 131072, + "max_tokens": 131072, "mode": "chat", "source": "https://openrouter.ai/api/v1/models", "supports_audio_input": false, @@ -67555,8 +67560,8 @@ "supports_prompt_caching": true }, "openrouter/deepseek/deepseek-v4-flash-0731": { - "cache_read_input_token_cost": 1.08e-08, - "input_cost_per_token": 1.08e-08, + "cache_read_input_token_cost": 5.1e-09, + "input_cost_per_token": 5.1e-09, "litellm_provider": "openrouter", "max_input_tokens": 1048576, "max_output_tokens": 943718, @@ -67608,6 +67613,7 @@ "input_cost_per_token": 9e-08, "output_cost_per_token": 1.8e-07, "cache_read_input_token_cost": 9e-09, + "deprecation_date": "2026-10-31", "litellm_provider": "openrouter", "max_input_tokens": 1048576, "max_output_tokens": 131072, @@ -67625,6 +67631,7 @@ "supports_web_search": false }, "openrouter/poolside/laguna-s-2.1:free": { + "deprecation_date": "2026-10-31", "input_cost_per_token": 0.0, "output_cost_per_token": 0.0, "litellm_provider": "openrouter", @@ -67644,14 +67651,14 @@ "supports_web_search": false }, "openrouter/moonshotai/kimi-k3": { - "cache_read_input_token_cost": 4.357e-07, - "input_cost_per_token": 4.357e-07, + "cache_read_input_token_cost": 2.7e-07, + "input_cost_per_token": 2.7e-06, "litellm_provider": "openrouter", "max_input_tokens": 1048576, "max_output_tokens": 943718, "max_tokens": 943718, "mode": "chat", - "output_cost_per_token": 1e-05, + "output_cost_per_token": 1.35e-05, "source": "https://openrouter.ai/api/v1/models", "supports_audio_input": false, "supports_function_calling": true, @@ -67667,6 +67674,7 @@ "input_cost_per_token": 6e-08, "output_cost_per_token": 1.2e-07, "cache_read_input_token_cost": 3e-08, + "deprecation_date": "2026-10-31", "litellm_provider": "openrouter", "max_input_tokens": 262144, "max_output_tokens": 32768, @@ -67684,6 +67692,7 @@ "supports_web_search": false }, "openrouter/poolside/laguna-xs-2.1:free": { + "deprecation_date": "2026-10-31", "input_cost_per_token": 0.0, "output_cost_per_token": 0.0, "litellm_provider": "openrouter", @@ -67866,13 +67875,13 @@ "supports_web_search": false }, "openrouter/nvidia/nemotron-3-ultra-550b-a55b": { - "input_cost_per_token": 6e-07, - "output_cost_per_token": 2.4e-06, - "cache_read_input_token_cost": 1.2e-07, + "input_cost_per_token": 5e-07, + "output_cost_per_token": 2.2e-06, + "cache_read_input_token_cost": 1e-07, "litellm_provider": "openrouter", "max_input_tokens": 262144, - "max_output_tokens": 182520, - "max_tokens": 182520, + "max_output_tokens": 16384, + "max_tokens": 16384, "mode": "chat", "source": "https://openrouter.ai/api/v1/models", "supports_audio_input": false, @@ -68130,14 +68139,14 @@ "supports_web_search": true }, "openrouter/deepseek/deepseek-v4-flash": { - "cache_read_input_token_cost": 8.372e-09, - "input_cost_per_token": 4.186e-08, + "cache_read_input_token_cost": 5.6e-09, + "input_cost_per_token": 2.8e-08, "litellm_provider": "openrouter", "max_input_tokens": 1048576, - "max_output_tokens": 131072, - "max_tokens": 131072, + "max_output_tokens": 384000, + "max_tokens": 384000, "mode": "chat", - "output_cost_per_token": 8.372e-08, + "output_cost_per_token": 5.6e-08, "source": "https://openrouter.ai/api/v1/models", "supports_audio_input": false, "supports_function_calling": true, @@ -68171,14 +68180,14 @@ "supports_web_search": false }, "openrouter/google/gemma-4-26b-a4b-it": { - "cache_read_input_token_cost": 4.25e-08, - "input_cost_per_token": 7.65e-08, + "cache_read_input_token_cost": 3.75e-08, + "input_cost_per_token": 6.75e-08, "litellm_provider": "openrouter", "max_input_tokens": 262144, "max_output_tokens": 235929, "max_tokens": 235929, "mode": "chat", - "output_cost_per_token": 2.55e-07, + "output_cost_per_token": 2.25e-07, "source": "https://openrouter.ai/api/v1/models", "supports_audio_input": false, "supports_function_calling": true, @@ -68957,11 +68966,11 @@ "openrouter/deepseek/deepseek-v3.1-terminus": { "cache_read_input_token_cost": 1.35e-07, "deprecation_date": "2026-09-28", - "input_cost_per_token": 3e-07, + "input_cost_per_token": 2.7e-07, "litellm_provider": "openrouter", "max_input_tokens": 163840, - "max_output_tokens": 65536, - "max_tokens": 65536, + "max_output_tokens": 147456, + "max_tokens": 147456, "mode": "chat", "output_cost_per_token": 1e-06, "source": "https://openrouter.ai/api/v1/models", @@ -69005,12 +69014,13 @@ "supports_web_search": false }, "openrouter/qwen/qwen3-next-80b-a3b-thinking": { + "deprecation_date": "2026-10-09", "input_cost_per_token": 1.5e-07, "output_cost_per_token": 1.2e-06, "litellm_provider": "openrouter", "max_input_tokens": 262144, - "max_output_tokens": 235929, - "max_tokens": 235929, + "max_output_tokens": 32768, + "max_tokens": 32768, "mode": "chat", "source": "https://openrouter.ai/api/v1/models", "supports_audio_input": false, @@ -69186,13 +69196,13 @@ "supports_web_search": false }, "openrouter/qwen/qwen3-30b-a3b-instruct-2507": { - "input_cost_per_token": 4.815e-08, + "input_cost_per_token": 1e-07, "litellm_provider": "openrouter", "max_input_tokens": 262144, - "max_output_tokens": 32000, - "max_tokens": 32000, + "max_output_tokens": 235929, + "max_tokens": 235929, "mode": "chat", - "output_cost_per_token": 1.9305e-07, + "output_cost_per_token": 3e-07, "source": "https://openrouter.ai/api/v1/models", "supports_audio_input": false, "supports_function_calling": true, @@ -72622,6 +72632,87 @@ "supports_audio_input": true, "supports_video_input": true }, + "bespoke/nimble-latest": { + "input_cost_per_token": 0.0, + "litellm_provider": "bespoke", + "max_input_tokens": 8192, + "mode": "evaluation", + "output_cost_per_token": 0.0, + "source": "https://github.com/bespokelabsai/nimble", + "supported_endpoints": [ + "/v1/systemone" + ], + "metadata": { + "notes": "Self-hosted decision model; infrastructure costs are paid separately" + } + }, + "bespoke/nimble": { + "input_cost_per_token": 0.0, + "litellm_provider": "bespoke", + "max_input_tokens": 8192, + "mode": "evaluation", + "output_cost_per_token": 0.0, + "source": "https://ollama.com/library/nimble", + "supported_endpoints": [ + "/v1/systemone" + ], + "metadata": { + "notes": "Self-hosted decision model under the name Ollama serves it as; infrastructure costs are paid separately" + } + }, + "bespoke/bespokelabs/Bespoke-Nimble-9B": { + "input_cost_per_token": 0.0, + "litellm_provider": "bespoke", + "max_input_tokens": 8192, + "mode": "evaluation", + "output_cost_per_token": 0.0, + "source": "https://github.com/bespokelabsai/nimble", + "supported_endpoints": [ + "/v1/systemone" + ], + "metadata": { + "notes": "Self-hosted decision model; infrastructure costs are paid separately" + } + }, + "laya/english": { + "input_cost_per_token": 0.0, + "litellm_provider": "laya", + "mode": "evaluation", + "output_cost_per_token": 0.0, + "source": "https://github.com/NandhaKishorM/laya", + "supported_endpoints": [ + "/v1/systemone" + ], + "metadata": { + "notes": "Self-hosted decision model; infrastructure costs are paid separately" + } + }, + "laya/multilingual": { + "input_cost_per_token": 0.0, + "litellm_provider": "laya", + "mode": "evaluation", + "output_cost_per_token": 0.0, + "source": "https://github.com/NandhaKishorM/laya", + "supported_endpoints": [ + "/v1/systemone" + ], + "metadata": { + "notes": "Self-hosted decision model; infrastructure costs are paid separately" + } + }, + "laya/typed-decisions": { + "input_cost_per_token": 0.0, + "litellm_provider": "laya", + "mode": "evaluation", + "output_cost_per_token": 0.0, + "source": "https://github.com/NandhaKishorM/laya", + "supported_endpoints": [ + "/v1/systemone" + ], + "metadata": { + "notes": "Self-hosted decision model; infrastructure costs are paid separately" + } + }, "typesafe/jev-1.13.0": { "input_cost_per_token": 4.2e-08, "litellm_provider": "typesafe", @@ -74279,14 +74370,14 @@ "supports_web_search": false }, "openrouter/inclusionai/ling-3.0-flash-fin": { - "cache_read_input_token_cost": 1.2e-08, - "input_cost_per_token": 6e-08, + "cache_read_input_token_cost": 8.4e-09, + "input_cost_per_token": 4.2e-08, "litellm_provider": "openrouter", "max_input_tokens": 262144, - "max_output_tokens": 235929, - "max_tokens": 235929, + "max_output_tokens": 32768, + "max_tokens": 32768, "mode": "chat", - "output_cost_per_token": 1.8e-07, + "output_cost_per_token": 1.232e-07, "source": "https://openrouter.ai/api/v1/models", "supports_audio_input": false, "supports_function_calling": true, @@ -76366,12 +76457,12 @@ "supports_web_search": false }, "openrouter/thinkingmachines/inkling": { - "cache_read_input_token_cost": 1.7e-07, - "input_cost_per_token": 1e-06, + "cache_read_input_token_cost": 1.6e-07, + "input_cost_per_token": 9.5e-07, "litellm_provider": "openrouter", "max_input_tokens": 524288, - "max_output_tokens": 471859, - "max_tokens": 471859, + "max_output_tokens": 262144, + "max_tokens": 262144, "mode": "chat", "output_cost_per_token": 4.05e-06, "source": "https://openrouter.ai/api/v1/models", @@ -79635,5 +79726,24 @@ "supports_response_schema": true, "supports_tool_choice": true, "supports_vision": true + }, + "openrouter/inclusionai/ling-3.1-flash": { + "input_cost_per_token": 0.0, + "litellm_provider": "openrouter", + "max_input_tokens": 262144, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 0.0, + "source": "https://openrouter.ai/api/v1/models", + "supports_audio_input": false, + "supports_function_calling": true, + "supports_pdf_input": false, + "supports_prompt_caching": false, + "supports_reasoning": true, + "supports_response_schema": false, + "supports_tool_choice": true, + "supports_vision": false, + "supports_web_search": false } } diff --git a/litellm/proxy/_experimental/mcp_server/contracts.py b/litellm/proxy/_experimental/mcp_server/contracts.py index b55034e6dc9..82b1861c1e8 100644 --- a/litellm/proxy/_experimental/mcp_server/contracts.py +++ b/litellm/proxy/_experimental/mcp_server/contracts.py @@ -3,12 +3,28 @@ from copy import deepcopy from dataclasses import dataclass, field from datetime import datetime from types import MappingProxyType -from typing import Final, Protocol +from typing import TYPE_CHECKING, Final, Literal, Protocol from litellm.proxy._experimental.mcp_server.tool_outcome import WireCompat from litellm.proxy._types import UserAPIKeyAuth from litellm.types.mcp_server.mcp_server_manager import MCPServer +if TYPE_CHECKING: + from litellm.proxy._experimental.mcp_server.server_resolution import ResolvedMCPServer + + +class TargetCatalog(Protocol): + async def resolve( + self, + server_id: str, + caller: UserAPIKeyAuth, + *, + is_admin_view: bool, + not_found_detail: Mapping[str, str], + forbidden_detail: Mapping[str, str], + non_admin_missing: Literal["not_found", "forbidden"], + ) -> "ResolvedMCPServer": ... + def copy_caller(auth: UserAPIKeyAuth | None) -> UserAPIKeyAuth | None: if auth is None: diff --git a/litellm/proxy/_experimental/mcp_server/db.py b/litellm/proxy/_experimental/mcp_server/db.py index 481ebdb1ef2..bf1f6fa90c1 100644 --- a/litellm/proxy/_experimental/mcp_server/db.py +++ b/litellm/proxy/_experimental/mcp_server/db.py @@ -8,6 +8,7 @@ from datetime import datetime, timedelta, timezone from typing import TYPE_CHECKING, Any, Final, Literal, Protocol, TypedDict, cast from fastapi import HTTPException +from pydantic import TypeAdapter from typing_extensions import ReadOnly from litellm._logging import verbose_proxy_logger @@ -52,8 +53,8 @@ from litellm.repositories.verification_token_repository import ( VerificationTokenRepository, ) from litellm.types.llms.custom_http import httpxSpecialProvider -from litellm.types.mcp import MCPCredentials -from litellm.types.mcp_server.mcp_server_manager import PinnedMCPTool +from litellm.types.mcp import MCPCredentials, MCPTransportType, MCPUpstreamProtocol, validate_mcp_protocol_transport +from litellm.types.mcp_server.mcp_server_manager import MCPInfo, PinnedMCPTool if TYPE_CHECKING: from prisma import models as prisma_db_models @@ -600,6 +601,7 @@ async def _mcp_server_write_if_identifier_free( alias: str | None, exclude_server_id: str | None, write: "Callable[[TableActions[prisma_db_models.LiteLLM_MCPServerTable]], Awaitable[prisma_db_models.LiteLLM_MCPServerTable | None]]", + lock_server_id: str | None = None, ) -> "prisma_db_models.LiteLLM_MCPServerTable | McpIdentifierConflict | None": """Run ``write`` only when no other live row owns ``server_name``/``alias``. @@ -619,6 +621,10 @@ async def _mcp_server_write_if_identifier_free( ) if conflict is not None: return conflict + if lock_server_id is not None: + await tx.execute_raw( + 'SELECT server_id FROM "LiteLLM_MCPServerTable" WHERE server_id=$1 FOR UPDATE', lock_server_id + ) return await write(tx.litellm_mcpservertable) @@ -1100,6 +1106,27 @@ async def get_draft_mcp_server( return table +def _validate_mcp_protocol_write( + stored: "prisma_db_models.LiteLLM_MCPServerTable", data_dict: Mapping[str, object] +) -> None: + raw_info: Final = data_dict.get("mcp_info", stored.mcp_info) + adapter: Final = TypeAdapter[MCPInfo | None](MCPInfo | None) + try: + info: Final = ( + adapter.validate_json(raw_info) if isinstance(raw_info, str) else adapter.validate_python(raw_info) + ) + validate_mcp_protocol_transport( + TypeAdapter[MCPUpstreamProtocol](MCPUpstreamProtocol).validate_python( + (info or {}).get("protocol_version", "auto") + ), + TypeAdapter[MCPTransportType](MCPTransportType).validate_python( + data_dict.get("transport", stored.transport) + ), + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + async def _update_mcp_server_row( prisma_client: PrismaClient, *, @@ -1107,29 +1134,36 @@ async def _update_mcp_server_row( data_dict: Mapping[str, object], ) -> "prisma_db_models.LiteLLM_MCPServerTable | McpIdentifierConflict | None": identifier_write: Final = any(field in data_dict for field in ("server_name", "alias")) + protocol_write: Final = bool({"transport", "mcp_info"}.intersection(data_dict)) async def _update( table: "TableActions[prisma_db_models.LiteLLM_MCPServerTable]", ) -> "prisma_db_models.LiteLLM_MCPServerTable | None": + if protocol_write: + stored: Final = await table.find_unique(where={"server_id": server_id}) + if stored is None: + return None + _validate_mcp_protocol_write(stored, data_dict) return await table.update( where={"server_id": server_id}, data=data_dict, ) - if not identifier_write: + if not identifier_write and not protocol_write: return await _update(_mcp_server_table_actions(prisma_client)) if "alias" in data_dict and not data_dict["alias"] and "server_name" not in data_dict: # Clearing the alias drops the prefix to the stored server_name, which # may already belong to another row, so that name needs the check too. existing: Final = await _db_find_mcp_server_row(prisma_client, server_id) if existing is None: - return await _update(_mcp_server_table_actions(prisma_client)) + return None return await _mcp_server_write_if_identifier_free( prisma_client, server_name=existing.server_name, alias=None, exclude_server_id=server_id, write=_update, + lock_server_id=server_id if protocol_write else None, ) return await _mcp_server_write_if_identifier_free( prisma_client, @@ -1137,6 +1171,7 @@ async def _update_mcp_server_row( alias=_identifier_field(data_dict, "alias"), exclude_server_id=server_id, write=_update, + lock_server_id=server_id if protocol_write else None, ) diff --git a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py index 0682217e481..816ae9aea59 100644 --- a/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py +++ b/litellm/proxy/_experimental/mcp_server/mcp_server_manager.py @@ -58,12 +58,10 @@ from litellm.constants import ( MCP_CLIENT_TIMEOUT, MCP_HEALTH_CHECK_TIMEOUT, MCP_METADATA_TIMEOUT, - MCP_NPM_CACHE_DIR, - MCP_STDIO_ALLOWED_COMMANDS, MCP_TOOL_LISTING_TIMEOUT, ) from litellm.exceptions import BlockedPiiEntityError, GuardrailRaisedException -from litellm.experimental_mcp_client.client import MCPClient, MCPSigV4Auth, strip_auth_scheme, to_basic_credentials +from litellm.experimental_mcp_client.client import MCPClient, strip_auth_scheme, to_basic_credentials from litellm.integrations.custom_guardrail import ( _sync_guardrail_info_to_logging_obj, # pyright: ignore[reportPrivateUsage] - the same bridge @log_guardrail_information uses; reimplementing it here would fork the metadata-key logic ) @@ -91,8 +89,6 @@ from litellm.proxy._experimental.mcp_server.mcp_debug import describe_upstream_h from litellm.proxy._experimental.mcp_server.oauth2_token_cache import ( MCPPerUserTokenCache, mcp_per_user_token_cache, - resolve_mcp_auth, - resolved_token_header, ) from litellm.proxy._experimental.mcp_server.oauth_utils import ( _redact_mcp_resource_url, @@ -105,10 +101,8 @@ from litellm.proxy._experimental.mcp_server.outbound_credentials import ( UpstreamCredentialProvider, ) from litellm.proxy._experimental.mcp_server.outbound_credentials.adapter import ( - prepare_mcp_client, raise_public, raise_token_exchange_challenge, - raise_user_oauth_challenge, to_server_spec, to_subject, ) @@ -118,19 +112,14 @@ from litellm.proxy._experimental.mcp_server.outbound_credentials.oauth_token_sto from litellm.proxy._experimental.mcp_server.outbound_credentials.per_user_oauth_store import ( LazyPerUserOAuthTokenStore, ) -from litellm.proxy._experimental.mcp_server.outbound_credentials.resolver import resolve_credentials_with_source from litellm.proxy._experimental.mcp_server.outbound_credentials.token_exchange_provider import ( build_token_exchanger, ) from litellm.proxy._experimental.mcp_server.outbound_credentials.types import ( DEFAULT_CREDENTIAL_HEADER, - AuthorizationCodeConfig, AuthResolution, - ClientCredentialsConfig, - CredError, IdJagConfig, PassthroughConfig, - ServerSpec, TokenExchangeConfig, ) from litellm.proxy._experimental.mcp_server.result_conversion import ( @@ -142,12 +131,24 @@ from litellm.proxy._experimental.mcp_server.result_conversion import ( from litellm.proxy._experimental.mcp_server.sampling_handler import ( MCP_SAMPLING_AVAILABLE, ) +from litellm.proxy._experimental.mcp_server.stdio_gate import ( + MCP_STDIO_DISABLED_MESSAGE, + is_mcp_stdio_blocked, + warn_if_mcp_stdio_blocked, +) from litellm.proxy._experimental.mcp_server.tool_catalog_guard import ( CatalogAlert, apply_description_overrides, pin_tool_catalog, scan_tool_descriptions, ) +from litellm.proxy._experimental.mcp_server.upstream import ( + passthrough_token_from_mcp_auth_header, + prepare_upstream_client, + resolve_upstream_auth, + take_forwarded_authorization, + to_server_spec_fail_closed, +) from litellm.proxy._experimental.mcp_server.utils import ( MCP_TOOL_PREFIX_SEPARATOR, MCPMissingUserEnvVarsError, @@ -198,10 +199,8 @@ from litellm.types.llms.custom_http import httpxSpecialProvider from litellm.types.mcp import ( DEFAULT_SUBJECT_TOKEN_TYPE, MCPAuth, - MCPStdioConfig, MCPTokenEndpointAuthMethod, MCPUpstreamProtocol, - has_header, without_header, ) from litellm.types.mcp_server.mcp_server_manager import ( @@ -1228,7 +1227,7 @@ def _resolve_openapi_tool_auth( Returns the ``Authorization`` value to inject, the extra headers to forward, and the credential to hand ``resolve_openapi_upstream_auth``, whose passthrough arm reads it via - ``_passthrough_token_from_mcp_auth_header``. The per-server Authorization travels only in the + ``passthrough_token_from_mcp_auth_header``. The per-server Authorization travels only in the credential, never also in the forwarded headers, because the resolver pops Authorization out of those and would otherwise have two sources to reconcile. """ @@ -1319,35 +1318,6 @@ def _client_forwarded_authorization_headers( return extra_headers -def _take_forwarded_authorization( - headers: dict[str, str] | None, -) -> tuple[str | None, dict[str, str] | None]: - """Pop the ``Authorization`` value out of ``headers`` (case-insensitive), returning it with the - remaining headers, so the passthrough resolver arm is the single Authorization source rather than - the header also riding in ``extra_headers`` (which the resolved auth would then defer to).""" - if not headers: - return None, headers - value: Final = next((v for k, v in headers.items() if k.lower() == "authorization"), None) - return value, without_header(headers, DEFAULT_CREDENTIAL_HEADER) - - -def _passthrough_token_from_mcp_auth_header( - mcp_auth_header: str | dict[str, str] | None, -) -> str | None: - """The caller's per-server upstream credential for a passthrough-mode server, or None. - - Sourced from ``x-mcp-{alias}-authorization`` (string or per-header dict form) or the deprecated - global ``x-mcp-auth`` fallback. Per-server headers are the multi-server shape: they bind one - token to one server, so an aggregate scope with several passthrough-mode servers never replays - a single credential across upstreams. The value is forwarded verbatim, so it must be the full - header value (e.g. ``Bearer ``).""" - if isinstance(mcp_auth_header, str): - return mcp_auth_header or None - if isinstance(mcp_auth_header, dict): - return next((v for k, v in mcp_auth_header.items() if k.lower() == "authorization"), None) - return None - - async def _materialize_auth_headers(auth: httpx2.Auth | None) -> dict[str, str] | None: """Extract the header a resolved ``httpx2.Auth`` would set, as a plain dict, or None. @@ -1412,25 +1382,6 @@ def _redacted_registry_dump(servers: dict[str, MCPServer]) -> dict[str, dict[str } -def _to_server_spec_fail_closed(server: MCPServer) -> ServerSpec | None: - """`to_server_spec`, except a half-configured `oauth2_id_jag` server refuses instead of deferring. - - ID-JAG has no v1 arm, so deferring to v1 would let `resolve_mcp_auth` honor a caller x-mcp-* - override or fall through to the static `authentication_token`, both of which bypass the per-user - identity assertion the mode promises. That is an operator misconfiguration, not a fallback. - """ - spec: Final = to_server_spec(server) - if spec is None and server.auth_type == MCPAuth.oauth2_id_jag: - raise_public( - CredError.of_misconfigured( - "oauth2_id_jag requires token_exchange_endpoint, id_jag_resource_token_endpoint, " - "client_id, and a client_secret or client_private_key; refusing to fall back to " - "a static credential." - ) - ) - return spec - - def _caller_authorization_fans_out( server: MCPServer, scope_servers: list[MCPServer] | None, @@ -2464,6 +2415,7 @@ class MCPServerManager: alias=alias, server_name=server_name, ) + warn_if_mcp_stdio_blocked(server_name, server_config.get("transport")) auth_type = server_config.get("auth_type", None) manual_issuer = _blank_to_none(server_config.get("issuer")) @@ -3282,6 +3234,7 @@ class MCPServerManager: # `credentials` field is the only one still encrypted here). # Re-decrypting plaintext would zero the values, so build with # env_vars_are_encrypted=False. + self._warn_if_newly_blocked_stdio(mcp_server, None) new_server: Final = await self.build_mcp_server_from_table(mcp_server, env_vars_are_encrypted=False) self._assign_unique_short_prefix(new_server) self._invalidate_server_definition_caches(mcp_server.server_id) @@ -4080,75 +4033,6 @@ class MCPServerManager: _write_user_env_vars_cache(user_id, server.server_id, values) return values - async def _resolve_v2_auth( - self, - *, - server: MCPServer, - spec: ServerSpec, - provider: UpstreamCredentialProvider, - subject_token: str | None, - user_api_key_auth: UserAPIKeyAuth | None, - extra_headers: dict[str, str] | None, - ) -> tuple[httpx2.Auth | None, dict[str, str] | None]: - """Resolve a v2-owned server's upstream credential into ``(resolved_auth, extra_headers)``. - - On a missing/rejected per-user credential this raises the mode's discovery challenge - (authorization_code's browser-OAuth 401, token_exchange's RFC 9728 challenge) or maps any - other ``CredError`` onto its public HTTP status; it never returns an error as a value. - """ - match await resolve_credentials_with_source(provider, to_subject(user_api_key_auth, subject_token), spec): - case Ok(credential): - auth: Final = credential.auth - # NoOpAuth has no header_name and so never conflicts. - header_name: Final[str | None] = getattr(auth, "header_name", None) - if header_name is None or not extra_headers: - source: Final = ( - AuthResolution.extra_headers - if credential.source == AuthResolution.no_auth and extra_headers - else credential.source - ) - record_auth_resolution(server.server_id, source) - return auth, extra_headers - if not has_header(extra_headers, header_name): - record_auth_resolution(server.server_id, credential.source) - return auth, extra_headers - if isinstance( - spec.config, - (TokenExchangeConfig, AuthorizationCodeConfig, IdJagConfig, ClientCredentialsConfig), - ): - # The resolver owns the credential here (token_exchange's exchanged token, - # authorization_code's stored token, id_jag's minted assertion, - # client_credentials' gateway-minted M2M token). It is authoritative: a - # guardrail such as MCPJWTSigner, static_headers, or any other injected - # Authorization must NOT shadow it (otherwise the upstream gets e.g. the - # signer's JWT instead of the minted token and rejects it, and for M2M the - # one-shot 401 refetch is lost with it). Drop only the header the resolved - # credential is about to occupy, so a static credential the operator aimed at a - # DIFFERENT header still reaches upstream. - record_auth_resolution(server.server_id, credential.source) - return auth, without_header(extra_headers, header_name) - # Other modes: an Authorization already supplied via extra_headers (a forwarded caller - # header or static_headers) is intentional and wins; v1 applies those last. - record_auth_resolution(server.server_id, AuthResolution.extra_headers) - return None, extra_headers - case Error(err): - record_auth_resolution(server.server_id, AuthResolution.failed) - if err.tag == "unauthorized" and isinstance(spec.config, AuthorizationCodeConfig): - # authorization_code's missing per-user token -> the per-server browser-OAuth - # challenge, built here where the full MCPServer is in hand. - raise_user_oauth_challenge(server, root_path=get_request_root_path()) - if err.tag == "unauthorized" and isinstance(spec.config, TokenExchangeConfig): - # token_exchange (OBO): a missing/rejected subject token -> the RFC 9728 challenge - # pointing at the IdP the client must SSO with to obtain one, rather than an opaque - # 401. No gateway-side browser flow. An IdP step-up rejection (Entra Conditional - # Access) threads its claims blob into the challenge for the client to satisfy. - raise_token_exchange_challenge( - server, - root_path=get_request_root_path(), - claims=err.unauthorized.claims, - ) - raise_public(err) - async def preflight_token_exchange( self, server: MCPServer, @@ -4188,7 +4072,7 @@ class MCPServerManager: case _: return resolved_server: Final = await self.ensure_oauth_metadata_discovered(server) - spec: Final = _to_server_spec_fail_closed(resolved_server) + spec: Final = to_server_spec_fail_closed(resolved_server) if spec is None or not isinstance(spec.config, (TokenExchangeConfig, IdJagConfig)): return if subject_token is None and isinstance(spec.config, TokenExchangeConfig): @@ -4218,200 +4102,33 @@ class MCPServerManager: client_ip: str | None = None, protocol_version_override: MCPUpstreamProtocol | None = None, ) -> MCPClient: - """ - Create an MCPClient instance for the given server. - - Auth resolution (single place for all auth logic): - 1. ``mcp_auth_header`` — per-request/per-user override - 2. OAuth2 Token Exchange (OBO) — exchange user token for scoped token - 3. OAuth2 client_credentials token — auto-fetched and cached - 4. ``server.authentication_token`` — static token from config/DB - - Args: - server: The server configuration. - mcp_auth_header: Optional per-request auth override. - extra_headers: Additional headers to forward. - stdio_env: Environment variables for stdio transport. - subject_token: Optional user JWT for token exchange (OBO) flow. - user_api_key_auth: Optional auth context for sampling callbacks. - - Returns: - Configured MCP client instance. - """ record_auth_resolution(server.server_id, AuthResolution.unresolved) resolved_server: Final = await self.ensure_oauth_metadata_discovered(server) - protocol_version: Final = ( - protocol_version_override if protocol_version_override is not None else resolved_server.protocol_version - ) - transport: Final = resolved_server.transport or MCPTransport.sse - spec = None if transport == MCPTransport.stdio else _to_server_spec_fail_closed(resolved_server) - provider: Final = cred_provider or self._cred_provider - # A caller-supplied per-request override (mcp_auth_header / x-mcp-*) defers to the v1 path - # so it wins - except for the modes the v2 resolver owns per-caller (authorization_code's - # stored token, token_exchange's RFC 8693 minted token, id_jag's minted assertion, and the - # passthrough modes' forwarded caller token). A caller must not be able to substitute another - # user's stored credential, nor silently disable the OBO / ID-JAG exchange and forward an - # arbitrary bearer upstream, so we keep the v2 spec and ignore the override for these; the - # REST tools preview supplies its not-yet-persisted token through the resolver - # (cred_provider), never this path. - if ( - spec is not None - and mcp_auth_header - and not isinstance( - spec.config, - (AuthorizationCodeConfig, IdJagConfig, PassthroughConfig, TokenExchangeConfig), - ) - ): - spec = None - auth_value: Final = await resolve_mcp_auth(resolved_server, mcp_auth_header) if spec is None else None - auth_header_name: Final = resolved_token_header(resolved_server, mcp_auth_header) if spec is None else None - - # Create sampling and elicitation callbacks for this client - sampling_cb = ( - _create_sampling_callback( - operation_context=OperationContext( - _caller=user_api_key_auth, raw_headers=raw_headers, client_ip=client_ip - ) - ) - if resolved_server.allow_sampling - else None - ) - elicitation_cb: Final = _create_elicitation_callback() if resolved_server.allow_elicitation else None - - # Handle stdio transport - if transport == MCPTransport.stdio: - resolved_env: Final = ( - stdio_env - if stdio_env is not None - else (dict(resolved_server.env) if resolved_server.env is not None else None) - ) - - # Ensure npm-based STDIO MCP servers have a writable cache dir. - # In containers the default (~/.npm or /app/.npm) may not exist - # or be read-only, causing npx to fail with ENOENT. - if resolved_env is not None and "NPM_CONFIG_CACHE" not in resolved_env: - resolved_env["NPM_CONFIG_CACHE"] = MCP_NPM_CACHE_DIR - # Defense-in-depth: block commands not in the allowlist. - # The Pydantic validator blocks new servers; this catches legacy - # config/DB records predating the allowlist. - if resolved_server.command: - base_command: Final = os.path.basename(resolved_server.command) - # Strip .exe/.cmd/.bat/.com suffix for Windows compatibility - base_command_no_ext = base_command.lower() - for ext in [".exe", ".cmd", ".bat", ".com"]: - if base_command.lower().endswith(ext): - base_command_no_ext = base_command[: -len(ext)].lower() - break - if ( - base_command.lower() not in MCP_STDIO_ALLOWED_COMMANDS - and base_command_no_ext not in MCP_STDIO_ALLOWED_COMMANDS - ): - raise HTTPException( - status_code=403, - detail=f"MCP stdio command '{resolved_server.command}' is not in the allowlist ({sorted(MCP_STDIO_ALLOWED_COMMANDS)}). " - f"Add it to LITELLM_MCP_STDIO_EXTRA_COMMANDS to allow this command.", + return await prepare_upstream_client( + resolved_server, + provider=cred_provider or self._cred_provider, + root_path=get_request_root_path(), + mcp_auth_header=mcp_auth_header, + extra_headers=extra_headers, + stdio_env=stdio_env, + subject_token=subject_token, + user_api_key_auth=user_api_key_auth, + protocol_version=( + protocol_version_override if protocol_version_override is not None else resolved_server.protocol_version + ), + sampling_callback=( + _create_sampling_callback( + operation_context=OperationContext( + _caller=user_api_key_auth, + raw_headers=raw_headers, + client_ip=client_ip, ) - - stdio_config: MCPStdioConfig | None = None - if resolved_server.command and resolved_server.args is not None: - stdio_config = MCPStdioConfig( - command=resolved_server.command, - args=resolved_server.args, - env=resolved_env, ) - - record_auth_resolution(server.server_id, AuthResolution.not_applicable) - return MCPClient( - server_url="", # Not used for stdio - transport_type=transport, - protocol_version=protocol_version, - auth_type=resolved_server.auth_type, - auth_value=auth_value, - timeout=(resolved_server.timeout if resolved_server.timeout is not None else MCP_CLIENT_TIMEOUT), - stdio_config=stdio_config, - extra_headers=extra_headers, - sampling_callback=sampling_cb, - elicitation_callback=elicitation_cb, - ) - else: - # For HTTP/SSE transports - server_url: Final = resolved_server.url or "" - - if spec is not None: - inbound_token = subject_token - if isinstance(spec.config, PassthroughConfig): - inbound_token, extra_headers = _take_forwarded_authorization(extra_headers) - per_server_token: Final = _passthrough_token_from_mcp_auth_header(mcp_auth_header) - if per_server_token is not None: - inbound_token = per_server_token - resolved_auth, extra_headers = await self._resolve_v2_auth( - server=resolved_server, - spec=spec, - provider=provider, - subject_token=inbound_token, - user_api_key_auth=user_api_key_auth, - extra_headers=extra_headers, - ) - return await prepare_mcp_client( - resolved_server, - MCPClient( - server_url=server_url, - transport_type=transport, - protocol_version=protocol_version, - auth_type=resolved_server.auth_type, - timeout=( - resolved_server.timeout if resolved_server.timeout is not None else MCP_CLIENT_TIMEOUT - ), - extra_headers=extra_headers, - resolved_auth=resolved_auth, - sampling_callback=sampling_cb, - elicitation_callback=elicitation_cb, - ), - ) - - # Create SigV4 auth if configured - aws_auth = None - if resolved_server.auth_type == MCPAuth.aws_sigv4: - aws_auth = MCPSigV4Auth( - aws_access_key_id=resolved_server.aws_access_key_id, - aws_secret_access_key=resolved_server.aws_secret_access_key, - aws_session_token=resolved_server.aws_session_token, - aws_region_name=resolved_server.aws_region_name, - aws_service_name=resolved_server.aws_service_name, - aws_role_name=resolved_server.aws_role_name, - aws_session_name=resolved_server.aws_session_name, - ) - - legacy_source: Final = ( - AuthResolution.aws_sigv4 - if aws_auth is not None - else AuthResolution.extra_headers - if extra_headers and has_header(extra_headers, auth_header_name or "Authorization") - else AuthResolution.per_request_header - if mcp_auth_header - else AuthResolution.static_token - if auth_value - else AuthResolution.extra_headers - if extra_headers - else AuthResolution.no_auth - ) - record_auth_resolution(server.server_id, legacy_source) - return await prepare_mcp_client( - resolved_server, - MCPClient( - server_url=server_url, - transport_type=transport, - protocol_version=protocol_version, - auth_type=resolved_server.auth_type, - auth_value=auth_value, - auth_header_name=auth_header_name, - timeout=(resolved_server.timeout if resolved_server.timeout is not None else MCP_CLIENT_TIMEOUT), - extra_headers=extra_headers, - aws_auth=aws_auth, - sampling_callback=sampling_cb, - elicitation_callback=elicitation_cb, - ), - ) + if resolved_server.allow_sampling + else None + ), + elicitation_callback=(_create_elicitation_callback() if resolved_server.allow_elicitation else None), + ) async def _get_tools_from_server( self, @@ -4439,6 +4156,9 @@ class MCPServerManager: global_mcp_tool_registry, ) + if self._skip_blocked_stdio_listing(server, "tool"): + return [] + verbose_logger.debug("Connecting to url: %s", server.url) verbose_logger.info("_get_tools_from_server for %s...", server.name) @@ -4638,6 +4358,19 @@ class MCPServerManager: ) return server.server_id, hashlib.sha256(material.encode()).hexdigest() + @staticmethod + def _warn_if_newly_blocked_stdio(row: LiteLLM_MCPServerTable, previous: MCPServer | None) -> None: + if previous is None or previous.transport != row.transport: + warn_if_mcp_stdio_blocked(row.alias or row.server_name, row.transport) + + def _skip_blocked_stdio_listing(self, server: MCPServer, listing: str) -> bool: + if not is_mcp_stdio_blocked(server.transport): + return False + verbose_logger.debug( + "Skipping %s listing for MCP server %s: %s", listing, server.name, MCP_STDIO_DISABLED_MESSAGE + ) + return True + async def get_prompts_from_server( self, server: MCPServer, @@ -4648,6 +4381,8 @@ class MCPServerManager: raw_headers: dict[str, str] | None = None, client_ip: str | None = None, ) -> list[Prompt]: + if self._skip_blocked_stdio_listing(server, "prompt"): + return [] try: headers: Final = ( dict( @@ -4694,6 +4429,8 @@ class MCPServerManager: raw_headers: dict[str, str] | None = None, client_ip: str | None = None, ) -> list[Resource]: + if self._skip_blocked_stdio_listing(server, "resource"): + return [] try: headers: Final = ( dict( @@ -4740,6 +4477,8 @@ class MCPServerManager: raw_headers: dict[str, str] | None = None, client_ip: str | None = None, ) -> list[ResourceTemplate]: + if self._skip_blocked_stdio_listing(server, "resource template"): + return [] try: headers: Final = ( dict( @@ -6324,6 +6063,8 @@ class MCPServerManager: mcp_server = fallback if mcp_server is None: raise ValueError(f"Tool {name} not found") + if is_mcp_stdio_blocked(mcp_server.transport): + raise HTTPException(status_code=403, detail=MCP_STDIO_DISABLED_MESSAGE) if resolved_by_server_name_only and not self.server_exposes_tool(mcp_server, name): raise ValueError(f"Tool {name} not found") @@ -6423,7 +6164,7 @@ class MCPServerManager: token, token_exchange's exchanged token, passthrough's forwarded caller token) must be materialized into headers here. Returns ``(resolved_auth_headers, forwarded_headers)``: the resolved headers are authoritative over every other Authorization source (the same - rule ``_resolve_v2_auth`` applies on the MCPClient path) and ``forwarded_headers`` comes + rule ``resolve_upstream_auth`` applies on the MCPClient path) and ``forwarded_headers`` comes back with any header the resolver claimed already dropped. Unmigrated (v1) servers resolve through the stored-token lookup instead, and a missing per-user credential raises the same discovery challenge the MCPClient path serves, rather than egressing unauthenticated. @@ -6446,11 +6187,12 @@ class MCPServerManager: if isinstance(spec.config, (TokenExchangeConfig, IdJagConfig)): subject_token = self._extract_subject_token(oauth2_headers, raw_headers, user_api_key_auth) elif isinstance(spec.config, PassthroughConfig): - inbound_token, forwarded_headers = _take_forwarded_authorization(forwarded_headers) - per_server_token: Final = _passthrough_token_from_mcp_auth_header(mcp_auth_header) + inbound_token, forwarded_headers = take_forwarded_authorization(forwarded_headers) + per_server_token: Final = passthrough_token_from_mcp_auth_header(mcp_auth_header) subject_token = per_server_token if per_server_token is not None else inbound_token - resolved_auth, forwarded_headers = await self._resolve_v2_auth( + resolved_auth, forwarded_headers = await resolve_upstream_auth( + root_path=get_request_root_path(), server=mcp_server, spec=spec, provider=self._cred_provider, @@ -6708,7 +6450,10 @@ class MCPServerManager: if matched is not None: matched_prefix, original_tool_name = matched matched_server: Final = prefix_to_server.get(matched_prefix) - if matched_server is not None and self.server_exposes_tool(matched_server, original_tool_name): + if matched_server is not None and ( + self.server_exposes_tool(matched_server, original_tool_name) + or is_mcp_stdio_blocked(matched_server.transport) + ): return matched_server return None @@ -6770,6 +6515,7 @@ class MCPServerManager: alias=getattr(server, "alias", None), server_name=getattr(server, "server_name", None), ) + self._warn_if_newly_blocked_stdio(server, existing_server) verbose_logger.debug("Building server from DB: %s (%s)", server.server_id, server.server_name) # raw_rows come straight from the DB, so their global env var # values (like credentials) are still encrypted here, unlike the diff --git a/litellm/proxy/_experimental/mcp_server/server_resolution.py b/litellm/proxy/_experimental/mcp_server/server_resolution.py index 8168fea9068..54fd17fa280 100644 --- a/litellm/proxy/_experimental/mcp_server/server_resolution.py +++ b/litellm/proxy/_experimental/mcp_server/server_resolution.py @@ -120,3 +120,42 @@ async def authorize_mcp_server( ) return resolved + + +@dataclass(frozen=True, slots=True) +class MCPServerTargetCatalog: + manager: MCPServerRegistry + db_lookup: Callable[[str], Awaitable[LiteLLM_MCPServerTable | None]] | None = None + temp_lookup: Callable[[str], Awaitable[MCPServer | None]] | None = None + id_client_ip: str | None = None + name_client_ip: str | None = None + match_name: bool = False + + async def resolve( + self, + server_id: str, + caller: UserAPIKeyAuth, + *, + is_admin_view: bool, + not_found_detail: Mapping[str, str], + forbidden_detail: Mapping[str, str], + non_admin_missing: Literal["not_found", "forbidden"], + ) -> ResolvedMCPServer: + resolved: Final = await resolve_mcp_server( + server_id, + manager=self.manager, + db_lookup=self.db_lookup, + temp_lookup=self.temp_lookup, + id_client_ip=self.id_client_ip, + name_client_ip=self.name_client_ip, + match_name=self.match_name, + ) + return await authorize_mcp_server( + resolved, + caller, + manager=self.manager, + is_admin_view=is_admin_view, + not_found_detail=not_found_detail, + forbidden_detail=forbidden_detail, + non_admin_missing=non_admin_missing, + ) diff --git a/litellm/proxy/_experimental/mcp_server/stdio_gate.py b/litellm/proxy/_experimental/mcp_server/stdio_gate.py new file mode 100644 index 00000000000..00fde229e2f --- /dev/null +++ b/litellm/proxy/_experimental/mcp_server/stdio_gate.py @@ -0,0 +1,28 @@ +import os +from typing import Final + +from litellm._logging import verbose_logger +from litellm.types.mcp import MCPTransport + +MCP_STDIO_ENABLED_ENV_VAR: Final = "LITELLM_ENABLE_MCP_STDIO" +MCP_STDIO_DISABLED_MESSAGE: Final = ( + f"stdio MCP servers are disabled on this proxy. " + f"Set {MCP_STDIO_ENABLED_ENV_VAR}=true on the proxy and restart to enable them" +) + + +def is_mcp_stdio_enabled() -> bool: + return os.getenv(MCP_STDIO_ENABLED_ENV_VAR, "").strip().lower() == "true" + + +def is_mcp_stdio_flag_key(env_var_name: str) -> bool: + return env_var_name.upper() == MCP_STDIO_ENABLED_ENV_VAR + + +def is_mcp_stdio_blocked(transport: str | None) -> bool: + return transport == MCPTransport.stdio and not is_mcp_stdio_enabled() + + +def warn_if_mcp_stdio_blocked(server_name: str | None, transport: str | None) -> None: + if is_mcp_stdio_blocked(transport): + verbose_logger.warning("MCP server '%s' will not start: %s", server_name, MCP_STDIO_DISABLED_MESSAGE) diff --git a/litellm/proxy/_experimental/mcp_server/upstream.py b/litellm/proxy/_experimental/mcp_server/upstream.py new file mode 100644 index 00000000000..89f433093e6 --- /dev/null +++ b/litellm/proxy/_experimental/mcp_server/upstream.py @@ -0,0 +1,315 @@ +from __future__ import annotations + +import os +from typing import Final + +import httpx2 +from fastapi import HTTPException + +from litellm.constants import MCP_CLIENT_TIMEOUT, MCP_NPM_CACHE_DIR, MCP_STDIO_ALLOWED_COMMANDS +from litellm.experimental_mcp_client.client import MCPClient, MCPSigV4Auth +from litellm.proxy._experimental.mcp_server.legacy_callbacks import ElicitationCallback, SamplingCallback +from litellm.proxy._experimental.mcp_server.mcp_debug import record_auth_resolution +from litellm.proxy._experimental.mcp_server.oauth2_token_cache import resolve_mcp_auth, resolved_token_header +from litellm.proxy._experimental.mcp_server.outbound_credentials import Error, Ok, UpstreamCredentialProvider +from litellm.proxy._experimental.mcp_server.outbound_credentials.adapter import ( + prepare_mcp_client, + raise_public, + raise_token_exchange_challenge, + raise_user_oauth_challenge, + to_server_spec, + to_subject, +) +from litellm.proxy._experimental.mcp_server.outbound_credentials.resolver import resolve_credentials_with_source +from litellm.proxy._experimental.mcp_server.outbound_credentials.types import ( + DEFAULT_CREDENTIAL_HEADER, + AuthorizationCodeConfig, + AuthResolution, + ClientCredentialsConfig, + CredError, + IdJagConfig, + PassthroughConfig, + ServerSpec, + TokenExchangeConfig, +) +from litellm.proxy._experimental.mcp_server.stdio_gate import MCP_STDIO_DISABLED_MESSAGE, is_mcp_stdio_enabled +from litellm.proxy._types import MCPTransport, UserAPIKeyAuth +from litellm.types.mcp import ( + MCPAuth, + MCPStdioConfig, + MCPTransportType, + MCPUpstreamProtocol, + has_header, + without_header, +) +from litellm.types.mcp_server.mcp_server_manager import MCPServer + + +def take_forwarded_authorization( + headers: dict[str, str] | None, +) -> tuple[str | None, dict[str, str] | None]: + """Pop the ``Authorization`` value out of ``headers`` (case-insensitive), returning it with the + remaining headers, so the passthrough resolver arm is the single Authorization source rather than + the header also riding in ``extra_headers`` (which the resolved auth would then defer to).""" + if not headers: + return None, headers + value: Final = next((v for k, v in headers.items() if k.lower() == "authorization"), None) + return value, without_header(headers, DEFAULT_CREDENTIAL_HEADER) + + +def passthrough_token_from_mcp_auth_header( + mcp_auth_header: str | dict[str, str] | None, +) -> str | None: + """The caller's per-server upstream credential for a passthrough-mode server, or None. + + Sourced from ``x-mcp-{alias}-authorization`` (string or per-header dict form) or the deprecated + global ``x-mcp-auth`` fallback. Per-server headers are the multi-server shape: they bind one + token to one server, so an aggregate scope with several passthrough-mode servers never replays + a single credential across upstreams. The value is forwarded verbatim, so it must be the full + header value (e.g. ``Bearer ``).""" + if isinstance(mcp_auth_header, str): + return mcp_auth_header or None + if isinstance(mcp_auth_header, dict): + return next((v for k, v in mcp_auth_header.items() if k.lower() == "authorization"), None) + return None + + +def to_server_spec_fail_closed(server: MCPServer) -> ServerSpec | None: + """`to_server_spec`, except a half-configured `oauth2_id_jag` server refuses instead of deferring. + + ID-JAG has no v1 arm, so deferring to v1 would let `resolve_mcp_auth` honor a caller x-mcp-* + override or fall through to the static `authentication_token`, both of which bypass the per-user + identity assertion the mode promises. That is an operator misconfiguration, not a fallback. + """ + spec: Final = to_server_spec(server) + if spec is None and server.auth_type == MCPAuth.oauth2_id_jag: + raise_public( + CredError.of_misconfigured( + "oauth2_id_jag requires token_exchange_endpoint, id_jag_resource_token_endpoint, " + "client_id, and a client_secret or client_private_key; refusing to fall back to " + "a static credential." + ) + ) + return spec + + +async def resolve_upstream_auth( + *, + server: MCPServer, + spec: ServerSpec, + root_path: str, + provider: UpstreamCredentialProvider, + subject_token: str | None, + user_api_key_auth: UserAPIKeyAuth | None, + extra_headers: dict[str, str] | None, +) -> tuple[httpx2.Auth | None, dict[str, str] | None]: + """Resolve a v2-owned server's upstream credential into ``(resolved_auth, extra_headers)``. + + On a missing/rejected per-user credential this raises the mode's discovery challenge + (authorization_code's browser-OAuth 401, token_exchange's RFC 9728 challenge) or maps any + other ``CredError`` onto its public HTTP status; it never returns an error as a value. + """ + match await resolve_credentials_with_source(provider, to_subject(user_api_key_auth, subject_token), spec): + case Ok(credential): + auth: Final = credential.auth + # NoOpAuth has no header_name and so never conflicts. + header_name: Final[str | None] = getattr(auth, "header_name", None) + if header_name is None or not extra_headers: + source: Final = ( + AuthResolution.extra_headers + if credential.source == AuthResolution.no_auth and extra_headers + else credential.source + ) + record_auth_resolution(server.server_id, source) + return auth, extra_headers + if not has_header(extra_headers, header_name): + record_auth_resolution(server.server_id, credential.source) + return auth, extra_headers + if isinstance( + spec.config, + (TokenExchangeConfig, AuthorizationCodeConfig, IdJagConfig, ClientCredentialsConfig), + ): + # The resolver owns the credential here (token_exchange's exchanged token, + # authorization_code's stored token, id_jag's minted assertion, + # client_credentials' gateway-minted M2M token). It is authoritative: a + # guardrail such as MCPJWTSigner, static_headers, or any other injected + # Authorization must NOT shadow it (otherwise the upstream gets e.g. the + # signer's JWT instead of the minted token and rejects it, and for M2M the + # one-shot 401 refetch is lost with it). Drop only the header the resolved + # credential is about to occupy, so a static credential the operator aimed at a + # DIFFERENT header still reaches upstream. + record_auth_resolution(server.server_id, credential.source) + return auth, without_header(extra_headers, header_name) + # Other modes: an Authorization already supplied via extra_headers (a forwarded caller + # header or static_headers) is intentional and wins; v1 applies those last. + record_auth_resolution(server.server_id, AuthResolution.extra_headers) + return None, extra_headers + case Error(err): + record_auth_resolution(server.server_id, AuthResolution.failed) + if err.tag == "unauthorized" and isinstance(spec.config, AuthorizationCodeConfig): + # authorization_code's missing per-user token -> the per-server browser-OAuth + # challenge, built here where the full MCPServer is in hand. + raise_user_oauth_challenge(server, root_path=root_path) + if err.tag == "unauthorized" and isinstance(spec.config, TokenExchangeConfig): + # token_exchange (OBO): a missing/rejected subject token -> the RFC 9728 challenge + # pointing at the IdP the client must SSO with to obtain one, rather than an opaque + # 401. No gateway-side browser flow. An IdP step-up rejection (Entra Conditional + # Access) threads its claims blob into the challenge for the client to satisfy. + raise_token_exchange_challenge( + server, + root_path=root_path, + claims=err.unauthorized.claims, + ) + raise_public(err) + + +def _stdio_config(server: MCPServer, stdio_env: dict[str, str] | None) -> MCPStdioConfig | None: + if not is_mcp_stdio_enabled(): + raise HTTPException(status_code=403, detail=MCP_STDIO_DISABLED_MESSAGE) + if server.command: + command: Final = os.path.basename(server.command) + lowercase: Final = command.lower() + normalized: Final = next( + ( + lowercase.removesuffix(suffix) + for suffix in (".exe", ".cmd", ".bat", ".com") + if lowercase.endswith(suffix) + ), + lowercase, + ) + if command not in MCP_STDIO_ALLOWED_COMMANDS and normalized not in MCP_STDIO_ALLOWED_COMMANDS: + raise HTTPException( + status_code=403, + detail=f"MCP stdio command '{server.command}' is not in the allowlist ({sorted(MCP_STDIO_ALLOWED_COMMANDS)}). " + "Add it to LITELLM_MCP_STDIO_EXTRA_COMMANDS to allow this command.", + ) + if not server.command or server.args is None: + return None + environment: Final = stdio_env if stdio_env is not None else server.env + return MCPStdioConfig( + command=server.command, + args=server.args, + env={"NPM_CONFIG_CACHE": MCP_NPM_CACHE_DIR, **environment} if environment is not None else None, + ) + + +async def prepare_upstream_client( + server: MCPServer, + *, + provider: UpstreamCredentialProvider, + root_path: str, + mcp_auth_header: str | dict[str, str] | None = None, + extra_headers: dict[str, str] | None = None, + stdio_env: dict[str, str] | None = None, + subject_token: str | None = None, + user_api_key_auth: UserAPIKeyAuth | None = None, + protocol_version: MCPUpstreamProtocol, + sampling_callback: SamplingCallback | None = None, + elicitation_callback: ElicitationCallback | None = None, +) -> MCPClient: + transport: Final[MCPTransportType] = server.transport or MCPTransport.sse + server_spec: Final = None if transport == MCPTransport.stdio else to_server_spec_fail_closed(server) + spec: Final = ( + None + if server_spec is not None + and mcp_auth_header + and not isinstance( + server_spec.config, (AuthorizationCodeConfig, IdJagConfig, PassthroughConfig, TokenExchangeConfig) + ) + else server_spec + ) + auth_value: Final = await resolve_mcp_auth(server, mcp_auth_header) if spec is None else None + auth_header_name: Final = resolved_token_header(server, mcp_auth_header) if spec is None else None + timeout: Final = server.timeout if server.timeout is not None else MCP_CLIENT_TIMEOUT + if transport == MCPTransport.stdio: + config: Final = _stdio_config(server, stdio_env) + record_auth_resolution(server.server_id, AuthResolution.not_applicable) + return MCPClient( + server_url="", + transport_type=transport, + protocol_version=protocol_version, + auth_type=server.auth_type, + auth_value=auth_value, + timeout=timeout, + stdio_config=config, + extra_headers=extra_headers, + sampling_callback=sampling_callback, + elicitation_callback=elicitation_callback, + ) + if spec is not None: + inbound_token, forwarded_headers = ( + take_forwarded_authorization(extra_headers) + if isinstance(spec.config, PassthroughConfig) + else (subject_token, extra_headers) + ) + per_server_token: Final = ( + passthrough_token_from_mcp_auth_header(mcp_auth_header) + if isinstance(spec.config, PassthroughConfig) + else None + ) + resolved_auth, resolved_headers = await resolve_upstream_auth( + server=server, + spec=spec, + provider=provider, + root_path=root_path, + subject_token=per_server_token if per_server_token is not None else inbound_token, + user_api_key_auth=user_api_key_auth, + extra_headers=forwarded_headers, + ) + return await prepare_mcp_client( + server, + MCPClient( + server_url=server.url or "", + transport_type=transport, + protocol_version=protocol_version, + auth_type=server.auth_type, + timeout=timeout, + extra_headers=resolved_headers, + resolved_auth=resolved_auth, + sampling_callback=sampling_callback, + elicitation_callback=elicitation_callback, + ), + ) + aws_auth: Final = ( + MCPSigV4Auth( + aws_access_key_id=server.aws_access_key_id, + aws_secret_access_key=server.aws_secret_access_key, + aws_session_token=server.aws_session_token, + aws_region_name=server.aws_region_name, + aws_service_name=server.aws_service_name, + aws_role_name=server.aws_role_name, + aws_session_name=server.aws_session_name, + ) + if server.auth_type == MCPAuth.aws_sigv4 + else None + ) + source: Final = ( + AuthResolution.aws_sigv4 + if aws_auth is not None + else AuthResolution.extra_headers + if extra_headers and has_header(extra_headers, auth_header_name or "Authorization") + else AuthResolution.per_request_header + if mcp_auth_header + else AuthResolution.static_token + if auth_value + else AuthResolution.extra_headers + if extra_headers + else AuthResolution.no_auth + ) + record_auth_resolution(server.server_id, source) + return await prepare_mcp_client( + server, + MCPClient( + server_url=server.url or "", + transport_type=transport, + protocol_version=protocol_version, + auth_type=server.auth_type, + auth_value=auth_value, + auth_header_name=auth_header_name, + timeout=timeout, + extra_headers=extra_headers, + aws_auth=aws_auth, + sampling_callback=sampling_callback, + elicitation_callback=elicitation_callback, + ), + ) diff --git a/litellm/proxy/_lazy_features.py b/litellm/proxy/_lazy_features.py index 0b470cf7bda..54d757d75aa 100644 --- a/litellm/proxy/_lazy_features.py +++ b/litellm/proxy/_lazy_features.py @@ -229,6 +229,8 @@ LAZY_FEATURES: Final[tuple[LazyFeature, ...]] = ( "/tinyfish/", "/transcribe", "/typesafe/", + "/laya/", + "/bespoke/", "/openrouter/", "/vertex-ai/", "/vertex_ai/", @@ -530,11 +532,11 @@ def _register_all_on_startup(inner: "Lifespan[FastAPI]", features: tuple[LazyFea (config pass-through endpoints), so the table is put back in lazy mode's order once it is up.""" @asynccontextmanager - async def lifespan(app: "FastAPI") -> AsyncGenerator[None]: + async def lifespan(app: "FastAPI") -> AsyncGenerator[Mapping[str, object]]: register_all_features(app, features) - async with inner(app): + async with inner(app) as state: _restore_registry_order(app, features) - yield + yield state if state is not None else {} return lifespan diff --git a/litellm/proxy/_lazy_openapi_snapshot.json b/litellm/proxy/_lazy_openapi_snapshot.json index 5de6e91a0f2..727ecb235bb 100644 --- a/litellm/proxy/_lazy_openapi_snapshot.json +++ b/litellm/proxy/_lazy_openapi_snapshot.json @@ -26318,6 +26318,30 @@ ] } }, + "/bespoke/v1/systemone": { + "post": { + "operationId": "bespoke_proxy_route_bespoke_v1_systemone_post", + "responses": { + "200": { + "content": { + "application/json": { + "schema": {} + } + }, + "description": "Successful Response" + } + }, + "security": [ + { + "APIKeyHeader": [] + } + ], + "summary": "Bespoke Proxy Route", + "tags": [ + "llm_passthrough" + ] + } + }, "/cohere/{endpoint}": { "delete": { "description": "[Docs](https://docs.litellm.ai/docs/pass_through/cohere)", @@ -27761,6 +27785,30 @@ ] } }, + "/laya/v1/systemone": { + "post": { + "operationId": "laya_proxy_route_laya_v1_systemone_post", + "responses": { + "200": { + "content": { + "application/json": { + "schema": {} + } + }, + "description": "Successful Response" + } + }, + "security": [ + { + "APIKeyHeader": [] + } + ], + "summary": "Laya Proxy Route", + "tags": [ + "llm_passthrough" + ] + } + }, "/milvus/{endpoint}": { "delete": { "description": "Enable using Milvus `/vectors` endpoint as a pass-through endpoint.", diff --git a/litellm/proxy/_types.py b/litellm/proxy/_types.py index bb113715d2c..59302e9f08e 100644 --- a/litellm/proxy/_types.py +++ b/litellm/proxy/_types.py @@ -16,6 +16,7 @@ from pydantic import ( JsonValue, PositiveInt, PrivateAttr, + TypeAdapter, field_validator, model_validator, ) @@ -28,6 +29,7 @@ from litellm.litellm_core_utils.initialize_dynamic_callback_params import ( validate_langfuse_span_scope_value, validate_no_callback_env_reference, ) +from litellm.proxy._experimental.mcp_server.stdio_gate import MCP_STDIO_DISABLED_MESSAGE, is_mcp_stdio_enabled from litellm.types.agents import AgentCaller, AgentResponse from litellm.types.integrations.compression_interception import ( CompressionSavingsMetadata, @@ -45,6 +47,8 @@ from litellm.types.mcp import ( MCPCredentials, MCPTransport, MCPTransportType, + MCPUpstreamProtocol, + validate_mcp_protocol_transport, ) from litellm.types.mcp_server.mcp_server_manager import MCPInfo from litellm.types.proxy.agent_identity import ManagedAgentContext @@ -506,6 +510,8 @@ class LiteLLMRoutes(enum.Enum): "/vllm", "/mistral", "/typesafe", + "/laya", + "/bespoke", "/openrouter", "/milvus", "/gigachat", @@ -541,6 +547,7 @@ class LiteLLMRoutes(enum.Enum): "/v1/traces/query/help", "/v1/traces/{trace_id}", "/v1/traces/{trace_id}/spans/{span_id}", + "/v1/traces/{trace_id}/spans/{span_id}/error", ] anthropic_routes = [ @@ -1583,6 +1590,30 @@ def _reject_unsupported_per_server_oauth_discovery(values: object, require_auth_ raise _per_server_oauth_discovery_error() +def _validate_mcp_transport_fields(values: object) -> None: + if not isinstance(values, dict): + return + transport: Final = values.get("transport") + if transport in (MCPTransport.http, MCPTransport.sse): + if not values.get("url") and not values.get("spec_path"): + raise ValueError("url or spec_path is required for HTTP/SSE transport") + return + if transport != MCPTransport.stdio: + return + if not is_mcp_stdio_enabled(): + raise ValueError(MCP_STDIO_DISABLED_MESSAGE) + command: Final = values.get("command") + if not command: + raise ValueError("command is required for stdio transport") + if not values.get("args"): + raise ValueError("args is required for stdio transport") + if os.path.basename(str(command)) not in MCP_STDIO_ALLOWED_COMMANDS: + raise ValueError( + f"Command '{command}' is not in the allowed commands list " + f"for stdio transport. Allowed commands: {sorted(MCP_STDIO_ALLOWED_COMMANDS)}" + ) + + class NewMCPServerRequest(LiteLLMPydanticObjectBase): server_id: str | None = None server_name: str | None = None @@ -1646,26 +1677,20 @@ class NewMCPServerRequest(LiteLLMPydanticObjectBase): description="Server-managed: set by the endpoint; caller values are overridden.", ) + @model_validator(mode="after") + def validate_protocol_transport(self) -> "NewMCPServerRequest": + validate_mcp_protocol_transport( + TypeAdapter[MCPUpstreamProtocol](MCPUpstreamProtocol).validate_python( + (self.mcp_info or {}).get("protocol_version", "auto") + ), + self.transport, + ) + return self + @model_validator(mode="before") @classmethod def validate_transport_fields(cls, values): - if isinstance(values, dict): - transport: Final = values.get("transport") - if transport == MCPTransport.stdio: - if not values.get("command"): - raise ValueError("command is required for stdio transport") - if not values.get("args"): - raise ValueError("args is required for stdio transport") - # Validate command against allowlist to prevent arbitrary execution - base_command: Final = os.path.basename(values["command"]) - if base_command not in MCP_STDIO_ALLOWED_COMMANDS: - raise ValueError( - f"Command '{values['command']}' is not in the allowed commands list " - f"for stdio transport. Allowed commands: {sorted(MCP_STDIO_ALLOWED_COMMANDS)}" - ) - elif transport in [MCPTransport.http, MCPTransport.sse]: - if not values.get("url") and not values.get("spec_path"): - raise ValueError("url or spec_path is required for HTTP/SSE transport") + _validate_mcp_transport_fields(values) return values @model_validator(mode="before") @@ -1745,26 +1770,22 @@ class UpdateMCPServerRequest(LiteLLMPydanticObjectBase): timeout: float | None = None max_concurrent_requests: int | None = None + @model_validator(mode="after") + def validate_protocol_transport(self) -> "UpdateMCPServerRequest": + if not {"transport", "mcp_info"}.issubset(self.model_fields_set): + return self + validate_mcp_protocol_transport( + TypeAdapter[MCPUpstreamProtocol](MCPUpstreamProtocol).validate_python( + (self.mcp_info or {}).get("protocol_version", "auto") + ), + self.transport, + ) + return self + @model_validator(mode="before") @classmethod def validate_transport_fields(cls, values): - if isinstance(values, dict): - transport: Final = values.get("transport") - if transport == MCPTransport.stdio: - if not values.get("command"): - raise ValueError("command is required for stdio transport") - if not values.get("args"): - raise ValueError("args is required for stdio transport") - # Validate command against allowlist to prevent arbitrary execution - base_command: Final = os.path.basename(values["command"]) - if base_command not in MCP_STDIO_ALLOWED_COMMANDS: - raise ValueError( - f"Command '{values['command']}' is not in the allowed commands list " - f"for stdio transport. Allowed commands: {sorted(MCP_STDIO_ALLOWED_COMMANDS)}" - ) - elif transport in [MCPTransport.http, MCPTransport.sse]: - if not values.get("url") and not values.get("spec_path"): - raise ValueError("url or spec_path is required for HTTP/SSE transport") + _validate_mcp_transport_fields(values) return values @model_validator(mode="before") @@ -5470,6 +5491,17 @@ class LiteLLM_JWTAuth(LiteLLMPydanticObjectBase): "'auto_register': auto-create a virtual key and mapping on first encounter." ), ) + auto_register_map_existing_key: bool = Field( + default=False, + description=( + "Only used with unregistered_jwt_client_behavior='auto_register'. When True and the virtual key claim " + "field is the user_id_jwt_field or user_email_jwt_field, the JWT claim is mapped to a virtual key the " + "JWT-resolved user already owns instead of minting a new one. If the user owns several, the most recently created key in the " + "JWT-resolved team (or with no team when the JWT resolves none) is chosen among keys that never " + "expire, are not blocked, are not Admin UI session keys, were not minted by auto_register, and " + "have no allowed_routes or include llm_api_routes. Otherwise a new key is minted as usual." + ), + ) routing_overrides: list[JWTRoutingOverride] | None = Field( default=None, description="Optional claim-based routing overrides for JWT-shaped tokens. Matching rules route requests to oauth2 before default JWT flow.", @@ -5570,6 +5602,15 @@ class LiteLLM_JWTAuth(LiteLLMPydanticObjectBase): return issuer_config.virtual_key_claim_field return self.virtual_key_claim_field + def is_user_identity_claim(self, claim_field: str, issuer: str | None) -> bool: + issuer_config: Final = self.get_issuer_config(issuer) + if issuer_config is None: + return claim_field in (self.user_id_jwt_field, self.user_email_jwt_field) + return claim_field in ( + issuer_config.user_id_jwt_field or self.user_id_jwt_field, + issuer_config.user_email_jwt_field or self.user_email_jwt_field, + ) + def get_unregistered_jwt_client_behavior(self, issuer: str | None) -> UnregisteredJWTClientBehavior: issuer_config: Final = self.get_issuer_config(issuer) if issuer_config is not None and issuer_config.unregistered_jwt_client_behavior is not None: diff --git a/litellm/proxy/auth/auth_utils.py b/litellm/proxy/auth/auth_utils.py index 813d72ed7ae..7ef82009184 100644 --- a/litellm/proxy/auth/auth_utils.py +++ b/litellm/proxy/auth/auth_utils.py @@ -1883,6 +1883,16 @@ def _extract_model_candidates_from_request( llm_router: Router | None = None, team_id: str | None = None, ) -> list[str]: + if route.rstrip("/") in ("/laya/v1/systemone", "/bespoke/v1/systemone"): + from litellm.llms.oss_decision import validate_oss_model + + provider: Final = "bespoke" if route.startswith("/bespoke/") else "laya" + try: + decision_request: Final = TypeAdapter(Mapping[str, object]).validate_python(request_data) + decision_model: Final = validate_oss_model(provider, decision_request.get("model")) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + return _dedupe_model_candidates((f"{provider}/{decision_model}",)) if route == "/cost/predict-cache": prediction_models: Final = _cache_prediction_model_candidates(request_data, llm_router, team_id) # pyright: ignore[reportUnknownArgumentType] # the typed reader validates each deployment ID from this legacy payload return _dedupe_model_candidates(prediction_models) diff --git a/litellm/proxy/auth/authorization.py b/litellm/proxy/auth/authorization.py new file mode 100644 index 00000000000..91549f19d2e --- /dev/null +++ b/litellm/proxy/auth/authorization.py @@ -0,0 +1,77 @@ +from collections.abc import Awaitable, Callable, Iterable, Sequence +from dataclasses import dataclass +from typing import Final, TypeAlias + +from litellm.proxy._types import KeyManagementRoutes, LiteLLM_TeamTable, LitellmUserRoles, UserAPIKeyAuth + + +@dataclass(frozen=True, slots=True) +class AllRows: + """Unrestricted reads, granted by the consuming endpoint's role checks.""" + + +@dataclass(frozen=True, slots=True) +class OwnedRows: + """Rows owned by ``user_id`` or by any of ``team_ids``; a ``None`` user grants no own-user rows.""" + + user_id: str | None + team_ids: tuple[str, ...] = () + + +ReadScope: TypeAlias = AllRows | OwnedRows + + +async def resolve_owned_read_scope( + user_id: str | None, + permitted_team_lookup: Callable[[], Awaitable[Sequence[str]]], +) -> OwnedRows: + """Resolve own-user and permitted-team reads, falling back to own-user on lookup failure.""" + if user_id is None: + return OwnedRows(None) + try: + team_ids: Final = tuple(await permitted_team_lookup()) + except Exception: # noqa: BLE001 # preserve spend-log own-user fallback for every permission lookup failure + return OwnedRows(user_id) + return OwnedRows(user_id, team_ids) + + +def can_read_team_logs(auth: UserAPIKeyAuth, team: LiteLLM_TeamTable) -> bool: + from litellm.proxy.management.teams.access import is_team_admin + from litellm.proxy.management_endpoints.common_utils import ( + _team_member_has_permission, # pyright: ignore[reportPrivateUsage] # reuse existing team permission policy + ) + + return is_team_admin(user_api_key_dict=auth, team_obj=team) or _team_member_has_permission( + user_api_key_dict=auth, + team_obj=team, + permission=KeyManagementRoutes.SPEND_LOGS.value, + ) + + +def permitted_log_team_ids(auth: UserAPIKeyAuth, teams: Iterable[LiteLLM_TeamTable]) -> tuple[str, ...]: + return tuple(team.team_id for team in teams if can_read_team_logs(auth, team)) + + +async def can_read_log_owner( + user_id: str | None, + owner_user: str | None, + owner_team_id: str | None, + team_permission_lookup: Callable[[str], Awaitable[bool]], +) -> bool: + """Authorize stored ownership without swallowing direct team-lookup failures.""" + if owner_user is not None and owner_user == user_id: + return True + if owner_team_id: + return await team_permission_lookup(owner_team_id) + return False + + +async def resolve_trace_read_scope( + auth: UserAPIKeyAuth, + permitted_team_lookup: Callable[[], Awaitable[Sequence[str]]], +) -> ReadScope | None: + if auth.user_role in (LitellmUserRoles.PROXY_ADMIN, LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY): + return AllRows() + if not auth.user_id: + return None + return await resolve_owned_read_scope(auth.user_id, permitted_team_lookup) diff --git a/litellm/proxy/auth/authorization_dependencies.py b/litellm/proxy/auth/authorization_dependencies.py new file mode 100644 index 00000000000..3e7ae75dc86 --- /dev/null +++ b/litellm/proxy/auth/authorization_dependencies.py @@ -0,0 +1,57 @@ +from __future__ import annotations + +from collections.abc import Awaitable, Callable +from functools import partial +from typing import TYPE_CHECKING, Annotated, Final, TypeAlias + +from fastapi import Depends + +from litellm.proxy._types import LiteLLM_TeamTable, UserAPIKeyAuth +from litellm.proxy.auth.authorization import permitted_log_team_ids + +if TYPE_CHECKING: + from litellm.proxy.common_utils.user_api_key_cache import UserApiKeyCache + from litellm.proxy.utils import PrismaClient, ProxyLogging + + +LogTeamLookup: TypeAlias = Callable[[UserAPIKeyAuth], Awaitable[tuple[str, ...]]] + + +async def load_permitted_log_team_ids( + auth: UserAPIKeyAuth, + *, + prisma_client: PrismaClient | None, + user_api_key_cache: UserApiKeyCache, + proxy_logging_obj: ProxyLogging, +) -> tuple[str, ...]: + from litellm.proxy.auth.auth_checks import get_user_object + from litellm.repositories.team_repository import TeamRepository + + if prisma_client is None: + return () + user_obj: Final = await get_user_object( + user_id=auth.user_id, + prisma_client=prisma_client, + user_api_key_cache=user_api_key_cache, + user_id_upsert=False, + proxy_logging_obj=proxy_logging_obj, + ) + if user_obj is None or not user_obj.teams: + return () + team_rows: Final = await TeamRepository(prisma_client).table.find_many(where={"team_id": {"in": user_obj.teams}}) + return permitted_log_team_ids(auth, (LiteLLM_TeamTable.model_validate(row.model_dump()) for row in team_rows)) + + +async def get_log_team_lookup() -> LogTeamLookup: + """Bind infrastructure without performing permission I/O before the handler's checks.""" + from litellm.proxy.proxy_server import prisma_client, proxy_logging_obj, user_api_key_cache + + return partial( + load_permitted_log_team_ids, + prisma_client=prisma_client, + user_api_key_cache=user_api_key_cache, + proxy_logging_obj=proxy_logging_obj, + ) + + +LogTeamLookupDependency: TypeAlias = Annotated[LogTeamLookup, Depends(get_log_team_lookup)] diff --git a/litellm/proxy/auth/route_checks.py b/litellm/proxy/auth/route_checks.py index 2ab76a7a101..6445f0d7b05 100644 --- a/litellm/proxy/auth/route_checks.py +++ b/litellm/proxy/auth/route_checks.py @@ -410,7 +410,7 @@ class RouteChecks: if RouteChecks.check_route_access(route=route, allowed_routes=LiteLLMRoutes.agent_inference_routes.value): return True - if route in LiteLLMRoutes.litellm_native_routes.value: + if RouteChecks.check_route_access(route=route, allowed_routes=LiteLLMRoutes.litellm_native_routes.value): return True # fuzzy match routes like "/v1/threads/thread_49EIN5QF32s4mH20M7GFKdlZ" diff --git a/litellm/proxy/auth/user_api_key_auth.py b/litellm/proxy/auth/user_api_key_auth.py index 3400dccf2a7..e82f3eed7cc 100644 --- a/litellm/proxy/auth/user_api_key_auth.py +++ b/litellm/proxy/auth/user_api_key_auth.py @@ -140,6 +140,7 @@ from litellm.proxy.utils import ( normalize_route_for_root_path, ) from litellm.repositories.table_repositories import TeamMembershipRepository +from litellm.repositories.verification_token_repository import VerificationTokenRepository from litellm.router_utils.common_utils import resolve_model_group_alias from litellm.secret_managers.main import get_secret_bool from litellm.types.services import ServiceTypes @@ -939,6 +940,24 @@ class _PendingAutoRegister(NamedTuple): jwt_issuer: str | None = None +def _claim_identifies_user(jwt_handler: JWTHandler, claim_field: str, jwt_issuer: str | None) -> bool: + if not jwt_handler.litellm_jwtauth.auto_register_map_existing_key: + return False + if jwt_handler.litellm_jwtauth.is_user_identity_claim(claim_field, jwt_issuer): + return True + verbose_proxy_logger.warning( + "JWT Key Mapping (auto_register_map_existing_key): claim '%s' is not the user_id or user_email JWT field " + "and may be shared by several users, so a new key is minted instead of reusing one the user owns.", + claim_field, + ) + return False + + +async def _reusable_key_hash_for_user(prisma_client: PrismaClient, user_id: str, team_id: str | None) -> str | None: + key: Final = await VerificationTokenRepository(prisma_client).find_newest_reusable_llm_api_key(user_id, team_id) + return None if key is None else key.token + + async def _auto_register_jwt_mapping( virtual_key_claim_field: str, claim_value: str, @@ -957,8 +976,10 @@ async def _auto_register_jwt_mapping( ) -> UserAPIKeyAuth | None: """ Auto-register: create a new virtual key + mapping for an unrecognised JWT - claim value. ``team_id`` and ``user_id`` must come from a successful - ``JWTAuthManager.auth_builder`` run — they encode the JWT identity AFTER + claim value, or point the mapping at a key the resolved user already owns + when ``auto_register_map_existing_key`` is set. ``team_id`` and ``user_id`` + must come from a successful ``JWTAuthManager.auth_builder`` run — they + encode the JWT identity AFTER RBAC/scope/custom_validate/email-domain policy has been enforced. The key is stamped with those values so the cached future-request path inherits the same team/user/org limits the auth_builder path would have applied. @@ -974,29 +995,38 @@ async def _auto_register_jwt_mapping( generate_key_helper_fn, ) - # ``table_name="key"`` is required: without it, generate_key_helper_fn - # falls into the user-upsert branch (`table_name is None or "user"`) and - # attempts to insert into LiteLLM_UserTable with user_id=None, which fails - # the NOT NULL @id constraint. Every successful key-creation caller (e.g. - # /key/generate) passes table_name="key" explicitly. - key_data: Final = await generate_key_helper_fn( - llm_router=None, - request_type="key", - table_name="key", - team_id=team_id, - user_id=user_id, - organization_id=org_id, - agent_id=agent_id, - metadata={ - "auto_registered": True, - "jwt_claim_field": virtual_key_claim_field, - "jwt_claim_value": claim_value, - }, + existing_token_hash: Final = ( + await _reusable_key_hash_for_user(prisma_client, user_id, team_id) + if user_id is not None and _claim_identifies_user(jwt_handler, virtual_key_claim_field, jwt_issuer) + else None ) - # generate_key_helper_fn returns the plaintext key in "token"; the persisted - # row in LiteLLM_VerificationToken uses its hash, so hash here to get the FK - # value referenced by LiteLLM_JWTKeyMapping.token. - token_hash = hash_token(key_data["token"]) + minted: Final = existing_token_hash is None + if existing_token_hash is not None: + token_hash = existing_token_hash + else: + # ``table_name="key"`` is required: without it, generate_key_helper_fn + # falls into the user-upsert branch (`table_name is None or "user"`) and + # attempts to insert into LiteLLM_UserTable with user_id=None, which fails + # the NOT NULL @id constraint. Every successful key-creation caller (e.g. + # /key/generate) passes table_name="key" explicitly. + key_data: Final = await generate_key_helper_fn( + llm_router=None, + request_type="key", + table_name="key", + team_id=team_id, + user_id=user_id, + organization_id=org_id, + agent_id=agent_id, + metadata={ + "auto_registered": True, + "jwt_claim_field": virtual_key_claim_field, + "jwt_claim_value": claim_value, + }, + ) + # generate_key_helper_fn returns the plaintext key in "token"; the persisted + # row in LiteLLM_VerificationToken uses its hash, so hash here to get the FK + # value referenced by LiteLLM_JWTKeyMapping.token. + token_hash = hash_token(key_data["token"]) try: await prisma_client.db.litellm_jwtkeymapping.create( @@ -1023,15 +1053,16 @@ async def _auto_register_jwt_mapping( virtual_key_claim_field, claim_value, ) - try: - await prisma_client.db.litellm_verificationtoken.delete(where={"token": token_hash}) - except Exception as delete_err: - # Don't fail the request if cleanup fails — the orphan is - # unmapped and inert. Log so an operator can prune it later. - verbose_proxy_logger.warning( - "JWT Key Mapping (auto_register): failed to delete orphaned key after race: %s", - delete_err, - ) + if minted: + try: + await prisma_client.db.litellm_verificationtoken.delete(where={"token": token_hash}) + except Exception as delete_err: + # Don't fail the request if cleanup fails — the orphan is + # unmapped and inert. Log so an operator can prune it later. + verbose_proxy_logger.warning( + "JWT Key Mapping (auto_register): failed to delete orphaned key after race: %s", + delete_err, + ) token_hash = await get_jwt_key_mapping_object( jwt_claim_name=virtual_key_claim_field, jwt_claim_value=claim_value, @@ -1061,7 +1092,8 @@ async def _auto_register_jwt_mapping( ) verbose_proxy_logger.info( - "JWT Key Mapping (auto_register): created new virtual key for %s='%s'.", + "JWT Key Mapping (auto_register): %s virtual key for %s='%s'.", + "created new" if minted else "mapped existing", virtual_key_claim_field, claim_value, ) @@ -1075,7 +1107,8 @@ async def _auto_register_jwt_mapping( ).resolve(hashed_token=token_hash) ) if auto_registered_key is not None: - auto_registered_key.org_id = org_id + if minted: + auto_registered_key.org_id = org_id auto_registered_key.end_user_id = end_user_id auto_registered_key.api_key = auto_registered_key.token return auto_registered_key @@ -1771,8 +1804,8 @@ async def _user_api_key_auth_builder( # mapping + virtual key from the *validated* identity, then # replace valid_token with the new key so downstream checks # use the key-scoped path. - if pending_auto_register is not None and prisma_client is not None: - auto_registered: Final = await _auto_register_jwt_mapping( + auto_registered: Final = ( + await _auto_register_jwt_mapping( virtual_key_claim_field=pending_auto_register.claim_field, claim_value=pending_auto_register.claim_value, jwt_handler=jwt_handler, @@ -1788,72 +1821,81 @@ async def _user_api_key_auth_builder( end_user_id=end_user_id, agent_id=agent_id, ) - if auto_registered is not None: - auto_registered.jwt_claims = jwt_claims - auto_registered.user_email = user_email - # The auto-registered token is built from the new key's - # columns, which carry no user budget. Carry over the - # already-loaded user row rather than re-reading it, or - # the budget check below has nothing to enforce. - auto_registered.user_model_max_budget = ( - user_object.model_max_budget if user_object is not None else None - ) - valid_token = auto_registered - api_key = valid_token.token or "" - - # Check if model has zero cost - if so, skip all budget checks - model = _get_model_from_request_context( - request_data=request_data, - route=route, - request=request, - llm_router=llm_router, - team_id=valid_token.team_id, + if pending_auto_register is not None and prisma_client is not None + else None ) - skip_budget_checks = False - if model is not None and llm_router is not None: - from litellm.proxy.auth.auth_checks import _is_model_cost_zero - - skip_budget_checks = _is_model_cost_zero(model=model, llm_router=llm_router) - if skip_budget_checks: - verbose_proxy_logger.info("Skipping all budget checks for zero-cost model: %s", model) - - # Fetch project object for JWT path if project_id is set - _jwt_project_obj = None - if valid_token.project_id is not None: - _jwt_project_obj = await get_project_object( - project_id=valid_token.project_id, - prisma_client=prisma_client, - user_api_key_cache=user_api_key_cache, - proxy_logging_obj=proxy_logging_obj, + if auto_registered is not None: + auto_registered.jwt_claims = jwt_claims + auto_registered.user_email = user_email + # The auto-registered token is built from the new key's + # columns, which carry no user budget. Carry over the + # already-loaded user row rather than re-reading it, or + # the budget check below has nothing to enforce. + auto_registered.user_model_max_budget = ( + user_object.model_max_budget if user_object is not None else None ) - if _jwt_project_obj is not None: - valid_token.project_metadata = _jwt_project_obj.metadata - valid_token.project_alias = _jwt_project_obj.project_alias + valid_token = auto_registered + api_key = valid_token.token or "" - # JWT auth returns here rather than falling through to the - # virtual-key checks below, so the user's per-model budget - # has to be enforced on this path too. Without it the - # post-call increment still charges the counter and nothing - # ever reads it, which is worse than not tracking at all. - # Guarded by the same flag the virtual-key path uses, or a - # zero-cost model would be refused here and allowed there, - # while the log above claims all budget checks were skipped. - if not skip_budget_checks: - await _check_user_model_budget( - valid_token=cast(UserAPIKeyAuth, valid_token), - model_max_budget_limiter=model_max_budget_limiter, - models=_get_model_names_for_budget_checks( - model=_get_model_from_request_context( - request_data=request_data, - route=route, - request=request, - llm_router=llm_router, - team_id=valid_token.team_id, - ) - ), + falls_through_to_key_checks: Final = ( + auto_registered is not None + and jwt_handler.litellm_jwtauth.auto_register_map_existing_key + and master_key is not None + ) + if not falls_through_to_key_checks: + # Check if model has zero cost - if so, skip all budget checks + model = _get_model_from_request_context( + request_data=request_data, + route=route, + request=request, + llm_router=llm_router, + team_id=valid_token.team_id, ) + skip_budget_checks = False + if model is not None and llm_router is not None: + from litellm.proxy.auth.auth_checks import _is_model_cost_zero - return cast(UserAPIKeyAuth, valid_token) + skip_budget_checks = _is_model_cost_zero(model=model, llm_router=llm_router) + if skip_budget_checks: + verbose_proxy_logger.info("Skipping all budget checks for zero-cost model: %s", model) + + # Fetch project object for JWT path if project_id is set + _jwt_project_obj = None + if valid_token.project_id is not None: + _jwt_project_obj = await get_project_object( + project_id=valid_token.project_id, + prisma_client=prisma_client, + user_api_key_cache=user_api_key_cache, + proxy_logging_obj=proxy_logging_obj, + ) + if _jwt_project_obj is not None: + valid_token.project_metadata = _jwt_project_obj.metadata + valid_token.project_alias = _jwt_project_obj.project_alias + + # JWT auth returns here rather than falling through to the + # virtual-key checks below, so the user's per-model budget + # has to be enforced on this path too. Without it the + # post-call increment still charges the counter and nothing + # ever reads it, which is worse than not tracking at all. + # Guarded by the same flag the virtual-key path uses, or a + # zero-cost model would be refused here and allowed there, + # while the log above claims all budget checks were skipped. + if not skip_budget_checks: + await _check_user_model_budget( + valid_token=cast(UserAPIKeyAuth, valid_token), + model_max_budget_limiter=model_max_budget_limiter, + models=_get_model_names_for_budget_checks( + model=_get_model_from_request_context( + request_data=request_data, + route=route, + request=request, + llm_router=llm_router, + team_id=valid_token.team_id, + ) + ), + ) + + return cast(UserAPIKeyAuth, valid_token) #### ELSE #### ## CHECK PASS-THROUGH ENDPOINTS ## diff --git a/litellm/proxy/common_utils/registry_read_through.py b/litellm/proxy/common_utils/registry_read_through.py index 8a82e253c5c..32beb0b7447 100644 --- a/litellm/proxy/common_utils/registry_read_through.py +++ b/litellm/proxy/common_utils/registry_read_through.py @@ -36,6 +36,7 @@ READ_THROUGH_MAX_RESYNCS_PER_WINDOW: Final = 20 class RegistryReadThrough: __slots__ = ( + "_is_loaded", "_lock", "_max_resyncs_per_window", "_miss_ttl_seconds", @@ -49,11 +50,13 @@ class RegistryReadThrough: def __init__( self, resync: Callable[[str], Awaitable[bool]], + is_loaded: Callable[[str], bool], miss_ttl_seconds: float = READ_THROUGH_MISS_TTL_SECONDS, max_resyncs_per_window: int = READ_THROUGH_MAX_RESYNCS_PER_WINDOW, resync_window_seconds: float = READ_THROUGH_RESYNC_WINDOW_SECONDS, ) -> None: self._resync = resync + self._is_loaded = is_loaded self._miss_ttl_seconds = miss_ttl_seconds self._max_resyncs_per_window = max_resyncs_per_window self._resync_window_seconds = resync_window_seconds @@ -78,6 +81,8 @@ class RegistryReadThrough: async with self._lock: if self._recent_misses.get_cache(key) is not None: return False + if self._is_loaded(key): + return True if not self._consume_resync_budget(): verbose_proxy_logger.warning( "registry read-through for %r skipped: resync budget of %s per %ss exhausted", @@ -190,9 +195,26 @@ async def _resync_agents(agent_id_or_name: str) -> bool: return True -model_registry_read_through: Final = RegistryReadThrough(resync=_resync_model_deployments) -guardrail_registry_read_through: Final = RegistryReadThrough(resync=_resync_guardrails) -agent_registry_read_through: Final = RegistryReadThrough(resync=_resync_agents) +def _model_is_loaded(model_name_or_id: str) -> bool: + from litellm.proxy import proxy_server + + router: Final = proxy_server.llm_router + if router is None: + return False + return model_name_or_id in router.model_names or router.has_model_id(model_name_or_id) + + +def _guardrail_is_loaded(guardrail_name: str) -> bool: + return _initialized_guardrail(guardrail_name) is not None + + +def _agent_is_loaded(agent_id_or_name: str) -> bool: + return _agent_from_registry(agent_id_or_name) is not None + + +model_registry_read_through: Final = RegistryReadThrough(resync=_resync_model_deployments, is_loaded=_model_is_loaded) +guardrail_registry_read_through: Final = RegistryReadThrough(resync=_resync_guardrails, is_loaded=_guardrail_is_loaded) +agent_registry_read_through: Final = RegistryReadThrough(resync=_resync_agents, is_loaded=_agent_is_loaded) def _agent_from_registry(agent_id_or_name: str) -> "AgentResponse | None": diff --git a/litellm/proxy/db/autorouter_session_rollup.py b/litellm/proxy/db/autorouter_session_rollup.py index b762a40f344..cdc949a6dca 100644 --- a/litellm/proxy/db/autorouter_session_rollup.py +++ b/litellm/proxy/db/autorouter_session_rollup.py @@ -4,11 +4,12 @@ Per-session auto-router benchmarks rollup. At request time the spend writer builds one AutoRouterTurnTransaction per successful auto-routed request (a request whose metadata carries a routing_decision) and queues it on the prisma client. The spend-log flush job drains the queue into -key and user session rollups with one atomic statement per turn: each upsert classifies +key and user session rollups, plus the per-day router rollup, with one atomic statement +per turn: each upsert classifies the turn (same model, first visit, return to a model the session already used, out of order) against the row's own columns, so nothing is read before the write and concurrent -pods compose. The benchmarks endpoint aggregates these rows and can recover matching historical -costs from retained spend logs when estimate coverage predates these columns. +pods compose. The benchmarks endpoint reads session shape from the session rows and money from the +day rows, so spend and savings count only requests on the selected UTC days. """ from __future__ import annotations @@ -71,45 +72,82 @@ tier_maps AS ( GROUP BY router_name, router_type, kv.key ) per_tier GROUP BY router_name, router_type +), +sessions AS ( + SELECT + router_name, + router_type, + COUNT(*)::int AS sessions, + SUM(turns)::int AS session_turns, + SUM(unordered_turns)::int AS unordered_turns, + SUM(covered_turns)::int AS covered_turns, + SUM(cache_hits)::int AS cache_hits, + SUM(same_model_turns)::int AS same_model_turns, + SUM(same_model_hits)::int AS same_model_hits, + SUM(first_visit_turns)::int AS first_visit_turns, + SUM(first_visit_hits)::int AS first_visit_hits, + SUM(return_turns)::int AS return_turns, + SUM(return_hits)::int AS return_hits, + SUM(return_expired_misses)::int AS return_expired_misses, + SUM(return_within_ttl_misses)::int AS return_within_ttl_misses, + SUM(ttl_5m_turns)::int AS ttl_5m_turns, + SUM(ttl_1h_turns)::int AS ttl_1h_turns, + SUM(total_tokens)::bigint AS total_tokens, + SUM(EXTRACT(EPOCH FROM (last_turn_at - first_turn_at)))::float8 AS session_seconds + FROM windowed + GROUP BY router_name, router_type +), +days AS ( + SELECT + router_name, + router_type, + SUM(turns)::int AS turns, + SUM(spend)::float8 AS spend, + SUM(saved_spend)::float8 AS saved_spend, + SUM(savings_estimated_turns)::int AS savings_estimated_turns, + SUM(savings_estimated_actual_spend)::float8 AS savings_estimated_actual_spend, + SUM(savings_estimated_saved_spend)::float8 AS savings_estimated_saved_spend, + SUM(classifier_cost)::float8 AS classifier_cost, + SUM(classifier_cost_recorded_turns)::int AS classifier_cost_recorded_turns + FROM "LiteLLM_AutoRouterDailySpend" + WHERE date >= $5 AND date <= $6 + AND ($3::text IS NULL OR api_key = $3::text) + AND ($4::text IS NULL OR user_id = $4::text) + GROUP BY router_name, router_type ) -SELECT - agg.*, - COALESCE(tier_maps.tier_turns, '{{}}'::jsonb) AS tier_turns -FROM ( SELECT router_name, router_type, - COUNT(*)::int AS sessions, - COALESCE(SUM(turns), 0)::int AS turns, - COALESCE(SUM(unordered_turns), 0)::int AS unordered_turns, - COALESCE(SUM(covered_turns), 0)::int AS covered_turns, - COALESCE(SUM(cache_hits), 0)::int AS cache_hits, - COALESCE(SUM(same_model_turns), 0)::int AS same_model_turns, - COALESCE(SUM(same_model_hits), 0)::int AS same_model_hits, - COALESCE(SUM(first_visit_turns), 0)::int AS first_visit_turns, - COALESCE(SUM(first_visit_hits), 0)::int AS first_visit_hits, - COALESCE(SUM(return_turns), 0)::int AS return_turns, - COALESCE(SUM(return_hits), 0)::int AS return_hits, - COALESCE(SUM(return_expired_misses), 0)::int AS return_expired_misses, - COALESCE(SUM(return_within_ttl_misses), 0)::int AS return_within_ttl_misses, - COALESCE(SUM(ttl_5m_turns), 0)::int AS ttl_5m_turns, - COALESCE(SUM(ttl_1h_turns), 0)::int AS ttl_1h_turns, - COALESCE(SUM(total_tokens), 0)::bigint AS total_tokens, - COALESCE(SUM(spend), 0)::float8 AS spend, - COALESCE(SUM(saved_spend), 0)::float8 AS saved_spend, - COALESCE(SUM(savings_estimated_turns), 0)::int AS savings_estimated_turns, - COALESCE(SUM(savings_estimated_actual_spend), 0)::float8 AS savings_estimated_actual_spend, - CASE WHEN BOOL_AND(savings_estimated_turns = turns AND classifier_cost_recorded_turns = turns) - THEN SUM(classifier_cost)::float8 END AS savings_estimated_classifier_cost, - COALESCE(SUM(savings_estimated_saved_spend), 0)::float8 AS savings_estimated_saved_spend, - COALESCE(SUM(classifier_cost), 0)::float8 AS classifier_cost, - COALESCE(SUM(classifier_cost_recorded_turns), 0)::int AS classifier_cost_recorded_turns, - COALESCE(SUM(EXTRACT(EPOCH FROM (last_turn_at - first_turn_at))), 0)::float8 AS session_seconds -FROM windowed -GROUP BY router_name, router_type -) agg + COALESCE(tier_maps.tier_turns, '{{}}'::jsonb) AS tier_turns, + COALESCE(sessions.sessions, 0) AS sessions, + COALESCE(sessions.session_turns, 0) AS session_turns, + COALESCE(sessions.unordered_turns, 0) AS unordered_turns, + COALESCE(sessions.covered_turns, 0) AS covered_turns, + COALESCE(sessions.cache_hits, 0) AS cache_hits, + COALESCE(sessions.same_model_turns, 0) AS same_model_turns, + COALESCE(sessions.same_model_hits, 0) AS same_model_hits, + COALESCE(sessions.first_visit_turns, 0) AS first_visit_turns, + COALESCE(sessions.first_visit_hits, 0) AS first_visit_hits, + COALESCE(sessions.return_turns, 0) AS return_turns, + COALESCE(sessions.return_hits, 0) AS return_hits, + COALESCE(sessions.return_expired_misses, 0) AS return_expired_misses, + COALESCE(sessions.return_within_ttl_misses, 0) AS return_within_ttl_misses, + COALESCE(sessions.ttl_5m_turns, 0) AS ttl_5m_turns, + COALESCE(sessions.ttl_1h_turns, 0) AS ttl_1h_turns, + COALESCE(sessions.total_tokens, 0) AS total_tokens, + COALESCE(sessions.session_seconds, 0) AS session_seconds, + COALESCE(days.turns, 0) AS turns, + COALESCE(days.spend, 0) AS spend, + COALESCE(days.saved_spend, 0) AS saved_spend, + COALESCE(days.savings_estimated_turns, 0) AS savings_estimated_turns, + COALESCE(days.savings_estimated_actual_spend, 0) AS savings_estimated_actual_spend, + COALESCE(days.savings_estimated_saved_spend, 0) AS savings_estimated_saved_spend, + COALESCE(days.classifier_cost, 0) AS classifier_cost, + COALESCE(days.classifier_cost_recorded_turns, 0) AS classifier_cost_recorded_turns +FROM sessions +FULL OUTER JOIN days USING (router_name, router_type) LEFT JOIN tier_maps USING (router_name, router_type) -ORDER BY agg.spend DESC +ORDER BY spend DESC, router_name, router_type """ @@ -391,15 +429,43 @@ ON CONFLICT ({user_column}api_key, session_id, router_name) DO UPDATE SET """ +_DAY_UPSERT_SQL: Final = f""" +day_rollup AS ( + INSERT INTO "LiteLLM_AutoRouterDailySpend" AS d ( + date, api_key, user_id, router_name, router_type, turns, spend, saved_spend, savings_estimated_turns, + savings_estimated_actual_spend, savings_estimated_saved_spend, classifier_cost, classifier_cost_recorded_turns + ) + VALUES ( + ({_TURN_AT}::timestamp)::date::text, {_p("api_key")}::text, {_p("user_id")}::text, {_p("router_name")}, + {_p("router_type")}, 1, {_p("spend")}::float8, {_p("saved_spend")}::float8, {_p("savings_estimated_turns")}::int, + {_p("savings_estimated_actual_spend")}::float8, {_p("savings_estimated_saved_spend")}::float8, + {_p("classifier_cost")}::float8, 1 + ) + ON CONFLICT (date, api_key, user_id, router_name, router_type) DO UPDATE SET + turns = d.turns + 1, + spend = d.spend + EXCLUDED.spend, + saved_spend = d.saved_spend + EXCLUDED.saved_spend, + savings_estimated_turns = d.savings_estimated_turns + EXCLUDED.savings_estimated_turns, + savings_estimated_actual_spend = d.savings_estimated_actual_spend + EXCLUDED.savings_estimated_actual_spend, + savings_estimated_saved_spend = d.savings_estimated_saved_spend + EXCLUDED.savings_estimated_saved_spend, + classifier_cost = d.classifier_cost + EXCLUDED.classifier_cost, + classifier_cost_recorded_turns = d.classifier_cost_recorded_turns + 1 + RETURNING 1 +) +""" + UPSERT_AUTOROUTER_SESSION_SQL: Final = f""" WITH key_rollup AS ( {_session_upsert_sql(user_scoped=False)} RETURNING 1 -) +), {_DAY_UPSERT_SQL} {_session_upsert_sql(user_scoped=True)} """ -UPSERT_AUTOROUTER_USER_SESSION_SQL: Final = _session_upsert_sql(user_scoped=True) +UPSERT_AUTOROUTER_USER_SESSION_SQL: Final = f""" +WITH {_DAY_UPSERT_SQL} +{_session_upsert_sql(user_scoped=True)} +""" def _as_sql_param(value: str | float | bool | datetime | None) -> str | float | None: diff --git a/litellm/proxy/db/baseline_accounting.py b/litellm/proxy/db/baseline_accounting.py index 05a4a989152..9536f8d740a 100644 --- a/litellm/proxy/db/baseline_accounting.py +++ b/litellm/proxy/db/baseline_accounting.py @@ -179,6 +179,8 @@ class _Change(BaseModel): actual_delta: float savings_delta: float daily: DailyBaselineAttribution | None + date: str | None = None + router_type: str | None = None class _TransactionManager(Protocol): @@ -303,6 +305,26 @@ WHERE {user_match}session.api_key = totals.api_key AND session.session_id = tota _UPDATE_SESSIONS: Final = _session_correction_sql(user_scoped=False) _UPDATE_USER_SESSIONS: Final = _session_correction_sql(user_scoped=True) +_UPDATE_DAYS: Final = """ +WITH totals AS ( + SELECT date, api_key, user_id, router_name, router_type, SUM(covered_delta)::int AS covered_delta, + SUM(actual_delta) AS actual_delta, SUM(savings_delta) AS savings_delta + FROM jsonb_to_recordset($1::jsonb) AS x( + date text, api_key text, user_id text, router_name text, router_type text, + covered_delta int, actual_delta float8, savings_delta float8 + ) + WHERE date IS NOT NULL + GROUP BY date, api_key, user_id, router_name, router_type +) +UPDATE "LiteLLM_AutoRouterDailySpend" AS day +SET saved_spend = day.saved_spend + totals.savings_delta, + savings_estimated_turns = day.savings_estimated_turns + totals.covered_delta, + savings_estimated_actual_spend = day.savings_estimated_actual_spend + totals.actual_delta, + savings_estimated_saved_spend = day.savings_estimated_saved_spend + totals.savings_delta +FROM totals +WHERE day.date = totals.date AND day.api_key = totals.api_key AND day.user_id = totals.user_id + AND day.router_name = totals.router_name AND day.router_type = totals.router_type +""" def _primary_transaction(client: PrismaClient) -> _TransactionManager: @@ -331,6 +353,8 @@ def _change(record: BaselineAccountingRecord, old: BaselinePublication | None, n savings_delta=(current.savings if current is not None else 0.0) - (previous.savings if previous is not None else 0.0), daily=record.daily, + date=record.turn.turn_at.date().isoformat() if record.turn is not None else None, + router_type=record.turn.router_type if record.turn is not None else None, ) @@ -373,6 +397,7 @@ async def _publish(db: SupportsRawQueries, changes: Sequence[_Change]) -> None: await db.execute_raw(_UPDATE_SESSIONS, serialized) if any(change.user_id for change in changes): await db.execute_raw(_UPDATE_USER_SESSIONS, serialized) + await db.execute_raw(_UPDATE_DAYS, serialized) for entity, table in DAILY_SPEND_TABLES.items(): if adjustments := tuple( change.daily.adjustment(target, change.savings_delta, change.request_id) diff --git a/litellm/proxy/db/db_spend_update_writer.py b/litellm/proxy/db/db_spend_update_writer.py index ef5dd3f663c..c64ef72ace6 100644 --- a/litellm/proxy/db/db_spend_update_writer.py +++ b/litellm/proxy/db/db_spend_update_writer.py @@ -70,6 +70,7 @@ from litellm.proxy.db.db_transaction_queue.window_spend_update_queue import ( WindowSpendUpdateQueue, ) from litellm.proxy.db.exception_handler import PrismaDBExceptionHandler +from litellm.proxy.db.model_usage_rollup import build_model_usage_transaction from litellm.proxy.route_llm_request import ROUTE_ENDPOINT_MAPPING from litellm.proxy.spend_tracking.compression_savings import ( extract_compression_saved_tokens, @@ -728,6 +729,20 @@ class DBSpendUpdateWriter: except Exception as e: verbose_proxy_logger.debug("_enqueue_tool_usage_transaction error (non-blocking): %s", e) + async def _enqueue_model_usage_transaction( + self, + payload: SpendLogsPayload, + prisma_client: PrismaClient, + ) -> None: + try: + transaction: Final = build_model_usage_transaction(payload) + if transaction is None: + return + async with prisma_client._model_usage_transactions_lock: + prisma_client.model_usage_transactions.append(transaction) + except Exception as e: + verbose_proxy_logger.debug("_enqueue_model_usage_transaction error (non-blocking): %s", e) + async def _enqueue_autorouter_turn_transaction( self, payload: SpendLogsPayload, @@ -1144,15 +1159,7 @@ class DBSpendUpdateWriter: traceback.format_exc(), ) - try: - from litellm.proxy.db.model_usage_rollup import increment_daily_model_usage - - await increment_daily_model_usage(prisma_client=prisma_client, payload=payload_copy) - except Exception: - verbose_proxy_logger.debug( - "_batch_database_updates: increment_daily_model_usage failed: %s", - traceback.format_exc(), - ) + await self._enqueue_model_usage_transaction(payload=payload_copy, prisma_client=prisma_client) async def _update_key_db( self, diff --git a/litellm/proxy/db/db_transaction_queue/spend_log_cleanup.py b/litellm/proxy/db/db_transaction_queue/spend_log_cleanup.py index 06e4d06fca4..679e286cedd 100644 --- a/litellm/proxy/db/db_transaction_queue/spend_log_cleanup.py +++ b/litellm/proxy/db/db_transaction_queue/spend_log_cleanup.py @@ -540,6 +540,18 @@ class SpendLogCleanup: deadline=deadline, ) + async def _delete_old_autorouter_daily_rows( + self, prisma_client: PrismaClient, cutoff_day: str, deadline: float + ) -> TableCleanupResult: + return await self._delete_old_rows_batched( + prisma_client, + cutoff_day, + table_name="LiteLLM_AutoRouterDailySpend", + key_columns=("date", "api_key", "user_id", "router_name", "router_type"), + time_column="date", + deadline=deadline, + ) + async def _delete_old_health_check_rows( self, prisma_client: PrismaClient, cutoff_date: datetime, deadline: float ) -> TableCleanupResult: @@ -623,16 +635,20 @@ class SpendLogCleanup: except Exception: # noqa: BLE001 # retained observations are retried by the next cleanup job verbose_proxy_logger.warning("Auto-router baseline retention remains pending") sessions_result: Final = await self._delete_old_autorouter_session_rows( - prisma_client, session_cutoff, self._group_deadline(deadline, 2) + prisma_client, session_cutoff, self._group_deadline(deadline, 3) ) verbose_proxy_logger.info("Deleted %s expired auto-router session rollup rows", sessions_result.rows_deleted) user_sessions_result: Final = await self._delete_old_autorouter_user_session_rows( - prisma_client, session_cutoff, deadline + prisma_client, session_cutoff, self._group_deadline(deadline, 2) ) verbose_proxy_logger.info( "Deleted %s expired auto-router user session rollup rows", user_sessions_result.rows_deleted ) - return (sessions_result, user_sessions_result) + days_result: Final = await self._delete_old_autorouter_daily_rows( + prisma_client, session_cutoff.date().isoformat(), deadline + ) + verbose_proxy_logger.info("Deleted %s expired auto-router daily rollup rows", days_result.rows_deleted) + return (sessions_result, user_sessions_result, days_result) async def _clean_health_checks( self, prisma_client: PrismaClient, retention_seconds: int, deadline: float diff --git a/litellm/proxy/db/model_usage_rollup.py b/litellm/proxy/db/model_usage_rollup.py index acd9130da30..98d53573f30 100644 --- a/litellm/proxy/db/model_usage_rollup.py +++ b/litellm/proxy/db/model_usage_rollup.py @@ -1,5 +1,12 @@ +from __future__ import annotations + +import asyncio +import random +from collections.abc import Mapping, Sequence +from dataclasses import dataclass from datetime import datetime -from typing import Final +from itertools import groupby +from typing import TYPE_CHECKING, Final, Protocol from pydantic import TypeAdapter, ValidationError @@ -8,15 +15,48 @@ from litellm.constants import ( MODEL_INSIGHTS_DEFAULT_TASK, MODEL_INSIGHTS_TASK_TAG_PREFIX, ) -from litellm.proxy._types import SpendLogsPayload +from litellm.proxy._types import DB_RETRY_SAFE_ERROR_TYPES, SpendLogsPayload from litellm.proxy.db.model_insights_tasks import load_model_insight_tasks -from litellm.proxy.utils import PrismaClient -from litellm.repositories.table_repositories import DailyModelUsageRepository + +if TYPE_CHECKING: + from litellm.proxy.utils import PrismaClient _METADATA: Final = TypeAdapter(dict[str, object]) _TAGS: Final = TypeAdapter(list[object]) +class _UpsertTable(Protocol): + def upsert(self, *, where: Mapping[str, object], data: Mapping[str, object]) -> None: ... + + +class _ModelUsageBatch(Protocol): + litellm_dailymodelusage: _UpsertTable + + +class _ModelUsageBatchManager(Protocol): + async def __aenter__(self) -> _ModelUsageBatch: ... + + async def __aexit__(self, exc_type: object, exc_value: object, traceback: object) -> bool | None: ... + + +@dataclass(frozen=True, slots=True) +class ModelUsageKey: + date: str + model_group: str + model: str + custom_llm_provider: str + task_type: str + + +@dataclass(frozen=True, slots=True) +class ModelUsageTransaction: + key: ModelUsageKey + spend: float + prompt_tokens: int + completion_tokens: int + successful: bool + + def model_usage_task_type(request_tags: str) -> str: try: tags: Final = _TAGS.validate_json(request_tags) @@ -48,43 +88,88 @@ def _date_from_start_time(start_time: datetime | str) -> str | None: return start_time[:10] if len(start_time) >= 10 else None -async def increment_daily_model_usage(prisma_client: PrismaClient, payload: SpendLogsPayload) -> None: +def build_model_usage_transaction(payload: SpendLogsPayload) -> ModelUsageTransaction | None: date: Final = _date_from_start_time(payload["startTime"]) if date is None or _is_internal_call(payload["metadata"]): - return - + return None model: Final = payload["model"] or "unknown" - model_group: Final = payload["model_group"] or model - provider: Final = payload["custom_llm_provider"] or "unknown" - task_type: Final = model_usage_task_type(payload["request_tags"]) - successful: Final = 1 if payload["status"] == "success" else 0 - failed: Final = 1 - successful - key: Final = { - "date": date, - "model_group": model_group, - "model": model, - "custom_llm_provider": provider, - "task_type": task_type, - } - await DailyModelUsageRepository(prisma_client).table.upsert( - where={"date_model_group_model_custom_llm_provider_task_type": key}, - data={ - "create": { - **key, - "spend": payload["spend"], - "prompt_tokens": payload["prompt_tokens"], - "completion_tokens": payload["completion_tokens"], - "request_count": 1, - "successful_requests": successful, - "failed_requests": failed, - }, - "update": { - "spend": {"increment": payload["spend"]}, - "prompt_tokens": {"increment": payload["prompt_tokens"]}, - "completion_tokens": {"increment": payload["completion_tokens"]}, - "request_count": {"increment": 1}, - "successful_requests": {"increment": successful}, - "failed_requests": {"increment": failed}, - }, - }, + return ModelUsageTransaction( + key=ModelUsageKey( + date=date, + model_group=payload["model_group"] or model, + model=model, + custom_llm_provider=payload["custom_llm_provider"] or "unknown", + task_type=model_usage_task_type(payload["request_tags"]), + ), + spend=payload["spend"], + prompt_tokens=payload["prompt_tokens"], + completion_tokens=payload["completion_tokens"], + successful=payload["status"] == "success", ) + + +def _model_usage_batch(prisma_client: PrismaClient) -> _ModelUsageBatchManager: + batch: Final[_ModelUsageBatchManager] = prisma_client.db.batch_() + return batch + + +def _sort_key(transaction: ModelUsageTransaction) -> tuple[str, str, str, str, str]: + key: Final = transaction.key + return (key.date, key.model_group, key.model, key.custom_llm_provider, key.task_type) + + +async def flush_model_usage_transactions( + prisma_client: PrismaClient, + transactions: Sequence[ModelUsageTransaction], + n_retry_times: int = 3, +) -> None: + """One upsert per rollup row for the whole drained batch, in a single transaction and in key order so + concurrent pods take row locks in the same order. Only ConnectError is retried: it proves nothing reached + the database, while a retry after an ambiguous post-send failure could double-count the increments.""" + if not transactions: + return + ordered: Final = sorted(transactions, key=_sort_key) + for attempt in range(n_retry_times + 1): + try: + async with _model_usage_batch(prisma_client) as batcher: + for key, grouped in groupby(ordered, key=lambda transaction: transaction.key): + entries = tuple(grouped) + spend = sum(entry.spend for entry in entries) + prompt_tokens = sum(entry.prompt_tokens for entry in entries) + completion_tokens = sum(entry.completion_tokens for entry in entries) + successful = sum(1 for entry in entries if entry.successful) + failed = len(entries) - successful + key_fields = { + "date": key.date, + "model_group": key.model_group, + "model": key.model, + "custom_llm_provider": key.custom_llm_provider, + "task_type": key.task_type, + } + batcher.litellm_dailymodelusage.upsert( + where={"date_model_group_model_custom_llm_provider_task_type": key_fields}, + data={ + "create": { + **key_fields, + "spend": spend, + "prompt_tokens": prompt_tokens, + "completion_tokens": completion_tokens, + "request_count": len(entries), + "successful_requests": successful, + "failed_requests": failed, + }, + "update": { + "spend": {"increment": spend}, + "prompt_tokens": {"increment": prompt_tokens}, + "completion_tokens": {"increment": completion_tokens}, + "request_count": {"increment": len(entries)}, + "successful_requests": {"increment": successful}, + "failed_requests": {"increment": failed}, + }, + }, + ) + return + except DB_RETRY_SAFE_ERROR_TYPES: + if attempt >= n_retry_times: + raise + await asyncio.sleep(2.0**attempt + random.uniform(0, 1)) diff --git a/litellm/proxy/discovery_endpoints/ui_discovery_endpoints.py b/litellm/proxy/discovery_endpoints/ui_discovery_endpoints.py index 8b042d18cd0..4c85d6148c0 100644 --- a/litellm/proxy/discovery_endpoints/ui_discovery_endpoints.py +++ b/litellm/proxy/discovery_endpoints/ui_discovery_endpoints.py @@ -4,6 +4,7 @@ from typing import Final from fastapi import APIRouter +from litellm.proxy._experimental.mcp_server.stdio_gate import is_mcp_stdio_enabled from litellm.proxy.common_utils.html_forms.default_credentials_hint import should_hide_default_credentials_hint from litellm.types.proxy.discovery_endpoints.ui_discovery_endpoints import ( UiDiscoveryEndpoints, @@ -41,4 +42,5 @@ async def get_ui_config(): hide_default_credentials_hint=hide_default_credentials_hint, is_control_plane=is_control_plane, workers=proxy_config.worker_registry if is_control_plane else [], + mcp_stdio_enabled=is_mcp_stdio_enabled(), ) diff --git a/litellm/proxy/lens/endpoints.py b/litellm/proxy/lens/endpoints.py index 349ecb9a353..dc0ae8c985c 100644 --- a/litellm/proxy/lens/endpoints.py +++ b/litellm/proxy/lens/endpoints.py @@ -9,12 +9,15 @@ from uuid import uuid4 from fastapi import APIRouter, Depends, HTTPException, Query, Request from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer -from pydantic import AwareDatetime, BaseModel, Field, TypeAdapter +from pydantic import AwareDatetime, BaseModel, Field -from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth +from litellm.proxy._types import LitellmUserRoles, ModelAccessDeniedProxyException, UserAPIKeyAuth +from litellm.proxy.auth.auth_checks import can_key_call_model +from litellm.proxy.auth.resolvers.exceptions import KeyNotFoundError from litellm.proxy.auth.user_api_key_auth import user_api_key_auth from litellm.proxy.db.routing_prisma_wrapper import writer_wrapper from litellm.proxy.lens.billing import validate_key +from litellm.proxy.lens.inference import Deployment, deployment_prices from litellm.proxy.lens.models import ( Claim, Execution, @@ -127,20 +130,60 @@ def validate_selection(settings: LensSettings) -> None: raise HTTPException(422, "Choose execution IDs returned by the activity preview") -def validate_model(settings: LensSettings, auth: UserAPIKeyAuth) -> None: - from litellm.proxy.proxy_server import llm_router +async def validate_model(settings: LensSettings, auth: UserAPIKeyAuth) -> None: + from litellm.proxy.proxy_server import llm_router, prisma_client validate_selection(settings) - if llm_router is None or settings.model not in llm_router.get_model_names(team_id=auth.team_id): + deployments: Final = ( + llm_router.get_model_list(model_name=settings.model, team_id=auth.team_id) if llm_router else () + ) + if not deployments: raise HTTPException(400, "Choose a model configured on this LiteLLM instance") - allowed_models: Final = TypeAdapter(tuple[str, ...]).validate_python(auth.model_dump().get("models") or ()) - if ( - auth.user_role != LitellmUserRoles.PROXY_ADMIN - and allowed_models - and settings.model not in allowed_models - and "all-proxy-models" not in allowed_models - ): - raise HTTPException(403, "This key does not have access to the analysis model") + if auth.user_role != LitellmUserRoles.PROXY_ADMIN: + try: + await can_key_call_model( + model=settings.model, + llm_model_list=deployments, + valid_token=auth, + llm_router=llm_router, + prisma_client=prisma_client, + ) + except ModelAccessDeniedProxyException as exc: + raise HTTPException(403, "This key does not have access to the analysis model") from exc + for deployment in deployments: + deployment_prices(Deployment.model_validate(deployment)) + + +async def worker_supports_model(worker: Worker, settings: LensSettings) -> bool: + if worker.revoked or worker.analysis_key_id is None: + return False + try: + auth: Final = await validate_key(worker.analysis_key_id) + if auth is None: + return False + await validate_model(settings, auth) + except KeyNotFoundError: + return False + except HTTPException as exc: + if exc.status_code not in (400, 401, 403): + raise + return False + return True + + +async def validate_workers(settings: LensSettings, scope: Scope) -> None: + workers: Final = repository().eligible_workers(scope) + first: Final = await anext(workers, None) + if first is None or await worker_supports_model(first, settings): + return + async for worker in workers: + if await worker_supports_model(worker, settings): + return + raise HTTPException( + 400, + "No worker can use this analysis model. Choose a model available to the worker's virtual key, " + "or update its model access and pricing.", + ) @router.get("", response_model=LensList) @@ -156,7 +199,8 @@ async def list_lenses(auth: Auth, storage: StorageDep) -> LensList: @router.post("", response_model=Lens) async def create_lens(settings: LensSettings, auth: Auth) -> Lens: scope: Final = user_scope(auth, write=True) - validate_model(settings, auth) + await validate_model(settings, auth) + await validate_workers(settings, scope) now: Final = datetime.now(timezone.utc) lens: Final = Lens( id=str(uuid4()), @@ -183,8 +227,10 @@ async def list_agents(auth: Auth, storage: StorageDep) -> tuple[str, ...]: @router.put("/{lens_id}", response_model=Lens) async def update_lens(lens_id: str, settings: LensSettings, auth: Auth) -> Lens: - await get_lens(lens_id, user_scope(auth, write=True)) - validate_model(settings, auth) + lens: Final = await get_lens(lens_id, user_scope(auth, write=True)) + validate_selection(settings) + if settings.model != lens.settings.model or (settings.enabled and not lens.settings.enabled): + await validate_model(settings, auth) return required( await repository().update( lens_id, @@ -202,9 +248,10 @@ async def update_lens(lens_id: str, settings: LensSettings, auth: Auth) -> Lens: @router.post("/{lens_id}/runs", response_model=Lens) async def run_lens(lens_id: str, body: RunRequest, auth: Auth) -> Lens: - await get_lens(lens_id, user_scope(auth, write=True)) - if body.settings is not None: - validate_model(body.settings, auth) + lens: Final = await get_lens(lens_id, user_scope(auth, write=True)) + settings: Final = body.settings or lens.settings + await validate_model(settings, auth) + await validate_workers(settings, lens.scope) now: Final = datetime.now(timezone.utc) job_id: Final = str(uuid4()) return required( @@ -528,10 +575,16 @@ async def heartbeat(lens_id: str, job_id: str, worker: WorkerAuth) -> bool: async def claim_candidate(candidate: Lens, worker: Worker, now: datetime) -> Claim | None: + active: Final = current_job(candidate) + if not await worker_supports_model(worker, active.settings if active else candidate.settings): + return None job_id: Final = str(uuid4()) def schedule(e: Lens) -> Lens: scheduled: Final = queue_job(e, now, job_id) if e.settings.enabled and e.next_run_at <= now else e + job: Final = current_job(scheduled) + if job and job.settings.model != (active.settings.model if active else candidate.settings.model): + return e return claim_job(scheduled, worker, now) updated: Final = await repository().update(candidate.id, schedule, changed_only=True) diff --git a/litellm/proxy/lens/inference.py b/litellm/proxy/lens/inference.py index 8b306932931..b8a9d7754ae 100644 --- a/litellm/proxy/lens/inference.py +++ b/litellm/proxy/lens/inference.py @@ -3,9 +3,10 @@ from types import MappingProxyType from typing import Final from fastapi import HTTPException, Request -from pydantic import BaseModel, ConfigDict, Field +from pydantic import BaseModel, ConfigDict, Field, field_validator import litellm +from litellm.exceptions import ModelNotMappedError from litellm.integrations.clickhouse.context import lens_analysis from litellm.litellm_core_utils.initialize_dynamic_callback_params import inherit_message_logging_privacy from litellm.proxy.lens.billing import complete, validate_key @@ -59,6 +60,17 @@ class Prices(BaseModel): input_cost_per_token_above_128k_tokens: float = 0 output_cost_per_token_above_128k_tokens: float = 0 + @field_validator( + "input_cost_per_token_above_200k_tokens", + "output_cost_per_token_above_200k_tokens", + "input_cost_per_token_above_128k_tokens", + "output_cost_per_token_above_128k_tokens", + mode="before", + ) + @classmethod + def missing_tier_rate(cls, value: object) -> object: + return 0 if value is None else value + def deployment_prices(deployment: Deployment) -> Prices: params: Final = deployment.litellm_params @@ -66,7 +78,14 @@ def deployment_prices(deployment: Deployment) -> Prices: return Prices( input_cost_per_token=params.input_cost_per_token, output_cost_per_token=params.output_cost_per_token ) - return Prices.model_validate(litellm.get_model_info(model=params.model)) + try: + return Prices.model_validate(litellm.get_model_info(model=params.model)) + except (ModelNotMappedError, ValueError) as exc: + raise HTTPException( + 400, + f"Pricing is not configured for {params.model}. Set input_cost_per_token and output_cost_per_token " + "on its deployment before running an investigation.", + ) from exc def quote(deployments: tuple[Deployment, ...], prompt: str) -> float: diff --git a/litellm/proxy/lens/repository.py b/litellm/proxy/lens/repository.py index 4aa840e181b..6e1e2da112a 100644 --- a/litellm/proxy/lens/repository.py +++ b/litellm/proxy/lens/repository.py @@ -1,11 +1,12 @@ -from collections.abc import Awaitable, Callable +import json +from collections.abc import AsyncIterator, Awaitable, Callable from types import MappingProxyType from typing import Final, Protocol from pydantic import BaseModel, JsonValue, TypeAdapter from litellm.proxy.db.prisma_client import PrismaWrapper -from litellm.proxy.lens.models import Job, Lens, Worker +from litellm.proxy.lens.models import Job, Lens, Scope, Worker class Database(Protocol): @@ -117,6 +118,33 @@ class LensRepository: rows: Final = _ROWS.validate_python(await self.db.query_raw('SELECT data FROM "LiteLLM_LensWorker"')) return tuple(Worker.model_validate(row.data) for row in rows) + async def eligible_workers(self, scope: Scope) -> AsyncIterator[Worker]: + scoped: Final = ( + {"all_teams": True} + if scope.all_teams + else {"team_id": scope.team_id} + if scope.team_id + else {"team_id": "", "api_key_hash": scope.api_key_hash} + ) + cursor = "" # rebind-ok: advance the keyset cursor after each bounded page + while True: + rows = _ROWS.validate_python( + await self.db.query_raw( + """SELECT data FROM "LiteLLM_LensWorker" + WHERE data @> '{"revoked": false}'::jsonb AND id > $1 + AND (data->'scope' @> '{"all_teams": true}'::jsonb OR data->'scope' @> $2::jsonb) + ORDER BY id LIMIT 50""", + cursor, + json.dumps(scoped), + ) + ) + workers = tuple(Worker.model_validate(row.data) for row in rows) + for worker in workers: + yield worker + if len(workers) < 50: + return + cursor = workers[-1].id + async def worker(self, token_hash: str) -> Worker | None: rows: Final = _ROWS.validate_python( await self.db.query_raw( diff --git a/litellm/proxy/lens/sources.py b/litellm/proxy/lens/sources.py index 17550cc4aab..3313cd2ded2 100644 --- a/litellm/proxy/lens/sources.py +++ b/litellm/proxy/lens/sources.py @@ -1,10 +1,9 @@ import base64 import json -from collections.abc import Awaitable, Mapping -from types import MappingProxyType -from typing import Final, Literal, Protocol +from collections.abc import Awaitable, Sequence +from typing import Final, Protocol, TypeAlias -from pydantic import BaseModel, TypeAdapter +from pydantic import TypeAdapter from litellm.proxy.lens.models import ( Evidence, @@ -16,59 +15,29 @@ from litellm.proxy.lens.models import ( Scope, TracePart, ) - - -class ActivityAvailability(BaseModel): - traces: bool = False - requests: bool = False +from litellm.rust_bridge.trace_queries import ( + ActivityAvailability, + AgentRow, + CountRow, + ExecutionRow, + LensAccessParams, + LensContentParams, + LensEvidenceParams, + LensSampleParams, + PartRow, +) class Storage(Protocol): - def lens_availability(self, parameters: Mapping[str, object]) -> Awaitable[object]: ... - def lens_agents(self, parameters: Mapping[str, object]) -> Awaitable[object]: ... - def lens_sample(self, parameters: Mapping[str, object]) -> Awaitable[object]: ... - def lens_content(self, parameters: Mapping[str, object]) -> Awaitable[object]: ... - def lens_evidence(self, parameters: Mapping[str, object]) -> Awaitable[object]: ... + def lens_availability(self, parameters: LensAccessParams) -> Awaitable[Sequence[ActivityAvailability]]: ... + def lens_agents(self, parameters: LensAccessParams) -> Awaitable[Sequence[AgentRow]]: ... + def lens_sample(self, parameters: LensSampleParams) -> Awaitable[Sequence[ExecutionRow]]: ... + def lens_content(self, parameters: LensContentParams) -> Awaitable[Sequence[PartRow]]: ... + def lens_evidence(self, parameters: LensEvidenceParams) -> Awaitable[Sequence[CountRow]]: ... -class ExecutionRow(BaseModel): - selection_key: str = "" - source: Literal["traces", "requests"] - trace_id: str - trace_ref: str = "" - team_id: str - name: str - start_time: str - span_count: int - root_seen: int - eligible: int - selected: int = 0 - service: str = "" - attributes: tuple[tuple[str, str], ...] = () - - -class PartRow(BaseModel): - span_id: str - parent_span_id: str - name: str - kind: str - content: str - truncated: int - - -class CountRow(BaseModel): - count: int - - -class AgentRow(BaseModel): - agent_name: str - - -_AVAILABILITY: Final = TypeAdapter(tuple[ActivityAvailability, ...]) -_AGENTS: Final = TypeAdapter(tuple[AgentRow, ...]) -_ROWS: Final = TypeAdapter(tuple[ExecutionRow, ...]) -_PARTS: Final = TypeAdapter(tuple[PartRow, ...]) -_COUNTS: Final = TypeAdapter(tuple[CountRow, ...]) +ExecutionIdParts: TypeAlias = tuple[str, str, str] | tuple[str, str, str, str] +_EXECUTION_ID: Final[TypeAdapter[ExecutionIdParts]] = TypeAdapter(ExecutionIdParts) def execution_id(source: str, team_id: str, trace_id: str, trace_ref: str = "") -> str: @@ -76,22 +45,12 @@ def execution_id(source: str, team_id: str, trace_id: str, trace_ref: str = "") def parse_execution(value: str) -> tuple[str, str, str, str]: - parts: Final = TypeAdapter(tuple[str, str, str] | tuple[str, str, str, str]).validate_json( - base64.urlsafe_b64decode(value) - ) + parts: Final = _EXECUTION_ID.validate_json(base64.urlsafe_b64decode(value)) return (parts[0], parts[1], parts[2], parts[3] if len(parts) == 4 else "") -def parameters(scope: Scope, filters: tuple[MetadataFilter, ...]) -> Mapping[str, object]: - return MappingProxyType( - { - "all_teams": int(scope.all_teams), - "team": scope.team_id, - "key_hash": scope.api_key_hash, - "filter_keys": tuple(f.key for f in filters), - "filter_values": tuple(f.value for f in filters), - } - ) +def access_parameters(scope: Scope) -> LensAccessParams: + return LensAccessParams(all_teams=1 if scope.all_teams else 0, team=scope.team_id, key_hash=scope.api_key_hash) def selection_id(value: str) -> str: @@ -104,11 +63,11 @@ class SourceReader: self.storage: Final = storage async def availability(self, scope: Scope) -> ActivityAvailability: - rows: Final = _AVAILABILITY.validate_python(await self.storage.lens_availability(parameters(scope, ()))) + rows: Final = await self.storage.lens_availability(access_parameters(scope)) return rows[0] if rows else ActivityAvailability() async def agents(self, scope: Scope) -> tuple[str, ...]: - rows: Final = _AGENTS.validate_python(await self.storage.lens_agents(parameters(scope, ()))) + rows: Final = await self.storage.lens_agents(access_parameters(scope)) return tuple(row.agent_name for row in rows) async def sample( @@ -122,25 +81,27 @@ class SourceReader: preview: bool = False, cursor: str = "", ) -> Sample: - params: Final = MappingProxyType( - { - **parameters(scope, settings.filters), - "source": settings.source, - "start": start, - "end": end, - "service": settings.service, - "agent_name": settings.agent_name, - "limit": page_size, - "offset": offset, - "after": cursor, - "sample_percent": str(settings.sample_percent), - "sample_cap": settings.sample_size or 0, - "preview": int(preview), - "selected_team": settings.team_id, - "execution_ids": tuple(selection_id(value) for value in settings.execution_ids), - } + params: Final = LensSampleParams( + all_teams=1 if scope.all_teams else 0, + team=scope.team_id, + key_hash=scope.api_key_hash, + source=settings.source, + start=start, + end=end, + service=settings.service, + agent_name=settings.agent_name, + filter_keys=tuple(f.key for f in settings.filters), + filter_values=tuple(f.value for f in settings.filters), + limit=page_size, + offset=offset, + after=cursor, + sample_percent=settings.sample_percent, + sample_cap=settings.sample_size or 0, + preview=1 if preview else 0, + selected_team=settings.team_id, + execution_ids=tuple(selection_id(value) for value in settings.execution_ids), ) - rows: Final = _ROWS.validate_python(await self.storage.lens_sample(params)) + rows: Final = await self.storage.lens_sample(params) return Sample( eligible=rows[0].eligible if rows else 0, selected=rows[0].selected if rows else 0, @@ -173,18 +134,18 @@ class SourceReader: ) async def content(self, scope: Scope, execution: Execution, cursor: str = "", offset: int = 0) -> ExecutionContent: - params: Final = MappingProxyType( - { - **parameters(scope, ()), - "source": execution.source, - "id": execution.trace_id, - "trace_ref": execution.trace_ref, - "record_team": execution.team_id, - "cursor": cursor, - "offset": offset + 1, - } + params: Final = LensContentParams( + all_teams=1 if scope.all_teams else 0, + team=scope.team_id, + key_hash=scope.api_key_hash, + source=execution.source, + id=execution.trace_id, + trace_ref=execution.trace_ref, + record_team=execution.team_id, + cursor=cursor, + offset=offset + 1, ) - rows: Final = _PARTS.validate_python(await self.storage.lens_content(params)) + rows: Final = await self.storage.lens_content(params) return ExecutionContent( execution=execution, parts=tuple( @@ -204,16 +165,16 @@ class SourceReader: ) async def verify_evidence(self, scope: Scope, execution: Execution, evidence: Evidence) -> bool: - params: Final = MappingProxyType( - { - **parameters(scope, ()), - "source": execution.source, - "id": execution.trace_id, - "trace_ref": execution.trace_ref, - "record_team": execution.team_id, - "span": evidence.span_id, - "quote": evidence.quote, - } + params: Final = LensEvidenceParams( + all_teams=1 if scope.all_teams else 0, + team=scope.team_id, + key_hash=scope.api_key_hash, + source=execution.source, + id=execution.trace_id, + trace_ref=execution.trace_ref, + record_team=execution.team_id, + span=evidence.span_id, + quote=evidence.quote, ) - rows: Final = _COUNTS.validate_python(await self.storage.lens_evidence(params)) + rows: Final = await self.storage.lens_evidence(params) return bool(rows and rows[0].count) diff --git a/litellm/proxy/lens/worker.py b/litellm/proxy/lens/worker.py index 62f8295e7d3..051b4a09392 100644 --- a/litellm/proxy/lens/worker.py +++ b/litellm/proxy/lens/worker.py @@ -8,6 +8,7 @@ from types import MappingProxyType from typing import Final import httpx +from pydantic import BaseModel, ConfigDict, ValidationError from .analysis import analyze_sample from .models import Claim, Coverage, ExecutionContent, ModelRequest, ModelResult, Progress, Result, Sample @@ -15,6 +16,17 @@ from .models import Claim, Coverage, ExecutionContent, ModelRequest, ModelResult logger: Final = logging.getLogger("litellm.lens.worker") +class ClaimedJobIdentity(BaseModel): + model_config = ConfigDict(frozen=True, extra="ignore") + id: str + + +class ClaimIdentity(BaseModel): + model_config = ConfigDict(frozen=True, extra="ignore") + lens_id: str + job: ClaimedJobIdentity + + def failure_message(error: Exception) -> str: if isinstance(error, (OSError, sqlite3.Error)): return "Worker temporary storage failed. Increase its capacity or reduce analysis parallelism." @@ -74,9 +86,24 @@ class LensWorker: async def run_once(self) -> bool: response: Final = await self.client.post("/lens/worker/claim", params=MappingProxyType({"protocol_version": 2})) response.raise_for_status() - if response.json() is None: + payload: Final = response.json() + if payload is None: return False - claim: Final = Claim.model_validate(response.json()) + try: + claim: Final = Claim.model_validate(payload) + except ValidationError: + identity: Final = ClaimIdentity.model_validate(payload) + failure: Final = await self.client.post( + f"/lens/worker/{identity.lens_id}/{identity.job.id}/result", + json=Result( + coverage=Coverage(), + error="The worker could not read this investigation. Update the worker to match the gateway, then retry.", + ).model_dump(), + ) + if failure.status_code != 409: + failure.raise_for_status() + logger.warning("Worker could not read a claimed investigation; reported a version compatibility failure") + return True prefix: Final = f"/lens/worker/{claim.lens_id}/{claim.job.id}" async def model(body: ModelRequest) -> ModelResult: diff --git a/litellm/proxy/litellm_pre_call_utils.py b/litellm/proxy/litellm_pre_call_utils.py index d6daf6ebe59..a809f53aa85 100644 --- a/litellm/proxy/litellm_pre_call_utils.py +++ b/litellm/proxy/litellm_pre_call_utils.py @@ -1760,7 +1760,7 @@ class LiteLLMProxyRequestSetup: ): # don't override k-v pair sent by request (user request) data[_metadata_variable_name]["spend_logs_metadata"][key] = value else: - data[_metadata_variable_name]["spend_logs_metadata"] = key_metadata["spend_logs_metadata"] + data[_metadata_variable_name]["spend_logs_metadata"] = dict(key_metadata["spend_logs_metadata"]) ## KEY-LEVEL DISABLE FALLBACKS if "disable_fallbacks" in key_metadata and isinstance(key_metadata["disable_fallbacks"], bool): @@ -1777,6 +1777,53 @@ class LiteLLMProxyRequestSetup: ) return data + @staticmethod + def add_team_and_project_level_controls( + user_api_key_dict: UserAPIKeyAuth, metadata: dict[str, object] + ) -> dict[str, object]: + team_metadata: Final = user_api_key_dict.team_metadata or MappingProxyType({}) + project_metadata: Final = user_api_key_dict.project_metadata or MappingProxyType({}) + request_tags: Final = metadata.get("tags") + team_tags: Final = team_metadata.get("tags") + project_tags: Final = project_metadata.get("tags") + disable_global_guardrails: Final = team_metadata.get("disable_global_guardrails") + opted_out_global_guardrails: Final = team_metadata.get("opted_out_global_guardrails") + spend_logs_metadata: Final = LiteLLMProxyRequestSetup._merge_spend_logs_metadata( + team_spend_logs_metadata=team_metadata.get("spend_logs_metadata"), + request_spend_logs_metadata=metadata.get("spend_logs_metadata"), + ) + tags: Final = LiteLLMProxyRequestSetup._merge_tags( + request_tags=LiteLLMProxyRequestSetup._merge_tags( + request_tags=request_tags if isinstance(request_tags, list) else None, + tags_to_add=team_tags if isinstance(team_tags, list) else None, + ), + tags_to_add=project_tags if isinstance(project_tags, list) else None, + ) + controls: Final = ( + ("tags", tags or None), + ("spend_logs_metadata", spend_logs_metadata), + ( + "disable_global_guardrails", + disable_global_guardrails if isinstance(disable_global_guardrails, bool) else None, + ), + ( + "opted_out_global_guardrails", + opted_out_global_guardrails if isinstance(opted_out_global_guardrails, list) else None, + ), + ) + return {**metadata, **{key: value for key, value in controls if value is not None}} + + @staticmethod + def _merge_spend_logs_metadata( + team_spend_logs_metadata: object, request_spend_logs_metadata: object + ) -> dict[str, object] | None: + """Team values as defaults, the request's own values win on the same key. None when neither is a dict""" + team_values: Final = team_spend_logs_metadata if isinstance(team_spend_logs_metadata, dict) else None + request_values: Final = request_spend_logs_metadata if isinstance(request_spend_logs_metadata, dict) else None + if team_values is None and request_values is None: + return None + return {**(team_values or {}), **(request_values or {})} + @staticmethod def _merge_tags(request_tags: list | None, tags_to_add: list | None) -> list: """ @@ -2312,38 +2359,12 @@ async def add_litellm_data_to_request( data=data, _metadata_variable_name=_metadata_variable_name, ) - ## TEAM-LEVEL SPEND LOGS/TAGS + data[_metadata_variable_name] = LiteLLMProxyRequestSetup.add_team_and_project_level_controls( + user_api_key_dict=user_api_key_dict, + metadata=data[_metadata_variable_name], + ) team_metadata: Final = user_api_key_dict.team_metadata or {} - if "tags" in team_metadata and team_metadata["tags"] is not None: - data[_metadata_variable_name]["tags"] = LiteLLMProxyRequestSetup._merge_tags( - request_tags=data[_metadata_variable_name].get("tags"), - tags_to_add=team_metadata["tags"], - ) - if "disable_global_guardrails" in team_metadata and isinstance(team_metadata["disable_global_guardrails"], bool): - data[_metadata_variable_name]["disable_global_guardrails"] = team_metadata["disable_global_guardrails"] - if "opted_out_global_guardrails" in team_metadata and isinstance( - team_metadata["opted_out_global_guardrails"], list - ): - data[_metadata_variable_name]["opted_out_global_guardrails"] = team_metadata["opted_out_global_guardrails"] - if "spend_logs_metadata" in team_metadata and isinstance(team_metadata["spend_logs_metadata"], dict): - if "spend_logs_metadata" in data[_metadata_variable_name] and isinstance( - data[_metadata_variable_name]["spend_logs_metadata"], dict - ): - for key, value in team_metadata["spend_logs_metadata"].items(): - if ( - key not in data[_metadata_variable_name]["spend_logs_metadata"] - ): # don't override k-v pair sent by request (user request) - data[_metadata_variable_name]["spend_logs_metadata"][key] = value - else: - data[_metadata_variable_name]["spend_logs_metadata"] = team_metadata["spend_logs_metadata"] - - ## PROJECT-LEVEL TAGS project_metadata: Final = user_api_key_dict.project_metadata or {} - if "tags" in project_metadata and project_metadata["tags"] is not None: - data[_metadata_variable_name]["tags"] = LiteLLMProxyRequestSetup._merge_tags( - request_tags=data[_metadata_variable_name].get("tags"), - tags_to_add=project_metadata["tags"], - ) # inherited_tags: every tag key/team/project policy contributed, read # directly from those three sources rather than snapshotted off the shared diff --git a/litellm/proxy/management_endpoints/auto_router_endpoints.py b/litellm/proxy/management_endpoints/auto_router_endpoints.py index 8b73b8177f4..35ff9186f5e 100644 --- a/litellm/proxy/management_endpoints/auto_router_endpoints.py +++ b/litellm/proxy/management_endpoints/auto_router_endpoints.py @@ -5,6 +5,8 @@ POST /auto_router/test_routing - Route one request through an unsaved complexity POST /auto_router/validate_complexity_router_config - Dry-run the complexity-router write gate without saving """ +import asyncio +import math from collections.abc import Mapping, Sequence from datetime import datetime, timedelta, timezone from itertools import chain, groupby @@ -40,6 +42,7 @@ from litellm.proxy.litellm_pre_call_utils import ( refresh_proxy_server_request_body_snapshot, ) from litellm.proxy.management.teams.access import is_team_admin +from litellm.proxy.management_endpoints.common_daily_activity import daily_activity_scope from litellm.proxy.management_helpers.auto_router_permissions import ( authorize_member_auto_router_dependencies, authorize_member_auto_router_team, @@ -47,6 +50,7 @@ from litellm.proxy.management_helpers.auto_router_permissions import ( ) from litellm.repositories.autorouter_session_repository import AutoRouterSessionRepository from litellm.repositories.base_repository import SupportsModelDump +from litellm.repositories.daily_activity_sql import build_where_clause from litellm.repositories.team_repository import TeamRepository from litellm.router_strategy.complexity_router import ComplexityRouter from litellm.router_utils.auto_router_model_naming import ( @@ -316,7 +320,7 @@ async def _authorize_models_this_test_can_call( its calls through the proxy. Team and member budgets are already enforced on every route. """ models: Final = _models_this_test_can_call(config) - if not models and config.classifier_type != "jev": + if not models and config.classifier_type != "oss_classifier": return from litellm.proxy.proxy_server import proxy_logging_obj @@ -342,9 +346,9 @@ async def _authorize_models_this_test_can_call( code=status.HTTP_400_BAD_REQUEST, ) from e - if config.classifier_type == "jev" and user_api_key_dict.budget_throttle_pct is not None: + if config.classifier_type == "oss_classifier" and user_api_key_dict.budget_throttle_pct is not None: raise ProxyException( - message="Budget has been exceeded! JEV Test Routing requires available budget.", + message="Budget has been exceeded! OSS Classifier Test Routing requires available budget.", type=ProxyErrorTypes.budget_exceeded, param=None, code=status.HTTP_400_BAD_REQUEST, @@ -616,34 +620,37 @@ async def preview_auto_router_routing( class _SessionAggRow(BaseModel): + """One router's window: session shape from overlapping sessions, money from the selected days.""" + router_name: str router_type: str - tier_turns: Mapping[str, int] - sessions: int - turns: int - unordered_turns: int - covered_turns: int - cache_hits: int - same_model_turns: int - same_model_hits: int - first_visit_turns: int - first_visit_hits: int - return_turns: int - return_hits: int - return_expired_misses: int - return_within_ttl_misses: int - ttl_5m_turns: int - ttl_1h_turns: int - total_tokens: int - spend: float - saved_spend: float + tier_turns: Mapping[str, int] = MappingProxyType({}) + sessions: int = 0 + session_turns: int = 0 + unordered_turns: int = 0 + covered_turns: int = 0 + cache_hits: int = 0 + same_model_turns: int = 0 + same_model_hits: int = 0 + first_visit_turns: int = 0 + first_visit_hits: int = 0 + return_turns: int = 0 + return_hits: int = 0 + return_expired_misses: int = 0 + return_within_ttl_misses: int = 0 + ttl_5m_turns: int = 0 + ttl_1h_turns: int = 0 + total_tokens: int = 0 + session_seconds: float = 0.0 + turns: int = 0 + spend: float = 0.0 + saved_spend: float = 0.0 savings_estimated_turns: int = 0 savings_estimated_actual_spend: float = 0.0 savings_estimated_classifier_cost: float | None = None savings_estimated_saved_spend: float = 0.0 - classifier_cost: float - classifier_cost_recorded_turns: int - session_seconds: float + classifier_cost: float = 0.0 + classifier_cost_recorded_turns: int = 0 _SESSION_AGG_ROWS: Final = TypeAdapter(list[_SessionAggRow]) @@ -692,6 +699,13 @@ def _compared_row(row: _SessionAggRow) -> _SessionAggRow: ) +def _per_session(row: _SessionAggRow, total: float) -> float | None: + """Unknown, not zero, when routed requests have no session rows of their own to average over.""" + if row.sessions: + return total / row.sessions + return None if row.turns else 0.0 + + def _benchmark_totals(row: _SessionAggRow) -> AutoRouterBenchmarkTotals: return_misses: Final = row.return_turns - row.return_hits saved_spend, baseline_spend = _savings_cohort( @@ -701,9 +715,9 @@ def _benchmark_totals(row: _SessionAggRow) -> AutoRouterBenchmarkTotals: return AutoRouterBenchmarkTotals( sessions=sessions, turns=row.turns, - avg_turns_per_session=row.turns / sessions if sessions else 0.0, - avg_session_seconds=row.session_seconds / sessions if sessions else 0.0, - avg_tokens_per_session=row.total_tokens / sessions if sessions else 0.0, + avg_turns_per_session=_per_session(row, row.session_turns), + avg_session_seconds=_per_session(row, row.session_seconds), + avg_tokens_per_session=_per_session(row, row.total_tokens), spend=row.spend, savings_estimated_turns=row.savings_estimated_turns, savings_estimated_actual_spend=row.savings_estimated_actual_spend, @@ -712,9 +726,8 @@ def _benchmark_totals(row: _SessionAggRow) -> AutoRouterBenchmarkTotals: classifier_cost=row.classifier_cost if row.classifier_cost_recorded_turns == row.turns else None, baseline_spend=baseline_spend, saved_pct=_pct(saved_spend, baseline_spend) if saved_spend is not None and baseline_spend is not None else None, - saved_per_session=(saved_spend / sessions if sessions else 0.0) if saved_spend is not None else None, cache=AutoRouterCacheStats( - coverage_pct=_pct(row.covered_turns, row.turns), + coverage_pct=_pct(row.covered_turns, row.session_turns), hit_rate_pct=_pct(row.cache_hits, row.covered_turns), same_model=_cache_bucket(row.same_model_turns, row.same_model_hits), first_visit=_cache_bucket(row.first_visit_turns, row.first_visit_hits), @@ -748,7 +761,6 @@ def _benchmark_group(row: _SessionAggRow) -> AutoRouterBenchmarkGroup: classifier_cost=totals.classifier_cost, baseline_spend=totals.baseline_spend, saved_pct=totals.saved_pct, - saved_per_session=totals.saved_per_session, cache=totals.cache, ) @@ -759,6 +771,7 @@ def _summed_agg_row(rows: Sequence[_SessionAggRow]) -> _SessionAggRow: router_type="", tier_turns=MappingProxyType({}), sessions=sum(row.sessions for row in rows), + session_turns=sum(row.session_turns for row in rows), turns=sum(row.turns for row in rows), unordered_turns=sum(row.unordered_turns for row in rows), covered_turns=sum(row.covered_turns for row in rows), @@ -790,6 +803,49 @@ def _summed_agg_row(rows: Sequence[_SessionAggRow]) -> _SessionAggRow: ) +async def _recorded_autorouter_savings( + prisma_client: "PrismaClient", start_day: str, end_day: str, api_key: str | None, user_id: str | None +) -> float: + """The selected days' auto-router savings exactly as the Overall view sums them: same table, same filters.""" + where, params = build_where_clause( + daily_activity_scope( + table="litellm_dailyuserspend", + entity_id_field="user_id", + entity_id=user_id, + exclude_entity_ids=None, + api_key=api_key, + start_date=start_day, + end_date=end_day, + model=None, + timezone_offset_minutes=None, + ) + ) + rows: Final = await _query_raw( + prisma_client, + f'SELECT COALESCE(SUM(autorouter_savings_spend), 0)::float8 AS saved FROM "LiteLLM_DailyUserSpend" WHERE {where}', + *params, + ) + return float(rows[0]["saved"]) if rows else 0.0 + + +def _with_recorded_savings( + totals: AutoRouterBenchmarkTotals, rows: Sequence[_SessionAggRow], recorded: float +) -> AutoRouterBenchmarkTotals: + """The headline is the recorded total. Savings outside the compared routers void the cost comparison, + and the part no router's day rows account for is reported as unattributed.""" + if math.isclose(recorded, totals.saved_spend or 0.0, abs_tol=1e-9): + return totals + unattributed: Final = recorded - sum(row.saved_spend for row in rows) + return totals.model_copy( + update={ + "saved_spend": recorded, + "unattributed_saved_spend": None if math.isclose(unattributed, 0.0, abs_tol=1e-9) else unattributed, + "baseline_spend": None, + "saved_pct": None, + } + ) + + def _strategy_router_key(deployment: object) -> tuple[str, str] | None: """``(model_name, kind)`` for a deployment whose routing the session rollup records. @@ -849,7 +905,7 @@ async def get_auto_router_benchmarks( str | None, Query(description="YYYY-MM-DD UTC, inclusive (defaults to 30 days before end_date)") ] = None, end_date: Annotated[str | None, Query(description="YYYY-MM-DD UTC, inclusive (defaults to today)")] = None, - api_key: Annotated[str | None, Query(description="Filter to one virtual key token hash")] = None, + api_key: Annotated[str | None, Query(min_length=1, description="Filter to one virtual key token hash")] = None, user_id: Annotated[ str | None, Query(min_length=1, description="Filter to one canonical internal user recorded on each turn") ] = None, @@ -860,9 +916,10 @@ async def get_auto_router_benchmarks( Reads session rollups folded once per request at spend-write time, so this endpoint never scans LiteLLM_SpendLogs. A user filter selects only turns attributed to that - internal user when written; older key-only history remains outside user views. A session - is in the window when it overlaps it: its last turn is on or after start_date and its first turn is on or before - end_date. Overall hit rate is over telemetry-bearing turns; each bucket's hit rate is + internal user when written; older key-only history remains outside user views. Money counts + only requests on the selected UTC days, and the all-router savings headline is the same daily + total the Overall view reads. Session shape and caching cover every session that overlaps the + window, whole. Overall hit rate is over telemetry-bearing turns; each bucket's hit rate is over that bucket's turns. The rollup supplies the measures, never the list. Which routers appear comes from the @@ -885,24 +942,35 @@ async def get_auto_router_benchmarks( if end_day < start_day: raise HTTPException(status_code=400, detail="end_date must not be earlier than start_date") - raw_rows: Final = await _query_raw( - prisma_client, - AUTOROUTER_BENCHMARKS_SQL, - start_day.isoformat(), - (end_day + timedelta(days=1)).isoformat(), - api_key, - user_id, + first_day: Final = start_day.strftime("%Y-%m-%d") + last_day: Final = end_day.strftime("%Y-%m-%d") + raw_rows, recorded = await asyncio.gather( + _query_raw( + prisma_client, + AUTOROUTER_BENCHMARKS_SQL, + start_day.isoformat(), + (end_day + timedelta(days=1)).isoformat(), + api_key, + user_id, + first_day, + last_day, + ), + _recorded_autorouter_savings(prisma_client, first_day, last_day, api_key, user_id), ) rows: Final = tuple(_compared_row(row) for row in _SESSION_AGG_ROWS.validate_python(raw_rows or ())) + totals: Final = _with_recorded_savings(_benchmark_totals(_summed_agg_row(rows)), rows, recorded) + unattributed: Final = MappingProxyType( + {"baseline_spend": None, "saved_pct": None} if totals.unattributed_saved_spend is not None else {} + ) groups: Final = ( - *(_benchmark_group(row) for row in rows), + *(_benchmark_group(row).model_copy(update=unattributed) for row in rows), *_idle_router_groups(llm_router, frozenset((row.router_name, row.router_type) for row in rows)), ) return AutoRouterBenchmarksResponse( - start_date=start_day.strftime("%Y-%m-%d"), - end_date=end_day.strftime("%Y-%m-%d"), + start_date=first_day, + end_date=last_day, routers_in_scope=len(groups), - totals=_benchmark_totals(_summed_agg_row(rows)), + totals=totals, groups=groups, ) diff --git a/litellm/proxy/management_endpoints/management_v1/spend_logs.py b/litellm/proxy/management_endpoints/management_v1/spend_logs.py index 1cbc454ca5e..e9e6e05ce15 100644 --- a/litellm/proxy/management_endpoints/management_v1/spend_logs.py +++ b/litellm/proxy/management_endpoints/management_v1/spend_logs.py @@ -1,12 +1,15 @@ """`/management/v1/spend_logs` facets.""" from datetime import datetime, timezone +from functools import partial from typing import Annotated, Final, Literal from fastapi import APIRouter, Depends, Query, Request from litellm._logging import verbose_proxy_logger from litellm.proxy._types import CommonProxyErrors, UserAPIKeyAuth +from litellm.proxy.auth.authorization import resolve_owned_read_scope +from litellm.proxy.auth.authorization_dependencies import LogTeamLookup, LogTeamLookupDependency from litellm.proxy.auth.user_api_key_auth import user_api_key_auth from litellm.proxy.list_api.common import ( PROBLEM_TYPE_BASE, @@ -16,7 +19,6 @@ from litellm.proxy.list_api.common import ( reject_unknown_query_params, ) from litellm.proxy.management_endpoints.management_v1.common import MANAGEMENT_V1_PREFIX -from litellm.proxy.utils import PrismaClient from litellm.types.proxy.management_endpoints.management_v1 import ( FacetListResponse, PageMeta, @@ -37,49 +39,29 @@ def _as_utc(value: datetime) -> datetime: async def _spend_log_scope_clause( user_api_key_dict: UserAPIKeyAuth, - prisma_client: PrismaClient, + log_team_lookup: LogTeamLookup, next_param_index: int, -) -> tuple[str | None, tuple[str | list[str], ...]]: +) -> tuple[str | None, tuple[object, ...]]: """SQL predicate restricting the facet to spend logs this caller may read. Returns ``(None, ())`` for a proxy admin. Mirrors the scoping ``/spend/logs/ui`` applies, so a dropdown can never offer a value from a row the caller could not open. """ - from litellm.proxy.spend_tracking.spend_management_endpoints import ( - _get_permitted_team_ids_for_spend_logs, - _is_admin_view_safe, - ) + from litellm.proxy.spend_tracking.spend_management_endpoints import _is_admin_view_safe, read_scope_sql if _is_admin_view_safe(user_api_key_dict=user_api_key_dict): return None, () - - try: - permitted_team_ids = await _get_permitted_team_ids_for_spend_logs( - prisma_client=prisma_client, - user_api_key_dict=user_api_key_dict, - ) - except Exception: - permitted_team_ids = [] - - caller_user_id: Final = user_api_key_dict.user_id - # = ANY(::text[]) rather than an expanded IN list, matching the clause - # ui_view_spend_logs builds: one parameter whatever the team count. - templates: Final = (('"user" = ${}',) if caller_user_id is not None else ()) + ( - ("team_id = ANY(${}::text[])",) if permitted_team_ids else () + scope: Final = await resolve_owned_read_scope( + user_api_key_dict.user_id, partial(log_team_lookup, user_api_key_dict) ) - params: Final = ((caller_user_id,) if caller_user_id is not None else ()) + ( - (permitted_team_ids,) if permitted_team_ids else () - ) - if not templates: - return "FALSE", () - clauses: Final = tuple(template.format(next_param_index + offset) for offset, template in enumerate(templates)) - return f"({' OR '.join(clauses)})", params + return read_scope_sql(scope, next_param_index) async def _list_spend_log_facet( request: Request, user_api_key_dict: UserAPIKeyAuth, + log_team_lookup: LogTeamLookup, start_time: datetime, end_time: datetime, q: str | None, @@ -107,7 +89,7 @@ async def _list_spend_log_facet( scope_clause, scope_params = await _spend_log_scope_clause( user_api_key_dict=user_api_key_dict, - prisma_client=prisma_client, + log_team_lookup=log_team_lookup, next_param_index=len(window_params) + len(search_params) + 1, ) @@ -178,6 +160,7 @@ async def _list_spend_log_facet( async def list_spend_log_end_users( request: Request, user_api_key_dict: Annotated[UserAPIKeyAuth, Depends(user_api_key_auth)], + log_team_lookup: LogTeamLookupDependency, start_time: Annotated[ datetime, Query(alias="filter[startTime][gte]", description="Window start (UTC when no offset is given)"), @@ -211,6 +194,7 @@ async def list_spend_log_end_users( return await _list_spend_log_facet( request=request, user_api_key_dict=user_api_key_dict, + log_team_lookup=log_team_lookup, start_time=start_time, end_time=end_time, q=q, @@ -229,6 +213,7 @@ async def list_spend_log_end_users( async def list_spend_log_users( request: Request, user_api_key_dict: Annotated[UserAPIKeyAuth, Depends(user_api_key_auth)], + log_team_lookup: LogTeamLookupDependency, start_time: Annotated[ datetime, Query(alias="filter[startTime][gte]", description="Window start (UTC when no offset is given)"), @@ -245,6 +230,7 @@ async def list_spend_log_users( return await _list_spend_log_facet( request=request, user_api_key_dict=user_api_key_dict, + log_team_lookup=log_team_lookup, start_time=start_time, end_time=end_time, q=q, diff --git a/litellm/proxy/management_endpoints/mcp_management_endpoints.py b/litellm/proxy/management_endpoints/mcp_management_endpoints.py index 8ed8ec8752e..0d358302fa2 100644 --- a/litellm/proxy/management_endpoints/mcp_management_endpoints.py +++ b/litellm/proxy/management_endpoints/mcp_management_endpoints.py @@ -44,6 +44,7 @@ from fastapi import ( status, ) from fastapi.responses import JSONResponse +from pydantic import TypeAdapter from typing_extensions import ReadOnly, TypedDict try: @@ -137,6 +138,7 @@ if MCP_AVAILABLE: def validate_tool_name(name: str) -> _ToolNameValidationResult: return _ToolNameValidationResult() + from litellm.proxy._experimental.mcp_server.contracts import TargetCatalog from litellm.proxy._experimental.mcp_server.db import ( McpIdentifierConflict, approve_mcp_server, @@ -178,6 +180,7 @@ if MCP_AVAILABLE: global_mcp_server_manager, ) from litellm.proxy._experimental.mcp_server.server_resolution import ( + MCPServerTargetCatalog, authorize_mcp_server, resolve_mcp_server, ) @@ -237,7 +240,9 @@ if MCP_AVAILABLE: MCPCredentials, MCPGatewaySessionsResponse, MCPGatewaySessionsTerminateResponse, + MCPUpstreamProtocol, normalize_upstream_header_name, + validate_mcp_protocol_transport, ) from litellm.types.mcp_server.mcp_server_manager import MCPServer, PinnedMCPTool @@ -2185,18 +2190,16 @@ if MCP_AVAILABLE: from litellm.proxy.auth.ip_address_utils import IPAddressUtils client_ip: Final = IPAddressUtils.get_mcp_client_ip(request) if request is not None else None - resolved: Final = await resolve_mcp_server( - server_id, + catalog: Final[TargetCatalog] = MCPServerTargetCatalog( manager=global_mcp_server_manager, temp_lookup=get_cached_temporary_mcp_server, id_client_ip=None, name_client_ip=client_ip, match_name=True, ) - authorized: Final = await authorize_mcp_server( - resolved, + authorized: Final = await catalog.resolve( + server_id, user_api_key_dict, - manager=global_mcp_server_manager, is_admin_view=_user_has_admin_view(user_api_key_dict), not_found_detail={"error": f"MCP server {server_id} not found"}, forbidden_detail={"error": f"Access denied to MCP server {server_id}"}, @@ -2739,15 +2742,13 @@ if MCP_AVAILABLE: 404, so server ids can't be enumerated), using the same allowed-server resolution the MCP gateway enforces on tool calls. """ - resolved: Final = await resolve_mcp_server( - server_id, + catalog: Final[TargetCatalog] = MCPServerTargetCatalog( manager=global_mcp_server_manager, db_lookup=lambda sid: get_mcp_server(prisma_client, sid), ) - authorized: Final = await authorize_mcp_server( - resolved, + authorized: Final = await catalog.resolve( + server_id, user_api_key_dict, - manager=global_mcp_server_manager, is_admin_view=_user_has_admin_view(user_api_key_dict), not_found_detail={"error": f"MCP Server {server_id} not found"}, forbidden_detail={ @@ -2938,6 +2939,32 @@ if MCP_AVAILABLE: statuses.append(status_obj) return statuses + def _validate_mcp_protocol_update( + payload: UpdateMCPServerRequest, + fields_set: set[str], + stored: LiteLLM_MCPServerTable | None, + read_failed: bool, + ) -> None: + if not {"transport", "mcp_info"}.intersection(fields_set): + return + if read_failed: + raise HTTPException( + status_code=503, detail="Cannot validate MCP configuration while stored state is unavailable" + ) + if stored is None: + return + effective_transport: Final = payload.transport if "transport" in fields_set else stored.transport + effective_info: Final = payload.mcp_info if "mcp_info" in fields_set else stored.mcp_info + try: + validate_mcp_protocol_transport( + TypeAdapter[MCPUpstreamProtocol](MCPUpstreamProtocol).validate_python( + (effective_info or {}).get("protocol_version", "auto") + ), + effective_transport, + ) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + @router.put( "/server", description="Allows deleting mcp serves in the db", @@ -2986,9 +3013,9 @@ if MCP_AVAILABLE: }, ) - # Snapshot the pre-update identity so we can detect a mint-relevant change below. The read is - # advisory (it only feeds the stale-token purge decision), so a failure skips the purge with a - # warning instead of failing the edit, whose primary job is the update itself. + # Snapshot stored configuration for protocol validation and mint-relevant changes below. + # Protocol or transport edits require this read; other edits may continue on read failure + # while skipping the best-effort stale-token purge. try: old_server_record = await get_mcp_server(prisma_client, payload.server_id) old_server_record_read_failed = False @@ -3001,6 +3028,8 @@ if MCP_AVAILABLE: old_server_record = None old_server_record_read_failed = True + _validate_mcp_protocol_update(payload, payload_fields_set, old_server_record, old_server_record_read_failed) + if payload.per_server_oauth_discovery and (old_server_record is not None or old_server_record_read_failed): relay_eligible: Final = old_server_record is not None and is_per_server_oauth_discovery_eligible( payload.auth_type if "auth_type" in payload_fields_set else old_server_record.auth_type, diff --git a/litellm/proxy/management_endpoints/model_management_endpoints.py b/litellm/proxy/management_endpoints/model_management_endpoints.py index 0f0d156d649..50bb831d169 100644 --- a/litellm/proxy/management_endpoints/model_management_endpoints.py +++ b/litellm/proxy/management_endpoints/model_management_endpoints.py @@ -39,6 +39,7 @@ from litellm.litellm_core_utils.ptu_pricing import ( SEARCH_CONTEXT_SIZES, ptu_config_error, ) +from litellm.litellm_core_utils.sensitive_data_masker import redact_credentials_in_payload from litellm.proxy._types import ( BlockModelRequest, CommonProxyErrors, @@ -115,6 +116,7 @@ from litellm.router_strategy.complexity_router import ( normalize_classification_examples, normalize_classification_prompt, ) +from litellm.router_strategy.complexity_router.config import resolve_complexity_router_config_write from litellm.router_utils.auto_router_model_naming import ( GATED_AUTO_ROUTER_CAPABILITIES, STRATEGY_ROUTER_PARAM_FIELDS, @@ -187,6 +189,25 @@ class _ProxyModelRow(Protocol): def model_dump_json(self, *, exclude_none: bool = False) -> str: ... +def _model_write_response( + row: _ProxyModelRow, member_write: MemberAutoRouterWrite | None +) -> _ProxyModelRow | Mapping[str, object]: + if member_write is None: + return row + payload: Final = TypeAdapter(dict[str, object]).validate_json(row.model_dump_json()) + stored_params: Final = payload.get("litellm_params") + params: Final = ( + TypeAdapter(dict[str, object]).validate_json(stored_params) + if isinstance(stored_params, str) + else TypeAdapter(dict[str, object]).validate_python(stored_params) + ) + redacted: Final = redact_credentials_in_payload(params) + return { + **payload, + "litellm_params": json.dumps(redacted) if isinstance(stored_params, str) else redacted, + } + + class _ProxyModelTable(Protocol): def find_unique(self, *, where: Mapping[str, object]) -> Awaitable[BaseModel | None]: ... @@ -407,34 +428,13 @@ WHERE model_id <> $1 def _effective_complexity_router_config( incoming_params: GenericLiteLLMParams | None, existing_params: GenericLiteLLMParams | None -) -> object: +) -> Mapping[str, object] | None: incoming: Final = None if incoming_params is None else incoming_params.complexity_router_config existing: Final = None if existing_params is None else existing_params.complexity_router_config - if incoming is None: - return existing - if existing is None or incoming.get("classifier_type") != "jev" or existing.get("classifier_type") != "jev": - return incoming - incoming_jev: Final[object] = incoming.get("jev_classifier_config") - existing_jev: Final[object] = existing.get("jev_classifier_config") - if not isinstance(incoming_jev, Mapping) or not isinstance(existing_jev, Mapping): - return incoming - supplied: Final = TypeAdapter(dict[str, object]).validate_python(incoming_jev) - stored: Final = TypeAdapter(dict[str, object]).validate_python(existing_jev) - same_base: Final = "api_base" not in supplied or supplied["api_base"] == stored.get("api_base") - transport: Final = MappingProxyType( - { - key: value - for key, value in stored.items() - if key in ("api_key", "api_base") and (key != "api_key" or same_base) - } - ) - return { - **incoming, - "jev_classifier_config": { - **transport, - **supplied, - }, - } + config_adapter: Final[TypeAdapter[Mapping[str, object] | None]] = TypeAdapter(Mapping[str, object] | None) + return resolve_complexity_router_config_write( + config_adapter.validate_python(incoming), config_adapter.validate_python(existing) + ).effective def _effective_model( @@ -1304,7 +1304,7 @@ async def patch_model( live_after=reload_outcome.live_after, ) - return updated_model + return _model_write_response(updated_model, member_write) except Exception as e: verbose_proxy_logger.exception("Error in patch_model: %s", e) @@ -1501,10 +1501,18 @@ async def _add_model_to_db( slot: AbstractAsyncContextManager[_ProxyModelTable] | None = None, ) -> "_ProxyModelRow | LiteLLM_ProxyModelTable": # encrypt litellm params # - _litellm_params_dict: Final = model_params.litellm_params.dict(exclude_none=True) + _litellm_params_dict: Final = TypeAdapter(dict[str, object]).validate_python( + model_params.litellm_params.model_dump(exclude_none=True) + ) + if "complexity_router_config" in _litellm_params_dict: + _litellm_params_dict["complexity_router_config"] = _effective_complexity_router_config( + model_params.litellm_params, None + ) _original_litellm_model_name: Final = model_params.litellm_params.model for k, v in _litellm_params_dict.items(): - encrypted_value = encrypt_value_helper(value=v, new_encryption_key=new_encryption_key) + encrypted_value = ( + encrypt_value_helper(value=v, new_encryption_key=new_encryption_key) if isinstance(v, str) else v + ) model_params.litellm_params[k] = encrypted_value _data: Final[dict] = { "model_id": model_params.model_info.id, @@ -2536,7 +2544,7 @@ async def add_new_model( live_after=reload_outcome.live_after, ) - return model_response + return _model_write_response(model_response, member_write) except Exception as e: verbose_proxy_logger.exception("litellm.proxy.proxy_server.add_new_model(): Exception occured - %s", e) @@ -2760,7 +2768,7 @@ async def update_model( live_after=reload_outcome.live_after, ) - return model_response + return None if model_response is None else _model_write_response(model_response, member_write) except Exception as e: verbose_proxy_logger.exception("litellm.proxy.proxy_server.update_model(): Exception occured - %s", e) if isinstance(e, HTTPException): diff --git a/litellm/proxy/management_endpoints/scim/scim_v2.py b/litellm/proxy/management_endpoints/scim/scim_v2.py index 99e5f0a4b2a..a7ee0170325 100644 --- a/litellm/proxy/management_endpoints/scim/scim_v2.py +++ b/litellm/proxy/management_endpoints/scim/scim_v2.py @@ -10,6 +10,7 @@ from copy import deepcopy from dataclasses import dataclass from functools import partial from itertools import chain +from types import MappingProxyType from typing import TYPE_CHECKING, Final, NamedTuple, Protocol, overload from fastapi import ( @@ -2708,6 +2709,75 @@ async def delete_group( raise handle_exception_on_proxy(e) +GROUP_PATCH_READ_ONLY_ATTRIBUTES: Final = frozenset({"id", "schemas", "meta"}) +_NO_FIELDS: Final[Mapping[str, object]] = MappingProxyType({}) + + +def _pathless_group_resource(op: SCIMPatchOperation) -> Mapping[str, object] | None: + """The partial Group resource a path-less op carries, or None when the op names a path. + + RFC 7644 Section 3.5.2 lets ``add`` and ``replace`` omit ``path`` and send the + attributes to apply as an object (what Okta Push Groups does on a rename); + ``remove`` always needs a path (Section 3.5.2.2). + """ + if op.path: + return None + resource: Final = _json_object_fields(op.value) + if op.op != "remove" and resource is not None: + return resource + detail: Final[_ScimErrorDetail] = { + "error": ( + "A remove operation requires a 'path' (RFC 7644 Section 3.5.2.2)" + if op.op == "remove" + else f"A {op.op} operation without a 'path' requires an object 'value' (RFC 7644 Section 3.5.2)" + ) + } + raise HTTPException(status_code=400, detail=detail) + + +def _group_patch_attribute_values(op: SCIMPatchOperation) -> tuple[tuple[str, object], ...]: + """The (attribute, value) pairs an operation applies, one per key of a path-less value.""" + resource: Final = _pathless_group_resource(op) + if resource is None: + return (((op.path or "").lower(), op.value),) + return tuple( + (key.lower(), value) + for key, value in resource.items() + if key and key.lower() not in GROUP_PATCH_READ_ONLY_ATTRIBUTES + ) + + +def _replaces_members(op: SCIMPatchOperation) -> bool: + if op.op != "replace": + return False + return any(attribute.startswith("members") for attribute, _ in _group_patch_attribute_values(op)) + + +def _patched_group_snapshot( + existing_snapshot: Mapping[str, object], + pathless_resources: Sequence[Mapping[str, object]], + mirrored_values: Sequence[tuple[str, object | None]], +) -> dict[str, object]: + """The ``scim_data`` snapshot after a PATCH: the path-less resources merged over the + existing snapshot in operation order (``members`` live in members_with_roles), then + each attribute in ``mirrored_values`` set to what the whole operation list left on + the team, so a later path op wins over an earlier path-less value; ``None`` drops it. + """ + pathless_items: Final = ( + (key, value) + for key, value in chain.from_iterable(resource.items() for resource in pathless_resources) + if key.lower() != "members" + ) + mirrored_keys: Final = frozenset(key.lower() for key, _ in mirrored_values) + kept: Final = ( + (key, value) + for key, value in chain(existing_snapshot.items(), pathless_items) + if key.lower() not in mirrored_keys + ) + refreshed: Final = ((key, value) for key, value in mirrored_values if value is not None) + return dict(chain(kept, refreshed)) + + async def _process_group_patch_operations( patch_ops: SCIMPatchOp, existing_team: LiteLLM_TeamTable, prisma_client: PrismaClient ) -> tuple[dict[str, object], set[str], set[str] | None]: @@ -2725,11 +2795,24 @@ async def _process_group_patch_operations( conditional on what the id turns out to be and leave members we should never have admitted - the phantom users this endpoint used to create for nested groups - impossible to clean up. + + A path-less op carries a partial Group resource: each attribute applies as if + sent with that path, and its attributes other than ``members`` (the roster + lives in members_with_roles) are merged in operation order into the + ``scim_data`` snapshot the PUT path writes, whose displayName and externalId + then mirror what the whole operation list left on the team. An empty metadata + key left behind by an earlier path-less op (stored whole under ``""``) is + dropped. """ update_data: Final[dict[str, object]] = {} + stored_metadata: Final[dict[str, object] | None] = existing_team.metadata + existing_metadata: Final = _json_object_fields(stored_metadata) or _NO_FIELDS + pathless_resources: Final = tuple( + resource for resource in map(_pathless_group_resource, patch_ops.Operations) if resource is not None + ) - # Create a fresh copy of existing metadata to avoid Prisma issues - metadata: Final = {**(existing_team.metadata or {}), SCIM_MANAGED_TEAM_METADATA_KEY: True} + kept_metadata_items: Final = ((key, value) for key, value in existing_metadata.items() if key) + metadata: Final = dict(chain(kept_metadata_items, ((SCIM_MANAGED_TEAM_METADATA_KEY, True),))) # Track member changes. members_with_roles is the source of truth for team # membership; the legacy `members` column is not populated by team creation @@ -2739,58 +2822,69 @@ async def _process_group_patch_operations( current_members: Final = set(await _get_team_member_user_ids_from_team(existing_team)) final_members = current_members.copy() - # Process each patch operation for op in patch_ops.Operations: - path = (op.path or "").lower() - value = op.value - op_type = op.op + for attribute, value in _group_patch_attribute_values(op): + op_type = op.op - if path == "displayname": - if op_type == "remove": - update_data["team_alias"] = None - else: - update_data["team_alias"] = str(value) - elif path == "externalid": - if op_type == "remove": - metadata.pop("externalId", None) - else: - metadata["externalId"] = str(value) - elif path.startswith("members"): - # Handle member operations - patched_members = ( - _parse_member_entries(value) - if value is not None - else tuple( - SCIMMember(value=member_id) for member_id in _extract_ids_from_path_filter(op.path, "members") + if attribute == "displayname": + if op_type == "remove": + update_data["team_alias"] = None + else: + update_data["team_alias"] = str(value) + elif attribute == "externalid": + if op_type == "remove": + metadata.pop("externalId", None) + else: + metadata["externalId"] = str(value) + elif attribute.startswith("members"): + patched_members = ( + _parse_member_entries(value) + if value is not None + else tuple( + SCIMMember(value=member_id) for member_id in _extract_ids_from_path_filter(op.path, "members") + ) ) + + if op_type == "remove": + final_members = final_members - await _member_ids_to_drop( + patched_members, frozenset(final_members), prisma_client + ) + else: + member_result = await _resolve_group_member_ids( + members=patched_members, + created_via="scim_group_patch", + prisma_client=prisma_client, + ) + if op_type == "replace": + final_members = set(member_result.all_member_ids) + elif op_type == "add": + final_members = final_members | set(member_result.all_member_ids) + elif op_type == "remove": + metadata.pop(attribute, None) + else: + metadata[attribute] = value + + if pathless_resources: + applied_attributes: Final = frozenset( + attribute for attribute, _ in chain.from_iterable(map(_group_patch_attribute_values, patch_ops.Operations)) + ) + mirrored_values: Final = tuple( + (snapshot_key, final_value) + for attribute, snapshot_key, final_value in ( + ("displayname", "displayName", update_data.get("team_alias")), + ("externalid", "externalId", metadata.get("externalId")), ) - - if op_type == "remove": - final_members = final_members - await _member_ids_to_drop( - patched_members, frozenset(final_members), prisma_client - ) - else: - member_result = await _resolve_group_member_ids( - members=patched_members, - created_via="scim_group_patch", - prisma_client=prisma_client, - ) - if op_type == "replace": - final_members = set(member_result.all_member_ids) - elif op_type == "add": - final_members = final_members | set(member_result.all_member_ids) - else: - # Handle other generic metadata - if op_type == "remove": - metadata.pop(path, None) - else: - metadata[path] = value + if attribute in applied_attributes + ) + metadata[SCIM_TEAM_DATA_METADATA_KEY] = _patched_group_snapshot( + existing_snapshot=_json_object_fields(existing_metadata.get(SCIM_TEAM_DATA_METADATA_KEY)) or _NO_FIELDS, + pathless_resources=pathless_resources, + mirrored_values=mirrored_values, + ) update_data["metadata"] = metadata - member_replace_present: Final = any( - op.op == "replace" and (op.path or "").lower().startswith("members") for op in patch_ops.Operations - ) + member_replace_present: Final = any(map(_replaces_members, patch_ops.Operations)) replace_target: Final = set(final_members) if member_replace_present else None return update_data, final_members, replace_target diff --git a/litellm/proxy/management_helpers/auto_router_permissions.py b/litellm/proxy/management_helpers/auto_router_permissions.py index c8bb95eb3bb..5845194fa9b 100644 --- a/litellm/proxy/management_helpers/auto_router_permissions.py +++ b/litellm/proxy/management_helpers/auto_router_permissions.py @@ -33,6 +33,10 @@ from litellm.repositories.prisma_protocols import DatabaseClient from litellm.repositories.project_repository import ProjectRepository from litellm.repositories.table_repositories import TeamMembershipRepository from litellm.router import Router +from litellm.router_strategy.complexity_router.config import ( + ComplexityRouterConfigWrite, + resolve_complexity_router_config_write, +) from litellm.router_utils.auto_router_model_naming import classify_strategy_router_model, strategy_router_dependencies from litellm.types.management_endpoints.auto_router_endpoints import RequestComplexityRouterConfig from litellm.types.router import Deployment, updateDeployment @@ -65,12 +69,12 @@ class _MemberRouterGenerationParams(BaseModel): stop: str | tuple[str, ...] | None = None -class _MemberJevClassifierConfig(BaseModel): - """The Jev classifier settings a team member may set. Credentials stay the proxy's own: a member-chosen - api_base would receive the proxy's TYPESAFE_API_KEY, and a member-chosen api_key would be sent from the proxy.""" +class _MemberOpenSourceClassifierConfig(BaseModel): + """Classifier settings a team member may set while the gateway owns the connection.""" model_config = ConfigDict(extra="forbid") + provider: Literal["jev", "laya", "bespoke"] = "jev" model: str api_key: None = None api_base: None = None @@ -123,14 +127,21 @@ def authorize_member_auto_router_team( def validate_member_auto_router_config(config: Mapping[str, object]) -> RequestComplexityRouterConfig: + return _validate_member_auto_router_config_write(resolve_complexity_router_config_write(config, None)) + + +def _validate_member_auto_router_config_write(write: ComplexityRouterConfigWrite) -> RequestComplexityRouterConfig: + if write.effective is None: + raise HTTPException(status_code=400, detail="A complexity_router_config is required.") try: - validated: Final = _MemberComplexityRouterConfig.model_validate(config) - for entries in validated.tier_model_configs.values(): - for entry in entries: - _MemberRouterGenerationParams.model_validate(entry.litellm_params) - if validated.jev_classifier_config is not None: - _MemberJevClassifierConfig.model_validate(validated.jev_classifier_config.model_dump()) - return validated + if write.submitted is not None: + validated: Final = _MemberComplexityRouterConfig.model_validate(write.submitted) + for entries in validated.tier_model_configs.values(): + for entry in entries: + _MemberRouterGenerationParams.model_validate(entry.litellm_params) + if validated.opensource_classifier_config is not None: + _MemberOpenSourceClassifierConfig.model_validate(validated.opensource_classifier_config.model_dump()) + return RequestComplexityRouterConfig.model_validate(write.effective) except ValidationError as exc: location: Final = ".".join(str(part) for part in exc.errors()[0]["loc"]) raise HTTPException(status_code=400, detail=f"Invalid member auto-router configuration at {location}.") from exc @@ -332,16 +343,15 @@ async def authorize_member_auto_router_write( if existing is not None and incoming.model_name not in (None, public_name, existing.model_name): raise HTTPException(status_code=403, detail="Team members cannot rename an auto router.") supplied_config: Final = _RouterConfigSource.model_validate(params.model_dump()).complexity_router_config - raw_config: Final = ( - supplied_config - if supplied_config is not None - else _RouterConfigSource.model_validate(existing.litellm_params.model_dump()).complexity_router_config + stored_config: Final = ( + _RouterConfigSource.model_validate(existing.litellm_params.model_dump()).complexity_router_config if existing is not None else None ) - if raw_config is None: - raise HTTPException(status_code=400, detail="A complexity_router_config is required.") - config: Final = validate_member_auto_router_config(raw_config) + resolved_config: Final = resolve_complexity_router_config_write(supplied_config, stored_config) + if resolved_config.supplied_connection_fields: + raise HTTPException(status_code=403, detail="Team members cannot change classifier connections.") + config: Final = _validate_member_auto_router_config_write(resolved_config) stored_default: Final = existing.litellm_params.complexity_router_default_model if existing is not None else None default_model: Final = ( params.complexity_router_default_model diff --git a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py index c7b597b584f..0cff558985b 100644 --- a/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/llm_passthrough_endpoints.py @@ -24,6 +24,7 @@ from typing import TYPE_CHECKING, Annotated, Final, Literal, Protocol, cast import httpx from fastapi import APIRouter, Depends, HTTPException, Request, Response, WebSocket from fastapi.responses import StreamingResponse +from pydantic import ConfigDict, TypeAdapter from starlette.websockets import WebSocketState from typing_extensions import ReadOnly, TypedDict @@ -58,6 +59,7 @@ from litellm.llms.deepgram.common_utils import ( ) from litellm.llms.fal_ai.cost_calculator import fal_ai_passthrough_cost, fal_ai_queue_base from litellm.llms.nvidia_nim.passthrough.transformation import nvidia_nim_model_group_in_path +from litellm.llms.oss_decision import OssDecisionProvider, oss_connection, validate_oss_request from litellm.llms.vertex_ai.vertex_llm_base import VertexBase from litellm.passthrough.main import AsyncPassthroughStreamingResponse from litellm.proxy._types import * @@ -636,6 +638,62 @@ async def typesafe_proxy_route( return await endpoint_func(request, fastapi_response, user_api_key_dict) +@router.post( + "/laya/v1/systemone", + tags=["Laya Pass-through", "pass-through"], +) +async def laya_proxy_route( + request: Request, + fastapi_response: Response, + user_api_key_dict: Annotated[UserAPIKeyAuth, Depends(user_api_key_auth)], +) -> Response: + return await _oss_decision_proxy_route("laya", request, fastapi_response, user_api_key_dict) + + +@router.post("/bespoke/v1/systemone", tags=["Bespoke Nimble Pass-through", "pass-through"]) +async def bespoke_proxy_route( + request: Request, + fastapi_response: Response, + user_api_key_dict: Annotated[UserAPIKeyAuth, Depends(user_api_key_auth)], +) -> Response: + return await _oss_decision_proxy_route("bespoke", request, fastapi_response, user_api_key_dict) + + +async def _oss_decision_proxy_route( + provider: OssDecisionProvider, request: Request, fastapi_response: Response, user_api_key_dict: UserAPIKeyAuth +) -> Response: + body: Final = TypeAdapter(dict[str, object]).validate_python(await _read_request_body(request)) + try: + _ = validate_oss_request(provider, body) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + try: + connection: Final = oss_connection(provider) + except ValueError as exc: + raise HTTPException( + status_code=503, detail=f"{provider} server is not configured correctly; check {provider.upper()}_API_BASE" + ) from exc + base_url: Final = httpx.URL(connection.api_base) + updated_url: Final = base_url.copy_with( + path=HttpPassThroughEndpointHelpers.join_base_and_endpoint_path(base_url, "/v1/systemone"), + ) + authorization: Final[Mapping[str, str]] = ( + MappingProxyType({"Authorization": f"Bearer {connection.api_key}"}) + if connection.api_key + else MappingProxyType({}) + ) + endpoint_func: Final = create_pass_through_route( + endpoint="v1/systemone", + target=str(updated_url), + custom_headers=MappingProxyType({**authorization, "Content-Type": "application/json"}), + custom_llm_provider=provider, + is_streaming_request=False, + ) + return TypeAdapter(Response, config=ConfigDict(arbitrary_types_allowed=True)).validate_python( + await endpoint_func(request, fastapi_response, user_api_key_dict) + ) + + @router.api_route( "/openrouter/{endpoint:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH"], diff --git a/litellm/proxy/pass_through_endpoints/llm_provider_handlers/batch_attribution.py b/litellm/proxy/pass_through_endpoints/llm_provider_handlers/batch_attribution.py index e7b608e162e..880fdad92bf 100644 --- a/litellm/proxy/pass_through_endpoints/llm_provider_handlers/batch_attribution.py +++ b/litellm/proxy/pass_through_endpoints/llm_provider_handlers/batch_attribution.py @@ -34,9 +34,7 @@ def is_collection_route(url_route: str, collection_suffix: str) -> bool: def request_tags_from_metadata(request_metadata: Mapping[str, object]) -> tuple[str, ...] | None: """Tags for the batch-cost spend row: the request's own tags when it sent any, - otherwise the key's tags, which auth exposes as user_api_key_auth_metadata (a - tagged key does not put its tags in the top-level metadata "tags" on the - passthrough path) + otherwise the key's tags, which auth exposes as user_api_key_auth_metadata """ tags: Final = _sanitized_str_tuple(request_metadata.get("tags")) if tags: diff --git a/litellm/proxy/pass_through_endpoints/llm_provider_handlers/typesafe_passthrough_logging_handler.py b/litellm/proxy/pass_through_endpoints/llm_provider_handlers/typesafe_passthrough_logging_handler.py index 3ad92acb48a..03ec559b83d 100644 --- a/litellm/proxy/pass_through_endpoints/llm_provider_handlers/typesafe_passthrough_logging_handler.py +++ b/litellm/proxy/pass_through_endpoints/llm_provider_handlers/typesafe_passthrough_logging_handler.py @@ -10,6 +10,7 @@ from litellm.litellm_core_utils.litellm_logging import Logging as LiteLLMLogging from litellm.litellm_core_utils.litellm_logging import ( get_standard_logging_object_payload, # pyright: ignore[reportUnknownVariableType] # legacy helper has an untyped signature ) +from litellm.llms.laya.common_utils import laya_response_model from litellm.proxy._types import PassThroughEndpointLoggingTypedDict from litellm.types.utils import ModelResponse, StandardPassThroughResponseObject, Usage @@ -69,9 +70,11 @@ class TypeSafePassthroughLoggingHandler: **kwargs: object, ) -> PassThroughEndpointLoggingTypedDict: response: Final = _parse_typesafe_response(response_body) - response_model: Final = response.model request_model_value: Final = request_body.get("model") request_model: Final = request_model_value if isinstance(request_model_value, str) else None + response_model: Final = ( + laya_response_model(response_body, request_model) if custom_llm_provider == "laya" else response.model + ) logged_model: Final = response_model or request_model or "unknown" model_name: Final = f"{custom_llm_provider}/{logged_model}" usage: Final = response.usage or _TypeSafeUsage() diff --git a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py index 22ecdc06ed9..a5b414e5a18 100644 --- a/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py +++ b/litellm/proxy/pass_through_endpoints/pass_through_endpoints.py @@ -26,6 +26,7 @@ from fastapi import ( status, ) from fastapi.responses import StreamingResponse +from pydantic import TypeAdapter from starlette.datastructures import UploadFile as StarletteUploadFile from starlette.websockets import WebSocketState from websockets.asyncio.client import connect @@ -64,6 +65,7 @@ from litellm.llms.base_llm.managed_resources.utils import ( resolve_passthrough_managed_id_provider, ) from litellm.llms.custom_httpx.http_handler import get_async_httpx_client +from litellm.llms.oss_decision import validate_oss_request from litellm.passthrough import BasePassthroughUtils from litellm.proxy._types import ( ConfigFieldInfo, @@ -74,7 +76,11 @@ from litellm.proxy._types import ( ProxyException, UserAPIKeyAuth, ) -from litellm.proxy.auth.auth_utils import request_dispatched_to_pass_through_endpoint +from litellm.proxy.auth.auth_utils import ( + get_model_from_request, + get_request_route, + request_dispatched_to_pass_through_endpoint, +) from litellm.proxy.auth.user_api_key_auth import user_api_key_auth from litellm.proxy.common_request_processing import ( ProxyBaseLLMRequestProcessing, @@ -100,6 +106,8 @@ from litellm.proxy.common_utils.sse_keepalive import ( from litellm.proxy.litellm_pre_call_utils import ( LiteLLMProxyRequestSetup, _get_dynamic_logging_metadata, # pyright: ignore[reportPrivateUsage] # shared proxy helper, same import style as _read_request_body above + _key_or_team_allows_client_pricing_override, # pyright: ignore[reportPrivateUsage] # reuse the proxy's pricing trust policy + _strip_client_pricing_overrides, # pyright: ignore[reportPrivateUsage] # sanitize before trusted hooks add guardrail costs ) from litellm.proxy.route_llm_request import ProxyModelNotFoundError from litellm.proxy.utils import normalize_route_for_root_path @@ -379,7 +387,9 @@ class HttpPassThroughEndpointHelpers(BasePassthroughUtils): @staticmethod def get_endpoint_type(url: str, custom_llm_provider: str | None = None) -> EndpointType: parsed_url: Final = urlparse(url) - if custom_llm_provider == "typesafe" and parsed_url.path.removesuffix("/").endswith("/v1/systemone"): + if custom_llm_provider in ("typesafe", "laya", "bespoke") and parsed_url.path.removesuffix("/").endswith( + "/v1/systemone" + ): return EndpointType.DECISIONS if ( ("generateContent") in url @@ -585,7 +595,18 @@ class HttpPassThroughEndpointHelpers(BasePassthroughUtils): """ Filter out litellm params from the request body """ + from litellm.proxy.proxy_server import llm_router + _parsed_body = _parsed_body or {} + managed_model: Final = get_model_from_request( + request_data=_parsed_body, + route=get_request_route(request), + request_headers=request.headers, + request_query_params=request.query_params, + llm_router=llm_router, + request=request, + team_id=user_api_key_dict.team_id, + ) litellm_keys_in_body: Final = MappingProxyType( {k: _parsed_body.pop(k) for k in types_utils.all_litellm_params if k in _parsed_body} @@ -600,11 +621,11 @@ class HttpPassThroughEndpointHelpers(BasePassthroughUtils): litellm_metadata: Final = litellm_keys_in_body.get("litellm_metadata") metadata: Final = litellm_keys_in_body.get("metadata") - if litellm_metadata: - _metadata.update(litellm_metadata) - if metadata: - _metadata.update(metadata) + for client_metadata in (litellm_metadata, metadata): + if isinstance(client_metadata, dict): + _metadata.update({k: v for k, v in client_metadata.items() if not k.startswith("user_api_key_")}) + _metadata = _apply_key_team_project_controls(user_api_key_dict=user_api_key_dict, metadata=_metadata) _metadata = _update_metadata_with_tags_in_header( request=request, metadata=_metadata, @@ -631,10 +652,19 @@ class HttpPassThroughEndpointHelpers(BasePassthroughUtils): # would attribute it to a budget the operator scoped to a LiteLLM model that # merely shares the name. if not request_dispatched_to_pass_through_endpoint(request): + _metadata["model_group"] = managed_model if isinstance(managed_model, str) else None _metadata["user_api_key_model_max_budget"] = user_api_key_dict.model_max_budget _metadata["user_api_key_team_model_max_budget"] = user_api_key_dict.team_model_max_budget _metadata["user_api_key_user_model_max_budget"] = user_api_key_dict.user_model_max_budget _metadata["user_api_key_end_user_model_max_budget"] = user_api_key_dict.end_user_model_max_budget + else: + for field in ( + "user_api_key_model_max_budget", + "user_api_key_team_model_max_budget", + "user_api_key_user_model_max_budget", + "user_api_key_end_user_model_max_budget", + ): + _metadata.pop(field, None) _metadata.update( LiteLLMProxyRequestSetup.get_sanitized_user_information_from_key(user_api_key_dict=user_api_key_dict) ) @@ -1131,6 +1161,15 @@ async def pass_through_request( _parsed_body, ) + if not _key_or_team_allows_client_pricing_override(user_api_key_dict): + pricing_body: Final = TypeAdapter(dict[str, object]).validate_python(_parsed_body) + _strip_client_pricing_overrides(pricing_body) + _parsed_body = pricing_body + if custom_llm_provider in ("laya", "bespoke"): + decision_request: Final = TypeAdapter(Mapping[str, object]).validate_python(_parsed_body) + checkpoint: Final = validate_oss_request(custom_llm_provider, decision_request) + _parsed_body["model"] = f"{custom_llm_provider}/{checkpoint}" + ### COLLECT GUARDRAILS FOR PASSTHROUGH ENDPOINT ### # Passthrough endpoints are opt-in only for guardrails # When enabled, collect guardrails from org/team/key levels + passthrough-specific @@ -1186,6 +1225,19 @@ async def pass_through_request( call_type="pass_through_endpoint", endpoint_type=endpoint_type, ) + if custom_llm_provider in ("laya", "bespoke"): + hook_body: Final = TypeAdapter(dict[str, object]).validate_python(_parsed_body) + hook_model: Final = hook_body.get("model") + decision_body: Final = MappingProxyType( + { + **hook_body, + "model": hook_model.removeprefix(f"{custom_llm_provider}/") + if isinstance(hook_model, str) + else hook_model, + } + ) + _ = validate_oss_request(custom_llm_provider, decision_body) + _parsed_body = TypeAdapter(dict[str, object]).validate_python(decision_body) resolved_timeout: Final = resolve_pass_through_request_timeout(timeout) async_client_obj: Final = get_async_httpx_client( llm_provider=httpxSpecialProvider.PassThroughEndpoint, @@ -1886,6 +1938,20 @@ async def pass_through_request( ) +def _apply_key_team_project_controls( + user_api_key_dict: UserAPIKeyAuth, metadata: dict[str, object] +) -> dict[str, object]: + data: Final = LiteLLMProxyRequestSetup.add_key_level_controls( + key_metadata=user_api_key_dict.metadata, + data={"metadata": metadata}, + _metadata_variable_name="metadata", + ) + return LiteLLMProxyRequestSetup.add_team_and_project_level_controls( + user_api_key_dict=user_api_key_dict, + metadata=data["metadata"], + ) + + def _update_metadata_with_tags_in_header(request: Request, metadata: dict) -> dict: """ If tags are in the request headers, add them to the metadata @@ -1906,9 +1972,10 @@ def _update_metadata_with_tags_in_header(request: Request, metadata: dict) -> di # Only add tags key if there are tags to add if tags_to_add: - if "tags" not in metadata: - metadata["tags"] = [] - metadata["tags"].extend(tags_to_add) + metadata["tags"] = LiteLLMProxyRequestSetup._merge_tags( + request_tags=metadata.get("tags"), + tags_to_add=tags_to_add, + ) return metadata @@ -2389,7 +2456,9 @@ async def websocket_passthrough_request( # with the existing _init_kwargs_for_pass_through_endpoint function class DummyRequest: def __init__(self, url: str, method: str = "WEBSOCKET", headers: dict | None = None): - self.url = url + self.url = httpx.URL(url) + self.scope = websocket.scope + self.query_params = websocket.query_params self.method = method self.headers = headers or {} diff --git a/litellm/proxy/pass_through_endpoints/success_handler.py b/litellm/proxy/pass_through_endpoints/success_handler.py index 6bba879b6c1..da3e28e25e4 100644 --- a/litellm/proxy/pass_through_endpoints/success_handler.py +++ b/litellm/proxy/pass_through_endpoints/success_handler.py @@ -334,8 +334,10 @@ class PassThroughEndpointLogging: ) standard_logging_response_object = transcribe_handler_result["result"] # rebind-ok: elif-chain kwargs = transcribe_handler_result["kwargs"] # rebind-ok: elif-chain contract - elif self.is_typesafe_route(custom_llm_provider) or self.is_openrouter_decisions_route( - url_route, custom_llm_provider + elif ( + self.is_typesafe_route(custom_llm_provider) + or custom_llm_provider in ("laya", "bespoke") + or self.is_openrouter_decisions_route(url_route, custom_llm_provider) ): from .llm_provider_handlers.typesafe_passthrough_logging_handler import ( TypeSafePassthroughLoggingHandler, diff --git a/litellm/proxy/proxy_cli.py b/litellm/proxy/proxy_cli.py index cb65cae79a4..a9745565799 100644 --- a/litellm/proxy/proxy_cli.py +++ b/litellm/proxy/proxy_cli.py @@ -1447,9 +1447,11 @@ def run_server( sys.exit(1) else: print( - "Unable to connect to DB. DATABASE_URL found in environment, but the prisma CLI is neither on " - "PATH nor importable as a package." + "\033[1;31mLiteLLM Proxy: a database URL is set but the prisma CLI is neither on PATH nor importable " + "as a package, so the database cannot be set up. Install it with `pip install 'litellm[extra_proxy]'` " + "or run a shipped LiteLLM image.\033[0m" ) + sys.exit(1) pgbouncer_settings: Final = PgBouncerSettings() upstream_database_url: Final = os.getenv("DATABASE_URL") if pgbouncer_settings.enabled and upstream_database_url is not None: diff --git a/litellm/proxy/proxy_server.py b/litellm/proxy/proxy_server.py index 5d47cceee02..e406e6faec4 100644 --- a/litellm/proxy/proxy_server.py +++ b/litellm/proxy/proxy_server.py @@ -335,6 +335,7 @@ from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler from litellm.llms.openai_like.model_info import MODEL_INFO_REFRESH_SECONDS from litellm.llms.vertex_ai.vertex_llm_base import VertexBase from litellm.proxy._experimental.mcp_server.byok_credential_cache import byok_credential_cache +from litellm.proxy._experimental.mcp_server.stdio_gate import MCP_STDIO_ENABLED_ENV_VAR, is_mcp_stdio_flag_key from litellm.proxy._lazy_features import attach_lazy_features, reserve_lazy_slot from litellm.proxy._types import * from litellm.proxy.analytics_endpoints.analytics_endpoints import ( @@ -5407,6 +5408,7 @@ class ProxyConfig: self._last_cyberark_config: dict[str, object] | None = None # mutable-ok: change-detection cache self._last_cleanup_schedule_attempt: tuple[object, ...] | None = None self._cleanup_reschedule_failed: bool = False + self._warned_db_mcp_stdio_flag_ignored: bool = False self._cyberark_boot_env: dict[str, str | None] | None = None # mutable-ok: deployment env snapshot, set once self.worker_registry: list[WorkerRegistryEntry] = [] self.config_sync_subscriber: ConfigSyncSubscriber | None = None @@ -6190,6 +6192,12 @@ class ProxyConfig: if key in self._BLOCKED_ENV_KEYS: verbose_proxy_logger.warning("Skipping blocked environment variable key: %s", key) continue + if isinstance(key, str) and is_mcp_stdio_flag_key(key): + verbose_proxy_logger.warning( + "Ignoring %s set in the config file. Set it in the proxy's environment instead", + MCP_STDIO_ENABLED_ENV_VAR, + ) + continue ######################################################### # handles this scenario: # ```yaml @@ -7591,6 +7599,14 @@ class ProxyConfig: """ decrypted_env_vars: Final = {} for k, v in environment_variables.items(): + if isinstance(k, str) and is_mcp_stdio_flag_key(k): + if not self._warned_db_mcp_stdio_flag_ignored: + verbose_proxy_logger.warning( + "Ignoring %s stored in the database. Set it in the proxy's environment instead", + MCP_STDIO_ENABLED_ENV_VAR, + ) + self._warned_db_mcp_stdio_flag_ignored = True + continue try: decrypted_value = decrypt_value_helper(value=v, key=k, return_original_value=return_original_value) if decrypted_value is not None: diff --git a/litellm/proxy/schema.prisma b/litellm/proxy/schema.prisma index 6f285e9dc39..aba89526cf6 100644 --- a/litellm/proxy/schema.prisma +++ b/litellm/proxy/schema.prisma @@ -1744,6 +1744,27 @@ model LiteLLM_AutoRouterUserSession { @@index([user_id, last_turn_at], map: "idx_autorouter_user_session_user_last_turn") } +// Auto-routed requests per UTC request day and router: the selected-day money behind the +// auto-router usage view. Written in the same statement as the session rollup, so a day row +// and its session row never disagree; corrected in the same transaction as late baselines. +model LiteLLM_AutoRouterDailySpend { + date String + api_key String + user_id String + router_name String + router_type String + turns Int @default(0) + spend Float @default(0) + saved_spend Float @default(0) + savings_estimated_turns Int @default(0) + savings_estimated_actual_spend Float @default(0) + savings_estimated_saved_spend Float @default(0) + classifier_cost Float @default(0) + classifier_cost_recorded_turns Int @default(0) + + @@id([date, api_key, user_id, router_name, router_type]) +} + // Shadow eval: evaluation of an auto-router against one or more keys' live traffic, in // either direction. forward duplicates the requests the keys did not route through the // router through it, answering whether they should adopt it; reverse duplicates the diff --git a/litellm/proxy/spend_tracking/spend_management_endpoints.py b/litellm/proxy/spend_tracking/spend_management_endpoints.py index 3102fc63cf4..48cc684549f 100644 --- a/litellm/proxy/spend_tracking/spend_management_endpoints.py +++ b/litellm/proxy/spend_tracking/spend_management_endpoints.py @@ -3,8 +3,8 @@ import collections import json import os from collections.abc import Mapping, Sequence -from dataclasses import dataclass from datetime import date, datetime, timedelta, timezone +from functools import partial from itertools import groupby from types import MappingProxyType from typing import ( @@ -37,6 +37,18 @@ from litellm.constants import ( from litellm.litellm_core_utils.classifier_logging import classifier_audit_fields, classifier_input_snapshot from litellm.proxy._types import * from litellm.proxy._types import ProviderBudgetResponse, ProviderBudgetResponseObject +from litellm.proxy.auth.authorization import ( + AllRows, + OwnedRows, + ReadScope, + can_read_log_owner, + can_read_team_logs, + resolve_owned_read_scope, +) +from litellm.proxy.auth.authorization_dependencies import ( + LogTeamLookup, + LogTeamLookupDependency, +) from litellm.proxy.auth.user_api_key_auth import user_api_key_auth from litellm.proxy.litellm_pre_call_utils import LiteLLMProxyRequestSetup from litellm.proxy.spend_tracking.spend_capture_rate import ( @@ -403,11 +415,6 @@ async def _find_team_row(prisma_client: PrismaClient, team_id: str) -> _Supports return await _team_table(prisma_client).find_unique(where={"team_id": team_id}) -async def _find_team_rows(prisma_client: PrismaClient, team_ids: Sequence[str]) -> Sequence[_SupportsModelDump]: - """Read team rows as Prisma model instances.""" - return await _team_table(prisma_client).find_many(where={"team_id": {"in": team_ids}}) - - @router.get( "/spend/keys", tags=["Budget & Spend Tracking"], @@ -2474,6 +2481,7 @@ def _build_spend_log_search_condition( ) async def ui_view_spend_logs( request: Request, + log_team_lookup: LogTeamLookupDependency, api_key: str | None = fastapi.Query( default=None, description="Get spend logs based on api key", @@ -2775,16 +2783,8 @@ async def ui_view_spend_logs( and team_id is None and (is_request_id_lookup or _can_user_view_spend_log(user_api_key_dict=user_api_key_dict)) ) - permitted_team_ids: Final = ( - await _get_permitted_team_ids_for_spend_logs_or_empty( - prisma_client=prisma_client, - user_api_key_dict=user_api_key_dict, - ) - if user_scope_applies - else () - ) - explicit_user_requires_caller_scope: Final = ( - user_scope_applies and not permitted_team_ids and user_id is not None + read_scope: Final = ( + await _spend_log_read_scope(user_api_key_dict, log_team_lookup) if user_scope_applies else AllRows() ) if not is_admin_view: if team_id is not None: @@ -2799,22 +2799,6 @@ async def ui_view_spend_logs( detail={"error": f"Not authorized to view team spend for team_id={team_id}"}, ) where_conditions["team_id"] = team_id - elif user_scope_applies: - if permitted_team_ids: - if user_id is None: - where_conditions.pop("user", None) - where_conditions["OR"] = [ - {"user": user_api_key_dict.user_id}, - {"team_id": {"in": permitted_team_ids}}, - ] - else: - if user_id is None: - where_conditions["user"] = user_api_key_dict.user_id - else: - where_conditions["AND"] = where_conditions.get("AND", []) + [ - {"user": user_api_key_dict.user_id} - ] - where_conditions.pop("team_id", None) # Calculate skip value for pagination skip: Final = (page - 1) * page_size @@ -2874,17 +2858,11 @@ async def ui_view_spend_logs( sql_params.append(request_id_filter) p += 1 - # Multi-team OR filter: (user = $X OR team_id = ANY($Y)) - if permitted_team_ids: - or_clause: Final = f'("user" = ${p} OR team_id = ANY(${p + 1}::text[]))' - sql_params.append(user_api_key_dict.user_id) - sql_params.append(permitted_team_ids) - p += 2 - sql_conditions.append(or_clause) - elif explicit_user_requires_caller_scope: - sql_conditions.append(f'"user" = ${p}') - sql_params.append(user_api_key_dict.user_id) - p += 1 + scope_clause, scope_params = read_scope_sql(read_scope, p) + if scope_clause: + sql_conditions.append(scope_clause) + sql_params.extend(scope_params) + p += len(scope_params) if session_id is not None and isinstance(session_id, str): like_escaped_session_id: Final = session_id.replace("\\", "\\\\").replace("%", "\\%").replace("_", "\\_") @@ -3390,6 +3368,7 @@ async def _resolve_request_response_payload( ) async def ui_view_request_response_for_request_id( request_id: str, + log_team_lookup: LogTeamLookupDependency, start_date: str | None = fastapi.Query( default=None, description="Time from which to start viewing key spend", @@ -3442,6 +3421,7 @@ async def ui_view_request_response_for_request_id( user_api_key_dict=user_api_key_dict, request_id=request_id, caller_is_admin=caller_is_admin, + log_team_lookup=log_team_lookup, ) ) stored_request_id: Final = _stored_request_id(spend_log_row, request_id) @@ -4512,6 +4492,7 @@ async def ui_get_spend_by_tags( }, ) async def ui_view_session_spend_logs( + log_team_lookup: LogTeamLookupDependency, session_id: str = fastapi.Query( description="Get all spend logs for a particular session", ), @@ -4549,36 +4530,16 @@ async def ui_view_session_spend_logs( detail="Database not connected", ) - if _is_admin_view_safe(user_api_key_dict=user_api_key_dict): - scope_sql = "" - scope_params = () - where_conditions = {"session_id": session_id} - else: - try: - permitted_team_ids = ( - await _get_permitted_team_ids_for_spend_logs( - prisma_client=prisma_client, - user_api_key_dict=user_api_key_dict, - ) - if _can_user_view_spend_log(user_api_key_dict=user_api_key_dict) - else [] - ) - except Exception: # noqa: BLE001 # mirror /spend/logs/ui: failed team lookup falls back to own-logs-only scope - permitted_team_ids = [] - if permitted_team_ids: - scope_sql = ' AND ("user" = $4 OR team_id = ANY($5::text[]))' - scope_params = (user_api_key_dict.user_id, permitted_team_ids) - where_conditions = { - "session_id": session_id, - "OR": [ - {"user": user_api_key_dict.user_id}, - {"team_id": {"in": permitted_team_ids}}, - ], - } - else: - scope_sql = ' AND "user" = $4' - scope_params = (user_api_key_dict.user_id,) - where_conditions = {"session_id": session_id, "user": user_api_key_dict.user_id} + read_scope: Final = ( + AllRows() + if _is_admin_view_safe(user_api_key_dict=user_api_key_dict) + else await _spend_log_read_scope(user_api_key_dict, log_team_lookup) + if _can_user_view_spend_log(user_api_key_dict=user_api_key_dict) + else OwnedRows(user_api_key_dict.user_id) + ) + scope_clause, scope_params = read_scope_sql(read_scope, 4) + scope_sql: Final = f" AND {scope_clause}" if scope_clause else "" + where_conditions: Final = {"session_id": session_id, **_read_scope_where(read_scope)} # Calculate pagination offsets skip: Final = (page - 1) * page_size @@ -4859,22 +4820,12 @@ async def _can_team_member_view_log( Returns True if the team exists and the user is either a team admin or a team member with the ``/spend/logs`` permission. """ - from litellm.proxy.management.teams.access import is_team_admin - from litellm.proxy.management_endpoints.common_utils import _team_member_has_permission - if team_id is None: return False team_row: Final = await _find_team_row(prisma_client, team_id) if team_row is None: return False - team_obj: Final = LiteLLM_TeamTable.model_validate(team_row.model_dump()) - if is_team_admin(user_api_key_dict=user_api_key_dict, team_obj=team_obj): - return True - return _team_member_has_permission( - user_api_key_dict=user_api_key_dict, - team_obj=team_obj, - permission=KeyManagementRoutes.SPEND_LOGS.value, - ) + return can_read_team_logs(user_api_key_dict, LiteLLM_TeamTable.model_validate(team_row.model_dump())) def _can_user_view_spend_log(user_api_key_dict: UserAPIKeyAuth) -> bool: @@ -4899,15 +4850,12 @@ async def _user_can_view_spend_log_owner( owner_user: str | None, owner_team_id: str | None, ) -> bool: - if owner_user is not None and owner_user == user_api_key_dict.user_id: - return True - if owner_team_id: - return await _can_team_member_view_log( - prisma_client=prisma_client, - user_api_key_dict=user_api_key_dict, - team_id=owner_team_id, - ) - return False + return await can_read_log_owner( + user_api_key_dict.user_id, + owner_user, + owner_team_id, + partial(_can_team_member_view_log, prisma_client, user_api_key_dict), + ) def _spend_log_forbidden(request_id: str) -> HTTPException: @@ -4940,44 +4888,50 @@ async def _assert_user_can_view_request_id( raise _spend_log_forbidden(request_id) -@dataclass(frozen=True, slots=True) -class _SpendLogViewer: - user_id: str | None - team_ids: tuple[str, ...] - - -async def _spend_log_viewer(prisma_client: PrismaClient, user_api_key_dict: UserAPIKeyAuth) -> _SpendLogViewer: - return _SpendLogViewer( - user_id=user_api_key_dict.user_id, - team_ids=await _get_permitted_team_ids_for_spend_logs_or_empty( - prisma_client=prisma_client, - user_api_key_dict=user_api_key_dict, - ), +async def _spend_log_read_scope(user_api_key_dict: UserAPIKeyAuth, log_team_lookup: LogTeamLookup) -> OwnedRows: + return await resolve_owned_read_scope( + user_api_key_dict.user_id, + partial(log_team_lookup, user_api_key_dict), ) -def _viewer_scope_clause(viewer: _SpendLogViewer | None) -> tuple[str, tuple[object, ...]]: - match viewer: - case None: - return ("", ()) - case _SpendLogViewer(user_id=user_id, team_ids=()): - return (' AND "user" = $2', (user_id,)) - case _SpendLogViewer(user_id=user_id, team_ids=team_ids): - return (' AND ("user" = $2 OR team_id = ANY($3::text[]))', (user_id, team_ids)) +def read_scope_sql(scope: ReadScope, next_param: int) -> tuple[str, tuple[object, ...]]: + if isinstance(scope, AllRows): + return "", () + if scope.user_id is not None and scope.team_ids: + return ( + f'("user" = ${next_param} OR team_id = ANY(${next_param + 1}::text[]))', + (scope.user_id, scope.team_ids), + ) + if scope.user_id is not None: + return f'"user" = ${next_param}', (scope.user_id,) + if scope.team_ids: + return f"team_id = ANY(${next_param}::text[])", (scope.team_ids,) + return "FALSE", () -def _spend_log_payload_query(request_id: str, viewer: _SpendLogViewer | None) -> tuple[str, tuple[object, ...]]: +def _read_scope_where(scope: ReadScope) -> Mapping[str, object]: + if isinstance(scope, AllRows): + return {} + user_grant: Final = ({"user": scope.user_id},) if scope.user_id is not None else () + team_grant: Final = ({"team_id": {"in": list(scope.team_ids)}},) if scope.team_ids else () + grants: Final = user_grant + team_grant + return grants[0] if len(grants) == 1 else {"OR": list(grants)} + + +def _spend_log_payload_query(request_id: str, scope: ReadScope) -> tuple[str, tuple[object, ...]]: """ Fetch the one row an id lookup resolves to, preferring the exact ``request_id`` match over rows that merely carry the id as their client-set ``litellm_call_id``. A non-admin viewer only ever gets rows they own or rows of a team they may view. """ - scope, scope_params = _viewer_scope_clause(viewer) + scope_clause, scope_params = read_scope_sql(scope, 2) + scope_sql: Final = f" AND {scope_clause}" if scope_clause else "" return ( f""" SELECT request_id, messages, response, proxy_server_request, metadata, "user", team_id FROM "LiteLLM_SpendLogs" - WHERE (request_id = $1 OR litellm_call_id = $1){scope} + WHERE (request_id = $1 OR litellm_call_id = $1){scope_sql} ORDER BY (request_id = $1) DESC LIMIT 1 """, @@ -4990,6 +4944,7 @@ async def _resolve_spend_log_payload_row( user_api_key_dict: UserAPIKeyAuth, request_id: str, caller_is_admin: bool, + log_team_lookup: LogTeamLookup, ) -> Mapping[str, object] | None: """ Resolve an id lookup to the caller's own spend-log row before any payload @@ -4998,8 +4953,8 @@ async def _resolve_spend_log_payload_row( that id is only the caller's ``litellm_call_id``; the row's stored ``request_id`` is the key that names the caller's own request. """ - viewer: Final = None if caller_is_admin else await _spend_log_viewer(prisma_client, user_api_key_dict) - sql_query, sql_params = _spend_log_payload_query(request_id, viewer) + scope: Final = AllRows() if caller_is_admin else await _spend_log_read_scope(user_api_key_dict, log_team_lookup) + sql_query, sql_params = _spend_log_payload_query(request_id, scope) rows: Final[Sequence[Mapping[str, object]] | None] = await _query_raw_or_none(prisma_client, sql_query, *sql_params) if not rows: return None @@ -5075,57 +5030,3 @@ async def _assert_user_owns_cold_storage_payload( owner_user, owner_team_id = _cold_storage_payload_owner(payload) if not await _user_can_view_spend_log_owner(prisma_client, user_api_key_dict, owner_user, owner_team_id): raise _spend_log_forbidden(request_id) - - -async def _get_permitted_team_ids_for_spend_logs( - prisma_client: PrismaClient, - user_api_key_dict: UserAPIKeyAuth, -) -> list[str]: - """ - Return team IDs where the user is either a team admin or has the - ``/spend/logs`` permission, allowing them to view team-wide spend logs. - """ - # Imported here to avoid circular import: proxy_server imports this module. - from litellm.proxy.auth.auth_checks import get_user_object - from litellm.proxy.management.teams.access import is_team_admin - from litellm.proxy.management_endpoints.common_utils import _team_member_has_permission - from litellm.proxy.proxy_server import proxy_logging_obj, user_api_key_cache - - user_obj: Final = await get_user_object( - user_id=user_api_key_dict.user_id, - prisma_client=prisma_client, - user_api_key_cache=user_api_key_cache, - user_id_upsert=False, - proxy_logging_obj=proxy_logging_obj, - ) - if user_obj is None or not user_obj.teams: - return [] - - team_rows: Final = await _find_team_rows(prisma_client, user_obj.teams) - - permitted: Final[list[str]] = [] - for team_row in team_rows: - team_obj = LiteLLM_TeamTable.model_validate(team_row.model_dump()) - if is_team_admin(user_api_key_dict=user_api_key_dict, team_obj=team_obj) or _team_member_has_permission( - user_api_key_dict=user_api_key_dict, - team_obj=team_obj, - permission=KeyManagementRoutes.SPEND_LOGS.value, - ): - permitted.append(team_obj.team_id) - return permitted - - -async def _get_permitted_team_ids_for_spend_logs_or_empty( - prisma_client: PrismaClient, - user_api_key_dict: UserAPIKeyAuth, -) -> tuple[str, ...]: - """Resolve permitted teams once, falling back to the caller's own-user scope.""" - try: - return tuple( - await _get_permitted_team_ids_for_spend_logs( - prisma_client=prisma_client, - user_api_key_dict=user_api_key_dict, - ) - ) - except Exception: - return () diff --git a/litellm/proxy/tracing_endpoints.py b/litellm/proxy/tracing_endpoints.py index e4d46560f08..046bcc9f704 100644 --- a/litellm/proxy/tracing_endpoints.py +++ b/litellm/proxy/tracing_endpoints.py @@ -10,6 +10,7 @@ GET /v1/traces/{trace_id}/spans/{span_id} SpanDetail import time from collections.abc import Mapping from dataclasses import dataclass +from functools import partial from http.client import responses from types import MappingProxyType from typing import Annotated, Final @@ -20,16 +21,20 @@ from pydantic import BaseModel, ConfigDict from litellm._logging import verbose_proxy_logger from litellm.constants import OTLP_RETRY_AFTER_SECONDS from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth +from litellm.proxy.auth.authorization import AllRows, ReadScope, resolve_trace_read_scope +from litellm.proxy.auth.authorization_dependencies import LogTeamLookupDependency from litellm.proxy.auth.user_api_key_auth import user_api_key_auth from litellm.proxy.common_utils.http_parsing_utils import is_otlp_trace_request from litellm.proxy.tracing_runtime import provide_receiver, require_receiver -from litellm.rust_bridge.traces import ClickHouseStorage, QueryScope +from litellm.rust_bridge.trace_query_responses import TraceQueryHelp, TraceSQLResponse +from litellm.rust_bridge.traces import AllQueryScope, ClickHouseStorage, OwnedQueryScope, QueryScope from litellm.tracing import ( Tenant, TraceReceiver, TracingPayloadTooLargeError, ) from litellm.tracing.decode import InvalidOTLPPayloadError, encode_otlp_response +from litellm.tracing.store import AmbiguousTraceError from litellm.tracing.types import SpanDetail, SpanErrorPage, Trace, TracePage, TraceScope router = APIRouter(tags=["agent tracing"]) @@ -40,14 +45,14 @@ MS_PER_DAY: Final = 24 * 60 * 60 * 1000 @dataclass(frozen=True, slots=True) class TraceAccessContext: receiver: TraceReceiver | None - read_scope: TraceScope | None + read_scope: ReadScope | None write_tenant: Tenant | None def reader(self) -> tuple[TraceReceiver, TraceScope]: tracing: Final = require_receiver(self.receiver) if self.read_scope is None: raise HTTPException(status_code=403, detail="Not allowed to view agent traces") - return tracing, self.read_scope + return tracing, _trace_scope(self.read_scope) def writer(self) -> tuple[TraceReceiver, Tenant]: if self.write_tenant is None: @@ -58,19 +63,24 @@ class TraceAccessContext: async def provide_trace_access( auth: Annotated[UserAPIKeyAuth, Depends(user_api_key_auth)], tracing: Annotated[TraceReceiver | None, Depends(provide_receiver)], + log_team_lookup: LogTeamLookupDependency, ) -> TraceAccessContext: - tenant: Final = Tenant(team_id=auth.team_id or "", api_key_hash=auth.token or "", org_id=auth.org_id or "") - match auth.user_role: - case LitellmUserRoles.PROXY_ADMIN: - return TraceAccessContext(tracing, TraceScope(team_ids=(), api_key_hash=""), tenant) - case LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY: - return TraceAccessContext(tracing, TraceScope(team_ids=(), api_key_hash=""), None) - case _ if auth.team_id: - return TraceAccessContext(tracing, TraceScope(team_ids=(auth.team_id,), api_key_hash=""), tenant) - case _ if auth.token: - return TraceAccessContext(tracing, TraceScope(team_ids=("",), api_key_hash=auth.token), tenant) - case _: - return TraceAccessContext(tracing, None, tenant) + tenant: Final = Tenant( + team_id=auth.team_id or "", api_key_hash=auth.token or "", org_id=auth.org_id or "", user_id=auth.user_id or "" + ) + write_tenant: Final = None if auth.user_role == LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY else tenant + read_scope: Final = await resolve_trace_read_scope(auth, partial(log_team_lookup, auth)) + return TraceAccessContext(tracing, read_scope, write_tenant) + + +def _trace_scope(scope: ReadScope) -> TraceScope: + if isinstance(scope, AllRows): + return TraceScope(all_teams=1, user_id="", team_ids=()) + return TraceScope( + all_teams=0, + user_id=scope.user_id or "", + team_ids=scope.team_ids, + ) def otlp_error_response( @@ -148,7 +158,7 @@ class TraceQueryRequest(BaseModel): @dataclass(frozen=True, slots=True) class TraceQueryAccess: storage: ClickHouseStorage - scope: QueryScope + scope: ReadScope secret: str @@ -160,37 +170,36 @@ def provide_trace_query_secret() -> str: return master_key -def trace_query_scope(auth: UserAPIKeyAuth) -> QueryScope: - if auth.user_role in (LitellmUserRoles.PROXY_ADMIN, LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY): - return {"kind": "admin"} - if auth.project_id and auth.token: - return {"kind": "key", "team_id": auth.team_id or "", "api_key_hash": auth.token} - if auth.project_id: - raise HTTPException(status_code=403, detail="Project trace SQL queries require a project key") - if auth.team_id: - return {"kind": "team", "team_id": auth.team_id} - if auth.token: - return {"kind": "key", "team_id": "", "api_key_hash": auth.token} - raise HTTPException(status_code=403, detail="Trace SQL queries require an authenticated trace scope") +def trace_query_scope(scope: ReadScope) -> QueryScope: + if isinstance(scope, AllRows): + return AllQueryScope(kind="all") + return OwnedQueryScope( + kind="owned", + user_id=scope.user_id or "", + team_ids=scope.team_ids, + ) async def provide_trace_query_access( auth: Annotated[UserAPIKeyAuth, Depends(user_api_key_auth)], tracing: Annotated[TraceReceiver | None, Depends(provide_receiver)], secret: Annotated[str, Depends(provide_trace_query_secret)], + log_team_lookup: LogTeamLookupDependency, ) -> TraceQueryAccess: - return TraceQueryAccess(require_receiver(tracing).store.storage, trace_query_scope(auth), secret) + storage: Final = require_receiver(tracing).store.storage + scope: Final = await resolve_trace_read_scope(auth, partial(log_team_lookup, auth)) + if scope is None: + raise HTTPException(status_code=403, detail="Not allowed to view logs") + return TraceQueryAccess(storage, scope, secret) -@router.post("/v1/traces/query") +@router.post("/v1/traces/query", response_model=TraceSQLResponse, response_model_exclude_unset=True) async def query_agent_traces( body: TraceQueryRequest, access: Annotated[TraceQueryAccess, Depends(provide_trace_query_access)], -) -> Response: +) -> TraceSQLResponse: try: - return Response( - content=await access.storage.query_sql(body.sql, access.scope, access.secret), media_type="application/json" - ) + return await access.storage.query_sql(body.sql, trace_query_scope(access.scope), access.secret) except ValueError as error: raise HTTPException(status_code=400, detail=str(error)) from error except RuntimeError as error: @@ -198,14 +207,12 @@ async def query_agent_traces( raise HTTPException(status_code=503, detail="Trace SQL query failed or exceeded reader limits") from error -@router.get("/v1/traces/query/help") +@router.get("/v1/traces/query/help", response_model=TraceQueryHelp, response_model_exclude_unset=True) async def help_agent_trace_queries( access: Annotated[TraceQueryAccess, Depends(provide_trace_query_access)], -) -> Response: +) -> TraceQueryHelp: try: - return Response( - content=await access.storage.query_help(access.scope, access.secret), media_type="application/json" - ) + return await access.storage.query_help(trace_query_scope(access.scope), access.secret) except RuntimeError as error: verbose_proxy_logger.warning("Trace query help unavailable: %s", error) raise HTTPException(status_code=503, detail="Trace query help is temporarily unavailable") from error @@ -218,7 +225,10 @@ async def get_agent_trace( trace_ref: Annotated[str, Query()] = "", ) -> Trace: tracing, scope = context.reader() - trace: Final = await tracing.get_trace(trace_id, scope, trace_ref) + try: + trace: Final = await tracing.get_trace(trace_id, scope, trace_ref) + except AmbiguousTraceError as error: + raise HTTPException(status_code=400, detail=str(error)) from error if trace is None: raise HTTPException(status_code=404, detail=f"Trace {trace_id} not found") return trace @@ -232,7 +242,10 @@ async def get_agent_trace_span( trace_ref: Annotated[str, Query()] = "", ) -> SpanDetail: tracing, scope = context.reader() - span: Final = await tracing.get_span(trace_id, span_id, scope, trace_ref) + try: + span: Final = await tracing.get_span(trace_id, span_id, scope, trace_ref) + except AmbiguousTraceError as error: + raise HTTPException(status_code=400, detail=str(error)) from error if span is None: raise HTTPException(status_code=404, detail=f"Span {span_id} not found") return span diff --git a/litellm/proxy/utils.py b/litellm/proxy/utils.py index 817231bc0b9..5828ed94984 100644 --- a/litellm/proxy/utils.py +++ b/litellm/proxy/utils.py @@ -266,6 +266,7 @@ if TYPE_CHECKING: from litellm.models.team import LiteLLM_TeamTableCachedObj from litellm.proxy.db.autorouter_session_rollup import AutoRouterTurnTransaction from litellm.proxy.db.baseline_accounting import BaselineAccountingRecord + from litellm.proxy.db.model_usage_rollup import ModelUsageTransaction from litellm.proxy.db.spend_log_tool_index import ToolUsageTransaction from litellm.repositories.prisma_protocols import TableActions from litellm.types.proxy.policy_engine.pipeline_types import GuardrailPipeline @@ -4400,6 +4401,8 @@ class PrismaClient: spend_log_write_lock = asyncio.Lock() tool_usage_transactions: list["ToolUsageTransaction"] = [] _tool_usage_transactions_lock = asyncio.Lock() + model_usage_transactions: ClassVar[list["ModelUsageTransaction"]] = [] + _model_usage_transactions_lock = asyncio.Lock() autorouter_turn_transactions: ClassVar[list["AutoRouterTurnTransaction"]] = [] _autorouter_turn_transactions_lock = asyncio.Lock() @@ -7508,6 +7511,8 @@ async def _total_queued_spend_transactions(prisma_client: PrismaClient) -> int: spend_queue_size: Final = len(prisma_client.spend_log_transactions) async with prisma_client._tool_usage_transactions_lock: tool_queue_size: Final = len(prisma_client.tool_usage_transactions) + async with prisma_client._model_usage_transactions_lock: + model_usage_queue_size: Final = len(prisma_client.model_usage_transactions) async with prisma_client._autorouter_turn_transactions_lock: autorouter_queue_size: Final = len(prisma_client.autorouter_turn_transactions) from litellm.proxy.db.shadow_eval_funnel import pending_shadow_eval_funnel_events @@ -7517,6 +7522,7 @@ async def _total_queued_spend_transactions(prisma_client: PrismaClient) -> int: return ( spend_queue_size + tool_queue_size + + model_usage_queue_size + autorouter_queue_size + baseline_queue_size + pending_shadow_eval_funnel_events() @@ -7652,6 +7658,20 @@ async def _run_spend_logs_job( tool_tracking_err, ) + async with prisma_client._model_usage_transactions_lock: + model_usage_to_process: Final = prisma_client.model_usage_transactions + prisma_client.model_usage_transactions = [] + try: + from litellm.proxy.db.model_usage_rollup import flush_model_usage_transactions + + await flush_model_usage_transactions(prisma_client=prisma_client, transactions=model_usage_to_process) + except Exception as model_usage_err: + verbose_proxy_logger.error( + "Spend tracking - model usage flush failed; %s model usage transactions dropped: %s", + len(model_usage_to_process), + model_usage_err, + ) + await flush_baseline_accounting(prisma_client) async with prisma_client._autorouter_turn_transactions_lock: diff --git a/litellm/repositories/verification_token_repository.py b/litellm/repositories/verification_token_repository.py index d02c2114136..b20fb47306e 100644 --- a/litellm/repositories/verification_token_repository.py +++ b/litellm/repositories/verification_token_repository.py @@ -8,6 +8,7 @@ from datetime import datetime from types import TracebackType from typing import TYPE_CHECKING, Final, Protocol +from litellm.constants import UI_SESSION_TOKEN_TEAM_ID from litellm.models.verification_token import ( LiteLLM_VerificationToken, ) @@ -123,6 +124,37 @@ class VerificationTokenRepository(BaseRepository[LiteLLM_VerificationToken]): records: Final[Sequence[PrismaVerificationToken]] = await self.table.find_many(where={"user_id": user_id}) return self._to_model_list(records) + async def find_newest_reusable_llm_api_key( + self, user_id: str, team_id: str | None + ) -> LiteLLM_VerificationToken | None: + records: Final[Sequence[PrismaVerificationToken]] = await self.table.find_many( + where={ + "user_id": user_id, + "team_id": team_id, + "expires": None, + "AND": [ + {"OR": [{"blocked": False}, {"blocked": None}]}, + { + "OR": [ + {"team_id": None}, + {"team_id": {"not": UI_SESSION_TOKEN_TEAM_ID}}, + ] + }, + { + "OR": [ + {"allowed_routes": {"is_empty": True}}, + {"allowed_routes": {"has": "llm_api_routes"}}, + ] + }, + ], + }, + order={"created_at": "desc"}, + ) + return next( + (key for key in self._to_model_list(records) if key.metadata.get("auto_registered") is not True), + None, + ) + async def find_by_team_id(self, team_id: str) -> list[LiteLLM_VerificationToken]: """Find all tokens belonging to a team.""" records: Final[Sequence[PrismaVerificationToken]] = await self.table.find_many(where={"team_id": team_id}) diff --git a/litellm/responses/litellm_completion_transformation/reasoning_items.py b/litellm/responses/litellm_completion_transformation/reasoning_items.py new file mode 100644 index 00000000000..ab982222902 --- /dev/null +++ b/litellm/responses/litellm_completion_transformation/reasoning_items.py @@ -0,0 +1,73 @@ +import json +import uuid +from collections.abc import Iterator, Mapping, Sequence +from typing import Final + +from pydantic import BaseModel, TypeAdapter, ValidationError + +REASONING_ITEM_ID_PREFIX: Final = "rs_" +_JSON_LIST: Final = TypeAdapter(list[object]) +_JSON_OBJECT: Final = TypeAdapter(dict[str, object]) + + +def mint_reasoning_item_id() -> str: + return f"{REASONING_ITEM_ID_PREFIX}{uuid.uuid4()}" + + +def is_verifiable_thinking_block(block: Mapping[str, object]) -> bool: + block_type: Final = block.get("type") + if block_type == "thinking": + return bool(block.get("signature")) + if block_type == "redacted_thinking": + return bool(block.get("data")) + return False + + +def encode_thinking_blocks(thinking_blocks: Sequence[Mapping[str, object]]) -> str | None: + preserved: Final = [block for block in thinking_blocks if is_verifiable_thinking_block(block)] + return json.dumps(preserved, separators=(",", ":")) if preserved else None + + +def _json_objects(members: Sequence[object]) -> Iterator[Mapping[str, object]]: + for member in members: + try: + yield _JSON_OBJECT.validate_python(member) + except ValidationError: + continue + + +def decode_thinking_blocks(encrypted_content: object) -> tuple[Mapping[str, object], ...] | None: + if not isinstance(encrypted_content, str) or not encrypted_content.strip(): + return None + try: + decoded: Final = _JSON_LIST.validate_json(encrypted_content) + except ValidationError: + return None + blocks: Final = tuple(block for block in _json_objects(decoded) if is_verifiable_thinking_block(block)) + return blocks or None + + +def is_minted_reasoning_item_id(item_id: object) -> bool: + if not isinstance(item_id, str) or not item_id.startswith(REASONING_ITEM_ID_PREFIX): + return False + suffix: Final = item_id.removeprefix(REASONING_ITEM_ID_PREFIX) + try: + parsed: Final = uuid.UUID(suffix) + except ValueError: + return False + return parsed.version == 4 and str(parsed) == suffix + + +def is_litellm_minted_reasoning_item(item: object) -> bool: + try: + fields: Final = _JSON_OBJECT.validate_python( + item.model_dump(exclude_none=True) if isinstance(item, BaseModel) else item + ) + except ValidationError: + return False + if fields.get("type") != "reasoning": + return False + return ( + is_minted_reasoning_item_id(fields.get("id")) + or decode_thinking_blocks(fields.get("encrypted_content")) is not None + ) diff --git a/litellm/responses/litellm_completion_transformation/streaming_iterator.py b/litellm/responses/litellm_completion_transformation/streaming_iterator.py index 3d6e4bf25d3..c215e3f8395 100644 --- a/litellm/responses/litellm_completion_transformation/streaming_iterator.py +++ b/litellm/responses/litellm_completion_transformation/streaming_iterator.py @@ -11,6 +11,7 @@ from litellm.responses.litellm_completion_transformation.custom_tools import ( is_custom_tool_call, serialize_tool_call_arguments, ) +from litellm.responses.litellm_completion_transformation.reasoning_items import mint_reasoning_item_id from litellm.responses.litellm_completion_transformation.transformation import ( LiteLLMCompletionResponsesConfig, ) @@ -944,7 +945,7 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator): if (hasattr(delta, "reasoning_content") and delta.reasoning_content) or _delta_has_signed_thinking_block(delta): self._reasoning_active = True if self._cached_reasoning_item_id is None: - self._cached_reasoning_item_id = f"rs_{uuid.uuid4()}" + self._cached_reasoning_item_id = mint_reasoning_item_id() self._reasoning_item_id = self._cached_reasoning_item_id event = OutputItemAddedEvent( @@ -1027,7 +1028,9 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator): # Ensure we have a valid reasoning_item_id self._cached_reasoning_item_id = ( - self._reasoning_item_id or self._cached_reasoning_item_id or f"rs_{uuid.uuid4()}" + self._reasoning_item_id + or self._cached_reasoning_item_id + or mint_reasoning_item_id() ) reasoning_item_id = self._cached_reasoning_item_id @@ -1186,7 +1189,7 @@ class LiteLLMCompletionStreamingIterator(ResponsesAPIStreamingIterator): reasoning_content: Final = chunk.choices[0].delta.reasoning_content if self._cached_reasoning_item_id is None: - self._cached_reasoning_item_id = f"rs_{uuid.uuid4()}" + self._cached_reasoning_item_id = mint_reasoning_item_id() return ReasoningSummaryTextDeltaEvent( type=ResponsesAPIStreamEvents.REASONING_SUMMARY_TEXT_DELTA, diff --git a/litellm/responses/litellm_completion_transformation/transformation.py b/litellm/responses/litellm_completion_transformation/transformation.py index 2fbbebe320f..e1c7cd4b890 100644 --- a/litellm/responses/litellm_completion_transformation/transformation.py +++ b/litellm/responses/litellm_completion_transformation/transformation.py @@ -105,6 +105,7 @@ from .custom_tools import ( unwrap_custom_tool_arguments, validated_allowed_callers, ) +from .reasoning_items import decode_thinking_blocks, encode_thinking_blocks, mint_reasoning_item_id NamespaceNameMap: TypeAlias = Mapping[str, tuple[str, str]] NamespaceTool: TypeAlias = Mapping[str, object] @@ -1494,39 +1495,16 @@ class LiteLLMCompletionResponsesConfig: Returns None for anything this deployment did not write, so a genuinely opaque blob is still skipped rather than forwarded as garbage. """ - encrypted_content: Final[object] = input_item.get("encrypted_content") - if not isinstance(encrypted_content, str) or not encrypted_content.strip(): + decoded: Final = decode_thinking_blocks(input_item.get("encrypted_content")) + if decoded is None: return None - try: - decoded: Final[object] = cast(object, json.loads(encrypted_content)) # cast-ok: json.loads returns Any - except ValueError: - return None - if not isinstance(decoded, list): - return None - - blocks: Final = tuple( - cast( # cast-ok: shape validated by _is_replayable_thinking_block + return tuple( + cast( # cast-ok: decode_thinking_blocks keeps verifiable thinking blocks only ChatCompletionThinkingBlock | ChatCompletionRedactedThinkingBlock, block, ) for block in decoded - if isinstance(block, Mapping) and LiteLLMCompletionResponsesConfig._is_replayable_thinking_block(block) ) - return blocks or None - - @staticmethod - def _is_replayable_thinking_block(block: Mapping[str, object]) -> bool: - """ - A thinking block is only worth replaying when the provider can verify - it: a ``thinking`` block needs its signature, a ``redacted_thinking`` - block needs its opaque data. - """ - block_type: Final[object] = block.get("type") - if block_type == "thinking": - return bool(block.get("signature")) - if block_type == "redacted_thinking": - return bool(block.get("data")) - return False @staticmethod def _is_input_item_tool_call_output(input_item: Mapping[str, object]) -> bool: @@ -2559,8 +2537,7 @@ class LiteLLMCompletionResponsesConfig: @staticmethod def _encode_thinking_blocks(message: Message) -> str | None: thinking_blocks: Final[Sequence[Mapping[str, object]]] = getattr(message, "thinking_blocks", None) or () - preserved: Final = tuple(block for block in thinking_blocks if block.get("signature") or block.get("data")) - return json.dumps(preserved, separators=(",", ":")) if preserved else None + return encode_thinking_blocks(thinking_blocks) @staticmethod def _extract_reasoning_output_items( @@ -2577,7 +2554,7 @@ class LiteLLMCompletionResponsesConfig: return [ GenericResponseOutputItem( type="reasoning", - id=f"rs_{uuid.uuid4()}", + id=mint_reasoning_item_id(), status=LiteLLMCompletionResponsesConfig._map_chat_completion_finish_reason_to_responses_status( choice.finish_reason ), diff --git a/litellm/router_strategy/complexity_router/complexity_router.py b/litellm/router_strategy/complexity_router/complexity_router.py index 3f56141b21c..3610991a20d 100644 --- a/litellm/router_strategy/complexity_router/complexity_router.py +++ b/litellm/router_strategy/complexity_router/complexity_router.py @@ -108,7 +108,7 @@ from .config import ( ComplexityRouterConfig, ComplexityTier, CustomDimension, - JevClassifierConfig, + OpenSourceClassifierConfig, TierDefinition, ) from .jev_classifier import ( @@ -1308,10 +1308,22 @@ class ComplexityRouter(CustomLogger): """ @staticmethod - def _build_jev_client(config: JevClassifierConfig) -> JevClassifierClient: + def _build_jev_client(config: OpenSourceClassifierConfig) -> JevClassifierClient: + if config.provider in ("laya", "bespoke"): + from litellm.llms.oss_decision import oss_connection + + connection: Final = oss_connection(config.provider, config.api_base, config.api_key) + return HttpJevClassifierClient( + api_key=connection.api_key, + api_base=connection.api_base, + http_client=get_async_httpx_client(httpxSpecialProvider.PassThroughEndpoint), + provider=config.provider, + ) api_key: Final = config.api_key or get_secret_str("TYPESAFE_API_KEY") if not api_key: - raise ValueError("jev_classifier_config.api_key or TYPESAFE_API_KEY is required for classifier_type 'jev'") + raise ValueError( + "opensource_classifier_config.api_key or TYPESAFE_API_KEY is required for classifier_type 'oss_classifier'" + ) api_base: Final = config.api_base or get_secret_str("TYPESAFE_API_BASE") or "https://api.typesafe.ai" return HttpJevClassifierClient( api_key=api_key, @@ -1354,12 +1366,12 @@ class ComplexityRouter(CustomLogger): if default_model: self.config.default_model = default_model - jev_config: Final = self.config.jev_classifier_config + jev_config: Final = self.config.opensource_classifier_config self._jev_client: JevClassifierClient | None = ( jev_client if jev_client is not None else self._build_jev_client(jev_config) - if self.config.classifier_type == "jev" and jev_config is not None + if self.config.classifier_type == "oss_classifier" and jev_config is not None else None ) @@ -1459,7 +1471,11 @@ class ComplexityRouter(CustomLogger): and self.config.classifier_llm_config.circuit_breaker_enabled ) else jev_config.circuit_breaker_cooldown_seconds - if (self.config.classifier_type == "jev" and jev_config is not None and jev_config.circuit_breaker_enabled) + if ( + self.config.classifier_type == "oss_classifier" + and jev_config is not None + and jev_config.circuit_breaker_enabled + ) else None ) self._classifier_circuit_breaker: _ClassifierCircuitBreaker | None = ( @@ -1909,7 +1925,7 @@ class ComplexityRouter(CustomLogger): return self._classify_with_heuristic_v2(prompt) if self.config.classifier_type == "custom": return await self._classify_with_plugin(prompt, system_prompt, request_kwargs, raw_messages) - if self.config.classifier_type == "jev": + if self.config.classifier_type == "oss_classifier": return await self._jev_classifier_outcome(prompt, system_prompt, request_kwargs, messages) if self.config.classifier_type in ("heuristic_first", "hybrid") and _encrypted_classifier_task( request_kwargs, self._reminder_markers_for_request(request_kwargs or EMPTY_MAPPING) @@ -2161,7 +2177,7 @@ class ComplexityRouter(CustomLogger): request_kwargs: Mapping[str, object] | None, messages: Sequence[Mapping[str, object]] | None, ) -> ClassificationOutcome: - config: Final = self.config.jev_classifier_config + config: Final = self.config.opensource_classifier_config client: Final = self._jev_client if config is None or client is None: return self._classifier_failure_outcome("jev classifier is not configured", prompt, system_prompt) @@ -2212,12 +2228,14 @@ class ComplexityRouter(CustomLogger): if not self._tier_pools().get(tier_name): raise ValueError(f"Jev classifier returned tier {tier_name!r}, which has no models configured") model: Final = response.model or config.model + accounting_provider: Final = "typesafe" if config.provider == "jev" else config.provider verdict: Final = JevVerdict( label=answer.choice, probabilities=answer.probabilities, confidence=answer.confidence, model=model, - cost=jev_classifier_cost(response, config.model), + cost=jev_classifier_cost(response, config.model, accounting_provider), + provider=accounting_provider, ) if breaker is not None and permit is not None: breaker.record_success(permit) @@ -2225,8 +2243,8 @@ class ComplexityRouter(CustomLogger): tier=tier, score=None, signals=( - f"jev-classifier:{tier_name}", - f"jev-confidence={answer.confidence:.6f}", + f"{config.provider}-classifier:{tier_name}", + f"{config.provider}-confidence={answer.confidence:.6f}", *( f"tier-probability:{label}={probability:.6f}" for label, probability in answer.probabilities.items() @@ -4757,7 +4775,7 @@ class ComplexityRouter(CustomLogger): tier_litellm_params: Final = self._litellm_params_for_model(tier, routed_model) classifier_model: Final = ( - f"typesafe/{outcome.jev_verdict.model}" + f"{outcome.jev_verdict.provider}/{outcome.jev_verdict.model}" if outcome.cause == "jev_classifier" and outcome.jev_verdict is not None else self.config.classifier_llm_config.model if outcome.cause in ("llm_classifier", "capability_classifier", "llm_v2_classifier", "llm_v2_fallback") diff --git a/litellm/router_strategy/complexity_router/config.py b/litellm/router_strategy/complexity_router/config.py index 00cff661d2f..88907731468 100644 --- a/litellm/router_strategy/complexity_router/config.py +++ b/litellm/router_strategy/complexity_router/config.py @@ -9,6 +9,7 @@ import math import re import warnings from collections.abc import Iterable, Mapping +from dataclasses import dataclass from enum import Enum from types import MappingProxyType from typing import Annotated, Final, Literal, NamedTuple @@ -19,6 +20,7 @@ from pydantic import ( Field, SkipValidation, StrictFloat, + TypeAdapter, field_serializer, field_validator, model_validator, @@ -674,47 +676,151 @@ class CapabilityClassifierConfig(BaseModel): return self -class JevClassifierConfig(BaseModel): +def normalize_classifier_config_aliases(config: Mapping[str, object]) -> Mapping[str, object]: + if "jev_classifier_config" in config and "opensource_classifier_config" in config: + return config + normalized: Final = dict(config) + if "jev_classifier_config" in normalized: + normalized["opensource_classifier_config"] = normalized.pop("jev_classifier_config") + if normalized.get("classifier_type") == "jev": + normalized["classifier_type"] = "oss_classifier" + classifier: Final = normalized.get("opensource_classifier_config") + if isinstance(classifier, Mapping): + classifier_fields: Final = TypeAdapter(Mapping[str, object]).validate_python(classifier) + if classifier_fields.get("provider") == "typesafe": + normalized["opensource_classifier_config"] = { + **classifier_fields, + "provider": "jev", + } + return normalized + + +class OpenSourceClassifierConfig(BaseModel): model_config = ConfigDict(extra="forbid", frozen=True) + provider: Literal["jev", "laya", "bespoke"] = "jev" model: str = "jev-latest" - api_key: str | None = Field(default=None, description="TypeSafe API key, falling back to TYPESAFE_API_KEY") + api_key: str | None = Field(default=None, description="Provider API key; optional for self-hosted providers") api_base: str | None = Field( default=None, - description="TypeSafe API base, falling back to TYPESAFE_API_BASE and then https://api.typesafe.ai", + description="Provider API base; defaults to the selected provider API_BASE environment variable", ) timeout_ms: int = Field(default=3000, ge=1) instructions: str | None = Field( default=None, - description="Replaces the built-in Jev question instructions", + description="Replaces the built-in classification instructions", ) circuit_breaker_enabled: bool = True circuit_breaker_cooldown_seconds: float = Field(default=30.0, gt=0.0) + @field_validator("provider", mode="before") + @classmethod + def _normalize_provider_alias(cls, value: object) -> object: + return "jev" if value == "typesafe" else value + @field_validator("instructions") @classmethod def _reject_blank_instructions(cls, value: str | None) -> str | None: if value is not None and not value.strip(): - raise ValueError("jev_classifier_config.instructions must be non-empty; omit it to use the default") + raise ValueError("opensource_classifier_config.instructions must be non-empty; omit it to use the default") return value @field_validator("api_key") @classmethod def _reject_blank_api_key(cls, value: str | None) -> str | None: if value is not None and not value.strip(): - raise ValueError("jev_classifier_config.api_key must be non-empty; omit it to use TYPESAFE_API_KEY") + raise ValueError( + "opensource_classifier_config.api_key must be non-empty; omit it to use the provider environment key" + ) return value @model_validator(mode="after") - def _keep_the_environment_key_on_the_environment_base(self) -> "JevClassifierConfig": + def _keep_the_environment_key_on_the_environment_base(self) -> "OpenSourceClassifierConfig": + if self.provider in ("laya", "bespoke"): + from litellm.llms.oss_decision import validate_oss_api_base, validate_oss_model + + _ = validate_oss_model(self.provider, self.model) + if self.api_base is not None: + _ = validate_oss_api_base(self.provider, self.api_base) + return self if self.api_base is not None and self.api_key is None: raise ValueError( - "jev_classifier_config.api_base requires jev_classifier_config.api_key: TYPESAFE_API_KEY is only sent " + "opensource_classifier_config.api_base requires opensource_classifier_config.api_key: TYPESAFE_API_KEY is only sent " "to TYPESAFE_API_BASE or https://api.typesafe.ai" ) return self +JevClassifierConfig = OpenSourceClassifierConfig + + +@dataclass(frozen=True, slots=True) +class ComplexityRouterConfigWrite: + submitted: Mapping[str, object] | None + effective: Mapping[str, object] | None + + @property + def supplied_connection_fields(self) -> frozenset[str]: + classifier: Final = self.submitted.get("opensource_classifier_config") if self.submitted is not None else None + return frozenset( + field for field in ("api_base", "api_key") if isinstance(classifier, Mapping) and field in classifier + ) + + +def resolve_complexity_router_config_write( + incoming: Mapping[str, object] | None, stored: Mapping[str, object] | None +) -> ComplexityRouterConfigWrite: + if incoming is None: + return ComplexityRouterConfigWrite(submitted=None, effective=stored) + return _resolve_normalized_complexity_router_config_write( + normalize_classifier_config_aliases(incoming), + normalize_classifier_config_aliases(stored) if stored is not None else None, + ) + + +def _resolve_normalized_complexity_router_config_write( + incoming: Mapping[str, object], stored: Mapping[str, object] | None +) -> ComplexityRouterConfigWrite: + if ( + stored is None + or incoming.get("classifier_type") != "oss_classifier" + or stored.get("classifier_type") != "oss_classifier" + ): + return ComplexityRouterConfigWrite(submitted=incoming, effective=incoming) + incoming_classifier: Final = incoming.get("opensource_classifier_config") + stored_classifier: Final = stored.get("opensource_classifier_config") + if not isinstance(incoming_classifier, Mapping) or not isinstance(stored_classifier, Mapping): + return ComplexityRouterConfigWrite(submitted=incoming, effective=incoming) + existing: Final = TypeAdapter(dict[str, object]).validate_python(stored_classifier) + supplied: Final = TypeAdapter(dict[str, object]).validate_python(incoming_classifier) + classifier: Final = ( + MappingProxyType({**supplied, "provider": existing["provider"]}) + if "provider" not in supplied and "provider" in existing + else supplied + ) + same_provider: Final = classifier.get("provider", "jev") == existing.get("provider", "jev") + same_base: Final = "api_base" not in classifier or ( + classifier["api_base"] is not None and classifier["api_base"] == existing.get("api_base") + ) + transport: Final = MappingProxyType( + { + key: value + for key, value in existing.items() + if same_provider and key in ("api_key", "api_base") and (key != "api_key" or same_base) + } + ) + return ComplexityRouterConfigWrite( + submitted=MappingProxyType({**incoming, "opensource_classifier_config": classifier}), + effective={ + **incoming, + "opensource_classifier_config": { + **transport, + **classifier, + }, + }, + ) + + MAX_CUSTOM_PATTERN_REPEAT: Final[int] = 64 MAX_CUSTOM_PATTERN_WORK: Final[int] = 2048 MAX_CUSTOM_DIMENSIONS_WORK: Final[int] = 8192 @@ -846,6 +952,20 @@ class ContextCompactionConfig(BaseModel): class ComplexityRouterConfig(BaseModel): """Configuration for the ComplexityRouter.""" + @model_validator(mode="before") + @classmethod + def _normalize_classifier_aliases(cls, value: object) -> object: + if not isinstance(value, Mapping): + return value + config: Final = TypeAdapter(dict[str, object]).validate_python(value) + if "jev_classifier_config" in config and "opensource_classifier_config" in config: + raise ValueError("Use only opensource_classifier_config; do not also supply jev_classifier_config") + return normalize_classifier_config_aliases(config) + + @property + def jev_classifier_config(self) -> OpenSourceClassifierConfig | None: + return self.opensource_classifier_config + # string = pin; list = random pick when adaptive=False, soft-floor home pool when adaptive=True tiers: dict[str, str | list[str]] = Field( default_factory=lambda: DEFAULT_TIER_MODELS.copy(), @@ -880,7 +1000,7 @@ class ComplexityRouterConfig(BaseModel): "becomes that tier's rubric bullet; entries named after a built-in tier may omit the " "description and inherit the built-in criteria. List order is ascending severity and " "decides which tier wins when several keyword_tier_rules match. Requires classifier_type " - "'llm', 'jev' or 'custom', a fallback_tier, and `tiers` keys matching the defined names exactly. Escalation, " + "'llm', 'oss_classifier' or 'custom', a fallback_tier, and `tiers` keys matching the defined names exactly. Escalation, " "adaptive selection, session affinity, plugins, tier_labels, and the calibration-example " "rubric presets are unavailable with a custom tier set: the first four are built on the " "built-in tier ladder, and the last two rename or exemplify tiers the set replaces." @@ -1024,7 +1144,7 @@ class ComplexityRouterConfig(BaseModel): "custom", "heuristic_first", "hybrid", - "jev", + "oss_classifier", ] = Field( default="heuristic", description=( @@ -1032,7 +1152,7 @@ class ComplexityRouterConfig(BaseModel): "an LLM tier-selection call, a Switchyard-compatible capability forecast, a joint Fuse V2 forecast, " "a custom classifier plugin, 'heuristic_first', which scores locally and only pays for the LLM classifier when the " "local scorer does not confidently land a cheap tier, or 'hybrid', which trusts the local scorer " - "everywhere except when its score lands near a tier boundary, or 'jev', a TypeSafe AI Jev structured choice call" + "everywhere except when its score lands near a tier boundary, or 'oss_classifier', a structured choice call using Jev, Laya or Bespoke Nimble" ), ) llm_v2_config: LLMV2Config | None = Field( @@ -1073,7 +1193,7 @@ class ComplexityRouterConfig(BaseModel): "and otherwise routes to capable_tier" ), ) - jev_classifier_config: JevClassifierConfig | None = None + opensource_classifier_config: OpenSourceClassifierConfig | None = None heuristic_first_max_tier: str | None = Field( default=None, description=( @@ -1639,14 +1759,16 @@ class ComplexityRouterConfig(BaseModel): return self @model_validator(mode="after") - def _validate_jev_classifier_config(self) -> "ComplexityRouterConfig": - jev: Final = self.jev_classifier_config - if self.classifier_type != "jev": + def _validate_opensource_classifier_config(self) -> "ComplexityRouterConfig": + jev: Final = self.opensource_classifier_config + if self.classifier_type != "oss_classifier": if jev is not None: - raise ValueError("jev_classifier_config requires classifier_type 'jev'; otherwise it has no effect") + raise ValueError( + "opensource_classifier_config requires classifier_type 'oss_classifier'; otherwise it has no effect" + ) return self if jev is None: - raise ValueError("jev_classifier_config is required when classifier_type is 'jev'") + raise ValueError("opensource_classifier_config is required when classifier_type is 'oss_classifier'") return self @model_validator(mode="after") @@ -1962,9 +2084,9 @@ class ComplexityRouterConfig(BaseModel): "enable_non_reasoning_tier cannot be combined with tier_definitions: a custom tier set " f"replaces the built-in ladder, so name a tier {non_reasoning_key} in tier_definitions instead" ) - if self.classifier_type not in ("llm", "custom", "jev"): + if self.classifier_type not in ("llm", "custom", "oss_classifier"): raise ValueError( - f"enable_non_reasoning_tier requires classifier_type 'llm', 'jev' or 'custom', got " + f"enable_non_reasoning_tier requires classifier_type 'llm', 'oss_classifier' or 'custom', got " f"{self.classifier_type!r}: the heuristic scorers only produce the four tiers from SIMPLE up, " f"so nothing would ever classify as {non_reasoning_key}" ) @@ -1997,7 +2119,7 @@ class ComplexityRouterConfig(BaseModel): raise ValueError(f"tier_definitions names must be unique (case-insensitive): {', '.join(duplicated)}") if self.classifier_type in ("heuristic", "heuristic_v2", "capability", "heuristic_first", "hybrid"): raise ValueError( - "tier_definitions requires classifier_type 'llm', 'jev' or 'custom': the heuristic scorer only " + "tier_definitions requires classifier_type 'llm', 'oss_classifier' or 'custom': the heuristic scorer only " "produces the built-in tiers from SIMPLE up, as does heuristic_v2" ) conflicts: Final = self._tier_definition_conflicts() @@ -2164,7 +2286,9 @@ class ComplexityRouterConfig(BaseModel): ) -COMPLEXITY_ROUTER_CONFIG_KEYS: Final[frozenset[str]] = frozenset(ComplexityRouterConfig.model_fields) +COMPLEXITY_ROUTER_CONFIG_KEYS: Final[frozenset[str]] = frozenset(ComplexityRouterConfig.model_fields) | frozenset( + ("jev_classifier_config",) +) """Every setting name this config owns, derived from the model so a field added later is covered. These names are disjoint from the OpenAI request params, from ``all_litellm_params``, and from the diff --git a/litellm/router_strategy/complexity_router/jev_classifier.py b/litellm/router_strategy/complexity_router/jev_classifier.py index a2f03b07e3a..2b97ae824dd 100644 --- a/litellm/router_strategy/complexity_router/jev_classifier.py +++ b/litellm/router_strategy/complexity_router/jev_classifier.py @@ -18,6 +18,7 @@ from litellm.litellm_core_utils.internal_call_metadata import ( from litellm.litellm_core_utils.litellm_logging import Logging from litellm.litellm_core_utils.logging_worker import GLOBAL_LOGGING_WORKER from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler +from litellm.llms.laya.common_utils import laya_response_model from litellm.proxy.pass_through_endpoints.llm_provider_handlers.typesafe_passthrough_logging_handler import ( TypeSafePassthroughLoggingHandler, ) @@ -78,10 +79,17 @@ class JevClassifierClient(Protocol): class HttpJevClassifierClient: - def __init__(self, api_key: str, api_base: str, http_client: AsyncHTTPHandler) -> None: + def __init__( + self, + api_key: str | None, + api_base: str, + http_client: AsyncHTTPHandler, + provider: Literal["typesafe", "laya", "bespoke"] = "typesafe", + ) -> None: self._api_key = api_key self._api_base = api_base.rstrip("/") self._http_client = http_client + self._provider = provider async def evaluate( self, @@ -90,26 +98,30 @@ class HttpJevClassifierClient: request_kwargs: Mapping[str, object] | None = None, ) -> JevSystemOneResponse: start_time: Final = datetime.now(timezone.utc) + authorization: Final[Mapping[str, str]] = ( + MappingProxyType({"Authorization": f"Bearer {self._api_key}"}) if self._api_key else MappingProxyType({}) + ) response: Final = await self._http_client.post( # pyright: ignore[reportUnknownMemberType] # AsyncHTTPHandler has a dynamic post signature f"{self._api_base}/v1/systemone", json=request.model_dump(mode="json"), - headers=MappingProxyType( - { - "Authorization": f"Bearer {self._api_key}", - "Content-Type": "application/json", - } - ), # pyright: ignore[reportArgumentType] # HTTP headers are not mutated by AsyncHTTPHandler + headers=MappingProxyType({**authorization, "Content-Type": "application/json"}), # pyright: ignore[reportArgumentType] # HTTP headers are not mutated by AsyncHTTPHandler timeout=timeout_s, ) response.raise_for_status() + body: Final = TypeAdapter(dict[str, object]).validate_json(response.content) + normalized_body: Final = ( + MappingProxyType({**body, "model": laya_response_model(body, request.model)}) + if self._provider == "laya" + else body + ) try: self._log_response(request, response, request_kwargs, start_time) except Exception as exc: # noqa: BLE001 # logging integrations must not discard a provider verdict verbose_router_logger.warning("JEV response logging failed (%s)", type(exc).__name__) - return TypeAdapter(JevSystemOneResponse).validate_python(response.json()) + return TypeAdapter(JevSystemOneResponse).validate_python(normalized_body) - @staticmethod def _log_response( + self, request: JevSystemOneRequest, response: httpx.Response, request_kwargs: Mapping[str, object] | None, @@ -139,7 +151,7 @@ class HttpJevClassifierClient: "turn_off_message_logging": effective_turn_off_message_logging(request_kwargs), } logging_obj: Final = Logging( - model=f"typesafe/{request.model}", + model=f"{self._provider}/{request.model}", messages=[{"role": "user", "content": request.state}], stream=False, call_type="pass_through_endpoint", @@ -150,7 +162,7 @@ class HttpJevClassifierClient: kwargs=params, ) logging_obj.update_environment_variables( - model=f"typesafe/{request.model}", + model=f"{self._provider}/{request.model}", user=parent_user if isinstance(parent_user := parent.get("user"), str) else None, optional_params={}, litellm_params=params, @@ -165,7 +177,7 @@ class HttpJevClassifierClient: end_time=end_time, cache_hit=False, request_body=MappingProxyType({"model": request.model}), - custom_llm_provider="typesafe", + custom_llm_provider=self._provider, litellm_params=params, ) success_handlers: Final = logging_obj.dispatch_success_handlers( @@ -189,6 +201,7 @@ class JevVerdict(NamedTuple): confidence: float model: str cost: float | None + provider: Literal["typesafe", "laya", "bespoke"] = "typesafe" class _RegistryPricing(BaseModel): @@ -211,12 +224,14 @@ def build_jev_request( return JevSystemOneRequest(state=state, model=model, questions=MappingProxyType({"tier": question})) -def jev_classifier_cost(response: JevSystemOneResponse, configured_model: str) -> float | None: +def jev_classifier_cost( + response: JevSystemOneResponse, configured_model: str, provider: Literal["typesafe", "laya", "bespoke"] = "typesafe" +) -> float | None: usage: Final = response.usage if usage is None: return None model: Final = response.model or configured_model - model_key: Final = f"typesafe/{model}" + model_key: Final = f"{provider}/{model}" if model_key not in litellm.model_cost: # pyright: ignore[reportUnknownMemberType] # registry is dynamically typed return None try: diff --git a/litellm/router_utils/auto_router_model_naming.py b/litellm/router_utils/auto_router_model_naming.py index 6b589c3bfc0..3423836e4fd 100644 --- a/litellm/router_utils/auto_router_model_naming.py +++ b/litellm/router_utils/auto_router_model_naming.py @@ -19,6 +19,7 @@ from litellm.router_strategy.complexity_router.config import ( COMPLEXITY_ROUTER_CONFIG_KEYS, DEFAULT_JEV_INSTRUCTIONS, LLM_CLASSIFIER_TYPES, + normalize_classifier_config_aliases, ) AUTO_ROUTER_MODEL_PREFIX: Final = "auto_router/" @@ -151,8 +152,11 @@ def strategy_router_dependencies( ) ) ) - complexity: Final = _mapping(litellm_params.get("complexity_router_config")) + complexity: Final = normalize_classifier_config_aliases(_mapping(litellm_params.get("complexity_router_config"))) classifier: Final = _mapping(complexity.get("classifier_llm_config")) + decision_classifier: Final = _mapping(complexity.get("opensource_classifier_config")) + decision_provider: Final = decision_classifier.get("provider", "jev") + accounting_provider: Final = "typesafe" if decision_provider == "jev" else decision_provider return tuple( dict.fromkeys( tuple(dep for tier in _mapping(complexity.get("tiers")).values() for dep in _pool(tier, "tier")) @@ -165,10 +169,10 @@ def strategy_router_dependencies( ) + ( _named( - f"typesafe/{_mapping(complexity.get('jev_classifier_config')).get('model', 'jev-latest')}", + f"{accounting_provider}/{decision_classifier.get('model', 'jev-latest')}", "evaluation", ) - if complexity.get("classifier_type") == "jev" + if complexity.get("classifier_type") == "oss_classifier" else () ) + ( @@ -206,9 +210,9 @@ def defines_custom_classifier_prompt(complexity_router_config: object) -> bool: Scoped to the classifier types that actually call an LLM, which is also where the config validator accepts these fields: the heuristic scorers never read them. """ - config: Final = _mapping(complexity_router_config) - if config.get("classifier_type") == "jev": - instructions: Final = _mapping(config.get("jev_classifier_config")).get("instructions") + config: Final = normalize_classifier_config_aliases(_mapping(complexity_router_config)) + if config.get("classifier_type") == "oss_classifier": + instructions: Final = _mapping(config.get("opensource_classifier_config")).get("instructions") return isinstance(instructions, str) and instructions != DEFAULT_JEV_INSTRUCTIONS if config.get("classifier_type") not in LLM_CLASSIFIER_TYPES: return False @@ -272,6 +276,9 @@ _OPERATOR_PROMPT_FIELDS_SQL: Final = " OR ".join( f"{{config}} ->> '{field}' IS NOT NULL" for field in OPERATOR_CLASSIFIER_PROMPT_FIELDS ) _DEFAULT_JEV_INSTRUCTIONS_SQL: Final = DEFAULT_JEV_INSTRUCTIONS.replace("'", "''") +_OPENSOURCE_CLASSIFIER_CONFIG_SQL: Final = ( + "COALESCE({config} -> 'opensource_classifier_config', {config} -> 'jev_classifier_config')" +) CUSTOMIZATION_CAPABILITY: Final = GatedAutoRouterCapability( key="tier_or_classifier_prompt", @@ -286,9 +293,9 @@ CUSTOMIZATION_CAPABILITY: Final = GatedAutoRouterCapability( f"({{config}} ->> 'classifier_type' IN ({_LLM_CLASSIFIER_TYPES_SQL}) AND (" "{config} -> 'classifier_llm_config' ->> 'system_prompt' IS NOT NULL OR " f"{_OPERATOR_PROMPT_FIELDS_SQL})) OR " - "({config} ->> 'classifier_type' = 'jev' AND " - "jsonb_typeof({config} -> 'jev_classifier_config' -> 'instructions') = 'string' AND " - f"{{config}} -> 'jev_classifier_config' ->> 'instructions' <> '{_DEFAULT_JEV_INSTRUCTIONS_SQL}')" + "({config} ->> 'classifier_type' IN ('oss_classifier', 'jev') AND " + f"jsonb_typeof({_OPENSOURCE_CLASSIFIER_CONFIG_SQL} -> 'instructions') = 'string' AND " + f"{_OPENSOURCE_CLASSIFIER_CONFIG_SQL} ->> 'instructions' <> '{_DEFAULT_JEV_INSTRUCTIONS_SQL}')" ), ) diff --git a/litellm/rust_bridge/_native.pyi b/litellm/rust_bridge/_native.pyi index 0f70c559840..45bccad6179 100644 --- a/litellm/rust_bridge/_native.pyi +++ b/litellm/rust_bridge/_native.pyi @@ -11,6 +11,7 @@ from litellm.rust_bridge.embeddings.entrypoints import LiteLLMEmbeddingRequest from litellm.rust_bridge.messages.entrypoints import LiteLLMMessagesRequest from litellm.rust_bridge.ocr.entrypoints import LiteLLMOcrRequest from litellm.rust_bridge.responses.entrypoints import LiteLLMResponsesRequest +from litellm.rust_bridge.trace_queries import ReadQueryName from litellm.rust_bridge.traces import DecodedSpan, QueryScope from litellm.types.llms.anthropic_messages.anthropic_response import AnthropicMessagesResponse from litellm.types.llms.openai import ResponsesAPIResponse @@ -40,9 +41,8 @@ class NativeTraceStorage: def ensure_schema(self) -> Future[None]: ... def insert_rows(self, table: str, rows: Sequence[Mapping[str, object]]) -> Future[None]: ... def query_sql(self, sql: str, scope: QueryScope, secret: str) -> Future[str]: ... - def query_help(self, scope: QueryScope, secret: str) -> Future[str]: ... - def lens_query(self, name: str, parameters: Mapping[str, str | int | Sequence[str]]) -> Future[str]: ... - def query(self, query: str, parameters: Mapping[str, str | int | Sequence[str]]) -> Future[str]: ... + def query_help(self, scope: QueryScope, secret: str) -> Future[JsonValue]: ... + def query(self, query: ReadQueryName, parameters: Mapping[str, str | int | float | Sequence[str]]) -> Future[str]: ... @final class NativeDiagnosticProcessor: diff --git a/litellm/rust_bridge/trace_queries.py b/litellm/rust_bridge/trace_queries.py new file mode 100644 index 00000000000..1add1787241 --- /dev/null +++ b/litellm/rust_bridge/trace_queries.py @@ -0,0 +1,314 @@ +from collections.abc import Mapping +from dataclasses import dataclass +from typing import Annotated, Final, Generic, Literal, TypeAlias, TypeVar + +from pydantic import BaseModel, ConfigDict, Field, TypeAdapter +from typing_extensions import NotRequired, ReadOnly, TypedDict + +ReadQueryName: TypeAlias = Literal[ + "list_traces", + "trace_spans", + "trace_identity", + "span_detail", + "span_error", + "spend_by_response_ids", + "availability", + "agents", + "sample", + "content", + "evidence", +] + +Int64: TypeAlias = Annotated[int, Field(ge=-(2**63), le=2**63 - 1)] +UInt64: TypeAlias = Annotated[int, Field(ge=0, le=2**64 - 1)] +UInt32: TypeAlias = Annotated[int, Field(ge=0, le=2**32 - 1)] +_PARAMETERS_CONFIG: Final = ConfigDict(frozen=True, extra="forbid") + + +class ListTracesParams(BaseModel): + model_config = _PARAMETERS_CONFIG + all_teams: Literal[0, 1] + user_id: str + team_ids: tuple[str, ...] + start_ms: Int64 + end_ms: Int64 + cursor_ms: Int64 + cursor_trace_id: str + limit: UInt32 + + +class TraceSpansParams(BaseModel): + model_config = _PARAMETERS_CONFIG + all_teams: Literal[0, 1] + user_id: str + team_ids: tuple[str, ...] + trace_id: str + trace_ref: str + + +class SpanDetailParams(BaseModel): + model_config = _PARAMETERS_CONFIG + all_teams: Literal[0, 1] + user_id: str + team_ids: tuple[str, ...] + trace_id: str + trace_ref: str + span_id: str + + +class SpanErrorParams(BaseModel): + model_config = _PARAMETERS_CONFIG + all_teams: Literal[0, 1] + user_id: str + team_ids: tuple[str, ...] + trace_id: str + trace_ref: str + span_id: str + error_offset: UInt64 + error_version: str + + +class SpendByResponseIdsParams(BaseModel): + model_config = _PARAMETERS_CONFIG + all_teams: Literal[0, 1] + user_id: str + team_ids: tuple[str, ...] + response_ids: tuple[str, ...] + start_ms: Int64 + end_ms: Int64 + + +class LensAccessParams(BaseModel): + model_config = _PARAMETERS_CONFIG + all_teams: Literal[0, 1] + team: str + key_hash: str + + +class LensSampleParams(BaseModel): + model_config = _PARAMETERS_CONFIG + all_teams: Literal[0, 1] + team: str + key_hash: str + source: Literal["traces", "requests", "both"] + start: UInt64 + end: UInt64 + agent_name: str + service: str + filter_keys: tuple[str, ...] + filter_values: tuple[str, ...] + selected_team: str + execution_ids: tuple[str, ...] + sample_cap: UInt64 + sample_percent: Annotated[float, Field(ge=0, le=100, allow_inf_nan=False)] + preview: Literal[0, 1] + after: str + limit: UInt32 + offset: UInt64 + + +class LensContentParams(BaseModel): + model_config = _PARAMETERS_CONFIG + all_teams: Literal[0, 1] + team: str + key_hash: str + source: Literal["traces", "requests"] + id: str + record_team: str + trace_ref: str + cursor: str + offset: UInt32 + + +class LensEvidenceParams(BaseModel): + model_config = _PARAMETERS_CONFIG + all_teams: Literal[0, 1] + team: str + key_hash: str + source: Literal["traces", "requests"] + id: str + record_team: str + trace_ref: str + span: str + quote: str + + +class TraceIdentityParams(BaseModel): + model_config = _PARAMETERS_CONFIG + all_teams: Literal[0, 1] + user_id: str + team_ids: tuple[str, ...] + trace_id: str + + +class TraceIdentityRow(BaseModel): + model_config = ConfigDict(frozen=True) + trace_ref: str + + +class ListTracesRow(TypedDict): + trace_id: ReadOnly[str] + trace_ref: ReadOnly[str] + team_id: ReadOnly[str] + api_key_hash: ReadOnly[str] + user_id: ReadOnly[str] + name: ReadOnly[str] + service: ReadOnly[str] + input_preview: ReadOnly[str] + status: ReadOnly[str] + start_ms: ReadOnly[int] + duration_ms: ReadOnly[int] + span_count: ReadOnly[int] + agent_count: ReadOnly[int] + agent_invocations: ReadOnly[int] + agent_names: NotRequired[ReadOnly[tuple[str, ...]]] + frameworks: NotRequired[ReadOnly[tuple[str, ...]]] + llm_calls: ReadOnly[int] + tool_calls: ReadOnly[int] + input_tokens: ReadOnly[int] + output_tokens: ReadOnly[int] + models: ReadOnly[tuple[str, ...]] + error_count: ReadOnly[int] + request_ids: ReadOnly[tuple[str, ...]] + + +class TraceSpansRow(TypedDict): + span_id: ReadOnly[str] + parent_span_id: ReadOnly[str] + name: ReadOnly[str] + type: ReadOnly[Literal["agent", "llm", "tool", "chain", "framework"]] + agent: ReadOnly[str] + framework: NotRequired[ReadOnly[str]] + status: ReadOnly[str] + status_message: ReadOnly[str] + error_truncated: ReadOnly[bool] + start_ns: ReadOnly[int] + duration_ns: ReadOnly[int] + service: ReadOnly[str] + input_preview: ReadOnly[str] + model: ReadOnly[str] + input_tokens: ReadOnly[int] + output_tokens: ReadOnly[int] + litellm_request_id: ReadOnly[str] + team_id: ReadOnly[str] + api_key_hash: ReadOnly[str] + user_id: ReadOnly[str] + + +class SpanDetailRow(TypedDict): + span_id: ReadOnly[str] + input: ReadOnly[str] + output: ReadOnly[str] + attributes: ReadOnly[Mapping[str, str]] + + +class SpanErrorRow(BaseModel): + model_config = ConfigDict(frozen=True) + span_id: str + message: str + total_chars: int + version: str + + +class SpendRow(BaseModel): + model_config = ConfigDict(frozen=True) + request_id: str + response_id: str + team_id: str + api_key: str + user: str + spend: float + start_ms: int + + +class ActivityAvailability(BaseModel): + model_config = ConfigDict(frozen=True) + traces: bool = False + requests: bool = False + + +class ExecutionRow(BaseModel): + model_config = ConfigDict(frozen=True) + selection_key: str = "" + source: Literal["traces", "requests"] + trace_id: str + trace_ref: str = "" + team_id: str + name: str + start_time: str + span_count: int + root_seen: int + eligible: int + selected: int = 0 + service: str = "" + attributes: tuple[tuple[str, str], ...] = () + + +class PartRow(BaseModel): + model_config = ConfigDict(frozen=True) + span_id: str + parent_span_id: str + name: str + kind: str + content: str + truncated: int + + +class CountRow(BaseModel): + model_config = ConfigDict(frozen=True) + count: int + + +class AgentRow(BaseModel): + model_config = ConfigDict(frozen=True) + agent_name: str + + +ParamsT: Final = TypeVar("ParamsT", bound=BaseModel) +RowT: Final = TypeVar("RowT") + + +class QueryResponse(BaseModel, Generic[RowT]): + model_config = ConfigDict(frozen=True) + data: tuple[RowT, ...] + + +@dataclass(frozen=True, slots=True) +class ReadQuery(Generic[ParamsT, RowT]): + name: ReadQueryName + parameters: type[ParamsT] + response: TypeAdapter[QueryResponse[RowT]] + + +LIST_TRACES: Final[ReadQuery[ListTracesParams, ListTracesRow]] = ReadQuery( + "list_traces", ListTracesParams, TypeAdapter(QueryResponse[ListTracesRow]) +) +TRACE_SPANS: Final[ReadQuery[TraceSpansParams, TraceSpansRow]] = ReadQuery( + "trace_spans", TraceSpansParams, TypeAdapter(QueryResponse[TraceSpansRow]) +) +SPAN_DETAIL: Final[ReadQuery[SpanDetailParams, SpanDetailRow]] = ReadQuery( + "span_detail", SpanDetailParams, TypeAdapter(QueryResponse[SpanDetailRow]) +) +SPAN_ERROR: Final[ReadQuery[SpanErrorParams, SpanErrorRow]] = ReadQuery( + "span_error", SpanErrorParams, TypeAdapter(QueryResponse[SpanErrorRow]) +) +SPEND_BY_RESPONSE_IDS: Final[ReadQuery[SpendByResponseIdsParams, SpendRow]] = ReadQuery( + "spend_by_response_ids", SpendByResponseIdsParams, TypeAdapter(QueryResponse[SpendRow]) +) +LENS_AVAILABILITY: Final[ReadQuery[LensAccessParams, ActivityAvailability]] = ReadQuery( + "availability", LensAccessParams, TypeAdapter(QueryResponse[ActivityAvailability]) +) +LENS_AGENTS: Final[ReadQuery[LensAccessParams, AgentRow]] = ReadQuery( + "agents", LensAccessParams, TypeAdapter(QueryResponse[AgentRow]) +) +LENS_SAMPLE: Final[ReadQuery[LensSampleParams, ExecutionRow]] = ReadQuery( + "sample", LensSampleParams, TypeAdapter(QueryResponse[ExecutionRow]) +) +LENS_CONTENT: Final[ReadQuery[LensContentParams, PartRow]] = ReadQuery( + "content", LensContentParams, TypeAdapter(QueryResponse[PartRow]) +) +LENS_EVIDENCE: Final[ReadQuery[LensEvidenceParams, CountRow]] = ReadQuery( + "evidence", LensEvidenceParams, TypeAdapter(QueryResponse[CountRow]) +) + +TRACE_IDENTITY: Final = ReadQuery("trace_identity", TraceIdentityParams, TypeAdapter(QueryResponse[TraceIdentityRow])) diff --git a/litellm/rust_bridge/trace_query_responses.py b/litellm/rust_bridge/trace_query_responses.py new file mode 100644 index 00000000000..ad7678fcd9b --- /dev/null +++ b/litellm/rust_bridge/trace_query_responses.py @@ -0,0 +1,112 @@ +from collections.abc import Mapping +from typing import Final, Literal + +from pydantic import BaseModel, ConfigDict, JsonValue + +_RESPONSE_CONFIG: Final = ConfigDict(frozen=True, extra="allow") +_HELP_CONFIG: Final = ConfigDict(frozen=True, extra="forbid") +TraceTableName = Literal["otel_traces", "agent_traces_by_key", "spend_logs"] +MetadataValueType = Literal["array", "boolean", "integer", "null", "number", "object", "string"] + + +class TraceQueryColumn(BaseModel): + model_config = _RESPONSE_CONFIG + name: str + type: str + + +class TraceQueryStatistics(BaseModel): + model_config = _RESPONSE_CONFIG + elapsed: float + rows_read: int | str + bytes_read: int | str + + +class TraceSQLResponse(BaseModel): + model_config = _RESPONSE_CONFIG + meta: tuple[TraceQueryColumn, ...] + data: tuple[Mapping[str, JsonValue], ...] + rows: int | str + statistics: TraceQueryStatistics + + +class TraceQueryTable(BaseModel): + model_config = _HELP_CONFIG + name: TraceTableName + columns: tuple[TraceQueryColumn, ...] + + +class TraceQueryNormalizedField(BaseModel): + model_config = _HELP_CONFIG + table: TraceTableName + name: str + column: str + type: str + meaning: str + + +class TraceQueryMetadataField(BaseModel): + model_config = _HELP_CONFIG + path: tuple[str | int, ...] + types: tuple[MetadataValueType, ...] + expression: str + + +class TraceQueryMetadata(BaseModel): + model_config = _HELP_CONFIG + table: TraceTableName + column: str + fields: tuple[TraceQueryMetadataField, ...] + sampled_rows: int + invalid_json_rows: int + truncated: bool + sample_sql: str + scope: str + error: str | None = None + + +class TraceQueryAttributeField(BaseModel): + model_config = _HELP_CONFIG + key: str + type: Literal["String"] + expression: str + + +class TraceQueryAttributes(BaseModel): + model_config = _HELP_CONFIG + table: TraceTableName + column: str + fields: tuple[TraceQueryAttributeField, ...] + truncated: bool + discovery_sql: str + scope: str + error: str | None = None + + +class TraceQueryRelationship(BaseModel): + model_config = _HELP_CONFIG + left: str + right: str + additional_predicates: str + meaning: str + + +class TraceQueryExample(BaseModel): + model_config = _HELP_CONFIG + name: str + sql: str + + +class TraceQueryHelp(BaseModel): + model_config = _HELP_CONFIG + dialect: str + access: str + response: str + tables: tuple[TraceQueryTable, ...] + normalized_fields: tuple[TraceQueryNormalizedField, ...] + metadata: TraceQueryMetadata + attributes: tuple[TraceQueryAttributes, ...] + relationships: tuple[TraceQueryRelationship, ...] + examples: tuple[TraceQueryExample, ...] + gotchas: tuple[str, ...] + guide: str diff --git a/litellm/rust_bridge/traces.py b/litellm/rust_bridge/traces.py index 1e579edb73e..f70b5ec3f93 100644 --- a/litellm/rust_bridge/traces.py +++ b/litellm/rust_bridge/traces.py @@ -1,12 +1,32 @@ from collections.abc import Awaitable, Mapping, Sequence from dataclasses import dataclass -from types import MappingProxyType -from typing import Final, Literal, Protocol, TypedDict, cast +from typing import Final, Literal, Protocol, TypedDict, TypeVar, runtime_checkable -from pydantic import BaseModel, ConfigDict, Field, JsonValue, TypeAdapter +from pydantic import BaseModel, ConfigDict, Field, JsonValue, TypeAdapter, ValidationError from typing_extensions import ReadOnly from litellm.rust_bridge.loader import get_native_bridge +from litellm.rust_bridge.trace_queries import ( + LENS_AGENTS, + LENS_AVAILABILITY, + LENS_CONTENT, + LENS_EVIDENCE, + LENS_SAMPLE, + ActivityAvailability, + AgentRow, + CountRow, + ExecutionRow, + LensAccessParams, + LensContentParams, + LensEvidenceParams, + LensSampleParams, + ParamsT, + PartRow, + ReadQuery, + ReadQueryName, + RowT, +) +from litellm.rust_bridge.trace_query_responses import TraceQueryHelp, TraceSQLResponse class DecodedEvent(TypedDict): @@ -19,6 +39,7 @@ class NormalizedSpan(BaseModel): observation_type: Literal["agent", "llm", "tool", "chain", "framework"] agent_name: str + framework: str litellm_request_id: str model: str input_tokens: int = Field(ge=0, le=2**32 - 1) @@ -56,25 +77,17 @@ class DecodedSpan(TypedDict): consumed_attributes: ReadOnly[tuple[str, str]] -ReadQueryName = Literal["list_traces", "trace_spans", "span_detail", "span_error", "spend_by_response_ids"] +class AllQueryScope(TypedDict): + kind: ReadOnly[Literal["all"]] -class AdminQueryScope(TypedDict): - kind: ReadOnly[Literal["admin"]] +class OwnedQueryScope(TypedDict): + kind: ReadOnly[Literal["owned"]] + user_id: ReadOnly[str] + team_ids: ReadOnly[tuple[str, ...]] -class TeamQueryScope(TypedDict): - kind: ReadOnly[Literal["team"]] - team_id: ReadOnly[str] - - -class KeyQueryScope(TypedDict): - kind: ReadOnly[Literal["key"]] - team_id: ReadOnly[str] - api_key_hash: ReadOnly[str] - - -QueryScope = AdminQueryScope | TeamQueryScope | KeyQueryScope +QueryScope = AllQueryScope | OwnedQueryScope class NativeStore(Protocol): @@ -86,13 +99,14 @@ class NativeStore(Protocol): def query_sql(self, sql: str, scope: QueryScope, secret: str) -> Awaitable[str]: ... - def query_help(self, scope: QueryScope, secret: str) -> Awaitable[str]: ... + def query_help(self, scope: QueryScope, secret: str) -> Awaitable[JsonValue]: ... - def lens_query(self, name: str, parameters: Mapping[str, str | int | Sequence[str]]) -> Awaitable[str]: ... - - def query(self, name: ReadQueryName, parameters: Mapping[str, str | int | Sequence[str]]) -> Awaitable[str]: ... + def query( + self, name: ReadQueryName, parameters: Mapping[str, str | int | float | Sequence[str]] + ) -> Awaitable[str]: ... +@runtime_checkable class NativeTraces(Protocol): NativeTraceConfig: type["NativeConfig"] NativeTraceStorage: type[NativeStore] @@ -108,13 +122,14 @@ class NativeTraces(Protocol): def trace_normalized_field_definitions(self) -> list[dict[str, str]]: ... -class QueryResponse(BaseModel): - model_config = ConfigDict(frozen=True) - data: list[dict[str, JsonValue]] - - -QUERY_PARAMETERS: Final = TypeAdapter(dict[str, str | int | list[str]]) +QUERY_PARAMETERS: Final = TypeAdapter(dict[str, str | int | float | list[str]]) _FIELD_DEFINITIONS_ADAPTER: Final = TypeAdapter(tuple[NormalizedFieldDefinition, ...]) +_SQL_RESPONSE: Final = TypeAdapter(TraceSQLResponse) +_HELP_RESPONSE: Final = TypeAdapter(TraceQueryHelp) +_ResponseT: Final = TypeVar("_ResponseT") +_NATIVE_ADAPTER: Final[TypeAdapter[NativeTraces]] = TypeAdapter( + NativeTraces, config=ConfigDict(arbitrary_types_allowed=True) +) class NativeConfig(Protocol): @@ -132,7 +147,7 @@ def _native() -> NativeTraces: native: Final = get_native_bridge() if native is None: raise RuntimeError("Agent tracing requires the Rust extension") - return cast(NativeTraces, native) # cast-ok: the native extension is validated against this protocol at call sites + return _NATIVE_ADAPTER.validate_python(native) def decode_otlp(body: bytes, content_type: str | None) -> list[DecodedSpan]: @@ -155,6 +170,20 @@ def encode_error(message: str) -> bytes: return _native().trace_encode_error(message) +def _decode_query_response(adapter: TypeAdapter[_ResponseT], body: str) -> _ResponseT: + try: + return adapter.validate_json(body) + except ValidationError as error: + raise RuntimeError("Native trace query returned an invalid response") from error + + +def _validate_query_response(adapter: TypeAdapter[_ResponseT], value: JsonValue) -> _ResponseT: + try: + return adapter.validate_python(value) + except ValidationError as error: + raise RuntimeError("Native trace query returned an invalid response") from error + + class ClickHouseStorage: def __init__(self, config: TraceStorageConfig) -> None: native: Final = _native() @@ -171,35 +200,30 @@ class ClickHouseStorage: async def insert_rows(self, table: str, rows: Sequence[Mapping[str, object]]) -> None: await self._native.insert_rows(table, rows) - async def query( - self, name: ReadQueryName, parameters: Mapping[str, object] | None = None - ) -> list[dict[str, JsonValue]]: - result: Final = await self._native.query( - name, QUERY_PARAMETERS.validate_python(parameters or MappingProxyType({})) - ) - return QueryResponse.model_validate_json(result).data + async def query(self, query: ReadQuery[ParamsT, RowT], parameters: ParamsT) -> tuple[RowT, ...]: + validated: Final = query.parameters.model_validate(parameters) + result: Final = await self._native.query(query.name, QUERY_PARAMETERS.validate_python(validated.model_dump())) + return _decode_query_response(query.response, result).data - async def query_sql(self, sql: str, scope: QueryScope, secret: str) -> str: - return await self._native.query_sql(sql, scope, secret) + async def query_sql(self, sql: str, scope: QueryScope, secret: str) -> TraceSQLResponse: + result: Final = await self._native.query_sql(sql, scope, secret) + return _decode_query_response(_SQL_RESPONSE, result) - async def query_help(self, scope: QueryScope, secret: str) -> str: - return await self._native.query_help(scope, secret) + async def query_help(self, scope: QueryScope, secret: str) -> TraceQueryHelp: + result: Final = await self._native.query_help(scope, secret) + return _validate_query_response(_HELP_RESPONSE, result) - async def _lens_query(self, name: str, parameters: Mapping[str, object]) -> list[dict[str, JsonValue]]: - result: Final = await self._native.lens_query(name, QUERY_PARAMETERS.validate_python(parameters)) - return QueryResponse.model_validate_json(result).data + async def lens_sample(self, parameters: LensSampleParams) -> tuple[ExecutionRow, ...]: + return await self.query(LENS_SAMPLE, parameters) - async def lens_sample(self, parameters: Mapping[str, object]) -> list[dict[str, JsonValue]]: - return await self._lens_query("sample", parameters) + async def lens_availability(self, parameters: LensAccessParams) -> tuple[ActivityAvailability, ...]: + return await self.query(LENS_AVAILABILITY, parameters) - async def lens_availability(self, parameters: Mapping[str, object]) -> list[dict[str, JsonValue]]: - return await self._lens_query("availability", parameters) + async def lens_agents(self, parameters: LensAccessParams) -> tuple[AgentRow, ...]: + return await self.query(LENS_AGENTS, parameters) - async def lens_agents(self, parameters: Mapping[str, object]) -> list[dict[str, JsonValue]]: - return await self._lens_query("agents", parameters) + async def lens_content(self, parameters: LensContentParams) -> tuple[PartRow, ...]: + return await self.query(LENS_CONTENT, parameters) - async def lens_content(self, parameters: Mapping[str, object]) -> list[dict[str, JsonValue]]: - return await self._lens_query("content", parameters) - - async def lens_evidence(self, parameters: Mapping[str, object]) -> list[dict[str, JsonValue]]: - return await self._lens_query("evidence", parameters) + async def lens_evidence(self, parameters: LensEvidenceParams) -> tuple[CountRow, ...]: + return await self.query(LENS_EVIDENCE, parameters) diff --git a/litellm/tracing/decode.py b/litellm/tracing/decode.py index e76c9aad97d..f7aa54e47cd 100644 --- a/litellm/tracing/decode.py +++ b/litellm/tracing/decode.py @@ -177,8 +177,10 @@ def _span_row(span: DecodedSpan) -> SpanRow: StatusMessage=span["status_message"] or _exception_message(span), TeamId="", ApiKeyHash="", + UserId="", ObservationType=normalized.observation_type, AgentName=normalized.agent_name, + Framework=normalized.framework, Model=normalized.model, LiteLLMRequestId=attributes.get("gen_ai.response.id") or normalized.litellm_request_id, InputTokens=normalized.input_tokens, diff --git a/litellm/tracing/receiver.py b/litellm/tracing/receiver.py index 9164e0722a3..c6f256d59ff 100644 --- a/litellm/tracing/receiver.py +++ b/litellm/tracing/receiver.py @@ -45,10 +45,11 @@ class TracingOverloadedError(RuntimeError): class Tenant: """Who sent the spans. Always taken from auth, never from span attributes.""" - def __init__(self, team_id: str, api_key_hash: str, org_id: str = "") -> None: + def __init__(self, team_id: str, api_key_hash: str, org_id: str = "", user_id: str = "") -> None: self.team_id = team_id self.api_key_hash = api_key_hash self.org_id = org_id + self.user_id = user_id def stamp(self, row: SpanRow) -> SpanRow: return self.stamp_rows((row,))[0] @@ -63,6 +64,7 @@ class Tenant: "litellm.team_id": self.team_id, "litellm.api_key_hash": self.api_key_hash, "litellm.org_id": self.org_id, + "litellm.user_id": self.user_id, } ) for identity, attributes in resources.items() @@ -75,6 +77,7 @@ class Tenant: **row, "TeamId": self.team_id, "ApiKeyHash": self.api_key_hash, + "UserId": self.user_id, "ResourceAttributes": resource, } return stamped diff --git a/litellm/tracing/store.py b/litellm/tracing/store.py index 91420ffd025..623e25849da 100644 --- a/litellm/tracing/store.py +++ b/litellm/tracing/store.py @@ -7,7 +7,7 @@ from collections.abc import Mapping, Sequence from datetime import datetime, timezone from itertools import chain from types import MappingProxyType -from typing import Any, Final +from typing import Annotated, Final, TypeAlias from pydantic import BaseModel, ConfigDict, Field, TypeAdapter @@ -16,6 +16,23 @@ from litellm.constants import AGENT_TRACING_LIST_PAGE_SIZE from litellm.integrations.clickhouse.schema import ( OTEL_TRACES_TABLE, ) +from litellm.rust_bridge.trace_queries import ( + LIST_TRACES, + SPAN_DETAIL, + SPAN_ERROR, + SPEND_BY_RESPONSE_IDS, + TRACE_IDENTITY, + TRACE_SPANS, + ListTracesParams, + ListTracesRow, + SpanDetailParams, + SpanErrorParams, + SpendByResponseIdsParams, + SpendRow, + TraceIdentityParams, + TraceSpansParams, + TraceSpansRow, +) from litellm.rust_bridge.traces import ClickHouseStorage from litellm.tracing.types import ( AgentNode, @@ -33,7 +50,11 @@ from litellm.tracing.ui_format import to_ui_content NANOS_PER_MS: Final = 1_000_000 SPEND_WINDOW_MS: Final = 30 * 60 * 1000 -_STATUS: Final = MappingProxyType({"STATUS_CODE_OK": "ok", "STATUS_CODE_ERROR": "error"}) +_STATUS: Final[Mapping[str, SpanStatus]] = MappingProxyType({"STATUS_CODE_OK": "ok", "STATUS_CODE_ERROR": "error"}) + + +TraceCursorParts: TypeAlias = tuple[Annotated[int, Field(gt=0)], Annotated[str, Field(min_length=1)]] +_TRACE_CURSOR: Final[TypeAdapter[TraceCursorParts]] = TypeAdapter(TraceCursorParts) class _ErrorCursor(BaseModel): @@ -42,43 +63,33 @@ class _ErrorCursor(BaseModel): version: str = Field(pattern=r"^[A-F0-9]{64}$") -class _ErrorRow(BaseModel): - model_config = ConfigDict(frozen=True) - span_id: str - message: str - total_chars: int - version: str +class AmbiguousTraceError(ValueError): + pass -class _SpendRow(BaseModel): - model_config = ConfigDict(frozen=True) - - request_id: str - response_id: str - team_id: str - api_key: str - spend: float - start_ms: int - - -_SPEND_ROWS: Final = TypeAdapter(tuple[_SpendRow, ...]) - - -def _spend_for(request_id: str, team_id: str, api_key_hash: str, rows: Sequence[_SpendRow]) -> float | None: +def _spend_for( + request_id: str, team_id: str, api_key_hash: str, user_id: str, rows: Sequence[SpendRow] +) -> float | None: + if not request_id: + return None matches: Final = tuple( - row for row in rows if row.response_id == request_id and row.team_id == team_id and row.api_key == api_key_hash + row + for row in rows + if row.response_id == request_id + and row.team_id == team_id + and (bool(user_id and row.user == user_id) or bool(api_key_hash and row.api_key == api_key_hash)) ) return matches[0].spend if len(matches) == 1 else None def _trace_spend( - request_ids: Sequence[str], team_id: str, api_key_hash: str, rows: Sequence[_SpendRow] + request_ids: Sequence[str], team_id: str, api_key_hash: str, user_id: str, rows: Sequence[SpendRow] ) -> float | None: - ids: Final = frozenset(request_id for request_id in request_ids if request_id) - costs: Final = tuple(_spend_for(request_id, team_id, api_key_hash, rows) for request_id in ids) - return ( - sum(cost for cost in costs if cost is not None) if costs and all(cost is not None for cost in costs) else None - ) + if not request_ids or any(not request_id for request_id in request_ids): + return None + ids: Final = frozenset(request_ids) + costs: Final = tuple(_spend_for(request_id, team_id, api_key_hash, user_id, rows) for request_id in ids) + return sum(cost for cost in costs if cost is not None) if all(cost is not None for cost in costs) else None def encode_cursor(start_ms: int, trace_id: str) -> str: @@ -89,18 +100,7 @@ def decode_cursor(cursor: str | None) -> tuple[int, str]: if not cursor: return 0, "" try: - value: Final = json.loads(base64.b64decode(cursor, altchars=b"-_", validate=True)) - if ( - not isinstance(value, list) - or len(value) != 2 - or not isinstance(value[0], int) - or isinstance(value[0], bool) - or value[0] <= 0 - or not isinstance(value[1], str) - or not value[1] - ): - raise ValueError("Invalid trace cursor") - return value[0], value[1] + return _TRACE_CURSOR.validate_json(base64.b64decode(cursor, altchars=b"-_", validate=True), strict=True) except (ValueError, UnicodeError, binascii.Error) as error: raise ValueError("Invalid trace cursor") from error @@ -113,12 +113,14 @@ def _status(code: str) -> SpanStatus: return _STATUS.get(code, "unset") -def trace_summary_from_row(row: dict[str, Any], spend_rows: Sequence[_SpendRow] = ()) -> TraceSummary: +def trace_summary_from_row(row: ListTracesRow, spend_rows: Sequence[SpendRow] = ()) -> TraceSummary: return TraceSummary( trace_id=row["trace_id"], trace_ref=row.get("trace_ref", ""), name=row["name"], service=row["service"], + agent_names=tuple(row.get("agent_names") or ()), + frameworks=tuple(row.get("frameworks") or ()), input_preview=row["input_preview"], start_time=_iso(int(row["start_ms"])), duration_ms=float(row["duration_ms"]), @@ -133,18 +135,23 @@ def trace_summary_from_row(row: dict[str, Any], spend_rows: Sequence[_SpendRow] output_tokens=int(row["output_tokens"]), models=tuple(row["models"]), spend=_trace_spend( - row.get("request_ids") or (), row.get("team_id") or "", row.get("api_key_hash") or "", spend_rows + row.get("request_ids") or (), + row.get("team_id") or "", + row.get("api_key_hash") or "", + row.get("user_id") or "", + spend_rows, ), ) -def span_from_row(row: dict[str, Any], trace_start_ns: int, spend_rows: Sequence[_SpendRow] = ()) -> Span: +def span_from_row(row: TraceSpansRow, trace_start_ns: int, spend_rows: Sequence[SpendRow] = ()) -> Span: return Span( span_id=row["span_id"], parent_span_id=row["parent_span_id"] or None, name=row["name"], type=row["type"], agent=row["agent"], + framework=row.get("framework") or "", start_offset_ms=(int(row["start_ns"]) - trace_start_ns) / NANOS_PER_MS, duration_ms=int(row["duration_ns"]) / NANOS_PER_MS, status=_status(row["status"]), @@ -156,7 +163,13 @@ def span_from_row(row: dict[str, Any], trace_start_ns: int, spend_rows: Sequence output_tokens=int(row["output_tokens"]), litellm_request_id=row["litellm_request_id"] or None, spend=( - _spend_for(row["litellm_request_id"], row.get("team_id") or "", row.get("api_key_hash") or "", spend_rows) + _spend_for( + row["litellm_request_id"], + row.get("team_id") or "", + row.get("api_key_hash") or "", + row.get("user_id") or "", + spend_rows, + ) if row["litellm_request_id"] else None ), @@ -169,8 +182,8 @@ def _parent_agent_of(span: Span, by_id: Mapping[str, Span]) -> str | None: if parent_id is None or parent_id not in by_id or parent_id == span["span_id"]: return None parent = by_id[parent_id] - if parent["type"] == "agent" and parent["name"] != span["name"]: - return parent["name"] + if parent["type"] == "agent" and (parent["agent"] or parent["name"]) != (span["agent"] or span["name"]): + return parent["agent"] or parent["name"] parent_id = parent["parent_span_id"] return None @@ -183,9 +196,9 @@ def agent_nodes(spans: Sequence[Span]) -> tuple[AgentNode, ...]: if span["type"] != "agent": continue node = agents.setdefault( - span["name"], + span["agent"] or span["name"], AgentNode( - name=span["name"], + name=span["agent"] or span["name"], parent_agent=_parent_agent_of(span, by_id), invocations=0, llm_calls=0, @@ -219,22 +232,15 @@ def agent_nodes(spans: Sequence[Span]) -> tuple[AgentNode, ...]: def _agent_spend(spans: Sequence[Span], agent_name: str) -> float | None: - by_request: Final = MappingProxyType( - { - span["litellm_request_id"]: span["spend"] - for span in spans - if span["type"] == "llm" and span["agent"] == agent_name and span["litellm_request_id"] - } - ) - return ( - sum(cost for cost in by_request.values() if cost is not None) - if by_request and all(cost is not None for cost in by_request.values()) - else None - ) + llm_spans: Final = tuple(span for span in spans if span["type"] == "llm" and span["agent"] == agent_name) + if any(not span["litellm_request_id"] or span["spend"] is None for span in llm_spans): + return None + by_request: Final = MappingProxyType({span["litellm_request_id"]: span["spend"] for span in llm_spans}) + return sum(cost for cost in by_request.values() if cost is not None) if by_request else None def trace_from_rows( - trace_id: str, rows: list[dict[str, Any]], trace_ref: str = "", spend_rows: Sequence[_SpendRow] = () + trace_id: str, rows: Sequence[TraceSpansRow], trace_ref: str = "", spend_rows: Sequence[SpendRow] = () ) -> Trace | None: if not rows: return None @@ -250,6 +256,8 @@ def trace_from_rows( trace_ref=trace_ref, name=root["name"], service=rows[0]["service"], + agent_names=tuple(sorted(frozenset(s["agent"] for s in spans if s["agent"]))), + frameworks=tuple(sorted(frozenset(s["framework"] for s in spans if s["framework"]))), input_preview=root["input_preview"], start_time=_iso(trace_start_ns // NANOS_PER_MS), duration_ms=(trace_end_ns - trace_start_ns) / NANOS_PER_MS, @@ -264,9 +272,10 @@ def trace_from_rows( output_tokens=sum(s["output_tokens"] for s in spans), models=tuple(sorted(frozenset(s["model"] for s in llm_spans if s["model"]))), spend=_trace_spend( - tuple(row["litellm_request_id"] for row in rows), + tuple(row["litellm_request_id"] for row in rows if row["type"] == "llm" or row["litellm_request_id"]), rows[0].get("team_id") or "", rows[0].get("api_key_hash") or "", + rows[0].get("user_id") or "", spend_rows, ), ), @@ -284,28 +293,34 @@ class TraceStore: async def insert_spans(self, rows: Sequence[SpanRow]) -> None: await self.storage.insert_rows(OTEL_TRACES_TABLE, tuple(rows)) + async def _reference(self, trace_id: str, scope: TraceScope, trace_ref: str) -> str | None: + if trace_ref: + return trace_ref + identities: Final = await self.storage.query(TRACE_IDENTITY, TraceIdentityParams(**scope, trace_id=trace_id)) + if len(identities) > 1: + raise AmbiguousTraceError("Multiple traces have this ID; provide trace_ref") + return identities[0].trace_ref if identities else None + async def _spend_rows( self, scope: TraceScope, request_ids: Sequence[str], start_ms: int, end_ms: int - ) -> tuple[_SpendRow, ...]: + ) -> tuple[SpendRow, ...]: ids: Final = tuple(sorted(frozenset(request_id for request_id in request_ids if request_id))) if not ids: return () try: rows: Final = await self.storage.query( - "spend_by_response_ids", - MappingProxyType( - { - **scope, - "response_ids": ids, - "start_ms": start_ms - SPEND_WINDOW_MS, - "end_ms": end_ms + SPEND_WINDOW_MS, - } + SPEND_BY_RESPONSE_IDS, + SpendByResponseIdsParams( + **scope, + response_ids=ids, + start_ms=start_ms - SPEND_WINDOW_MS, + end_ms=end_ms + SPEND_WINDOW_MS, ), ) except RuntimeError as error: verbose_logger.warning("Trace spend lookup unavailable: %s", error) return () - return _SPEND_ROWS.validate_python(rows) + return tuple(rows) async def list_traces( self, @@ -316,17 +331,15 @@ class TraceStore: limit: int = AGENT_TRACING_LIST_PAGE_SIZE, ) -> TracePage: cursor_ms, cursor_trace_id = decode_cursor(cursor) - rows = await self.storage.query( - "list_traces", - MappingProxyType( - { - **scope, - "start_ms": start_ms, - "end_ms": end_ms, - "cursor_ms": cursor_ms, - "cursor_trace_id": cursor_trace_id, - "limit": limit, - } + rows: Final = await self.storage.query( + LIST_TRACES, + ListTracesParams( + **scope, + start_ms=start_ms, + end_ms=end_ms, + cursor_ms=cursor_ms, + cursor_trace_id=cursor_trace_id, + limit=limit, ), ) spend_rows: Final = await self._spend_rows( @@ -335,12 +348,17 @@ class TraceStore: min((int(row["start_ms"]) for row in rows), default=start_ms), max((int(row["start_ms"]) + int(row["duration_ms"]) for row in rows), default=end_ms), ) - next_cursor = encode_cursor(int(rows[-1]["start_ms"]), rows[-1]["trace_ref"]) if len(rows) == limit else None + next_cursor: Final = ( + encode_cursor(int(rows[-1]["start_ms"]), rows[-1]["trace_ref"]) if len(rows) == limit else None + ) return TracePage(data=tuple(trace_summary_from_row(r, spend_rows) for r in rows), next_cursor=next_cursor) async def get_trace(self, trace_id: str, scope: TraceScope, trace_ref: str = "") -> Trace | None: - rows = await self.storage.query( - "trace_spans", MappingProxyType({**scope, "trace_id": trace_id, "trace_ref": trace_ref}) + reference: Final = await self._reference(trace_id, scope, trace_ref) + if reference is None: + return None + rows: Final = await self.storage.query( + TRACE_SPANS, TraceSpansParams(**scope, trace_id=trace_id, trace_ref=reference) ) spend_rows: Final = await self._spend_rows( scope, @@ -348,12 +366,15 @@ class TraceStore: min((int(row["start_ns"]) // NANOS_PER_MS for row in rows), default=0), max(((int(row["start_ns"]) + int(row["duration_ns"])) // NANOS_PER_MS for row in rows), default=0), ) - return trace_from_rows(trace_id, rows, trace_ref, spend_rows) + return trace_from_rows(trace_id, rows, reference, spend_rows) async def get_span(self, trace_id: str, span_id: str, scope: TraceScope, trace_ref: str = "") -> SpanDetail | None: - rows = await self.storage.query( - "span_detail", - MappingProxyType({**scope, "trace_id": trace_id, "span_id": span_id, "trace_ref": trace_ref}), + reference: Final = await self._reference(trace_id, scope, trace_ref) + if reference is None: + return None + rows: Final = await self.storage.query( + SPAN_DETAIL, + SpanDetailParams(**scope, trace_id=trace_id, span_id=span_id, trace_ref=reference), ) if not rows: return None @@ -363,7 +384,7 @@ class TraceStore: output=rows[0]["output"], input_ui=to_ui_content(rows[0]["input"]), output_ui=to_ui_content(rows[0]["output"]), - attributes=rows[0]["attributes"], + attributes=dict(rows[0]["attributes"]), ) async def get_span_error( @@ -377,22 +398,23 @@ class TraceStore: ) except (ValueError, binascii.Error) as error: raise ValueError("Invalid diagnostic cursor") from error + reference: Final = await self._reference(trace_id, scope, trace_ref) + if reference is None: + return None rows: Final = await self.storage.query( - "span_error", - MappingProxyType( - { - **scope, - "trace_id": trace_id, - "span_id": span_id, - "trace_ref": trace_ref, - "error_offset": position.offset if position else 0, - "error_version": position.version if position else "", - } + SPAN_ERROR, + SpanErrorParams( + **scope, + trace_id=trace_id, + span_id=span_id, + trace_ref=reference, + error_offset=position.offset if position else 0, + error_version=position.version if position else "", ), ) if not rows: return None - row: Final = _ErrorRow.model_validate(rows[0]) + row: Final = rows[0] offset: Final = (position.offset if position else 0) + len(row.message) continuation: Final = _ErrorCursor(offset=offset, version=row.version) if offset < row.total_chars else None return SpanErrorPage( diff --git a/litellm/tracing/types.py b/litellm/tracing/types.py index ff965483013..e177094d464 100644 --- a/litellm/tracing/types.py +++ b/litellm/tracing/types.py @@ -26,6 +26,7 @@ class Span(TypedDict): name: ReadOnly[str] type: ReadOnly[SpanType] agent: ReadOnly[str] # the agent this span runs inside, e.g. "researcher" + framework: ReadOnly[str] # SDK that emitted the span, e.g. "claude-agent-sdk"; "" when unknown start_offset_ms: ReadOnly[float] # relative to trace start duration_ms: ReadOnly[float] status: ReadOnly[SpanStatus] @@ -56,6 +57,8 @@ class TraceSummary(TypedDict): trace_ref: ReadOnly[NotRequired[str]] name: ReadOnly[str] service: ReadOnly[str] + agent_names: ReadOnly[NotRequired[tuple[str, ...]]] + frameworks: ReadOnly[NotRequired[tuple[str, ...]]] input_preview: ReadOnly[str] start_time: ReadOnly[str] # ISO 8601 duration_ms: ReadOnly[float] @@ -100,10 +103,11 @@ class SpanErrorPage(TypedDict): class TraceScope(TypedDict): - """Who is asking. Empty team_ids = all teams (admins only).""" + """Authenticated request-log visibility.""" + all_teams: ReadOnly[Literal[0, 1]] + user_id: ReadOnly[str] team_ids: ReadOnly[tuple[str, ...]] - api_key_hash: ReadOnly[str] class SpanRow(TypedDict): @@ -126,8 +130,10 @@ class SpanRow(TypedDict): StatusMessage: ReadOnly[str] TeamId: ReadOnly[str] ApiKeyHash: ReadOnly[str] + UserId: ReadOnly[str] ObservationType: SpanType AgentName: str + Framework: ReadOnly[str] LiteLLMRequestId: str Model: str InputTokens: int diff --git a/litellm/types/management_endpoints/auto_router_endpoints.py b/litellm/types/management_endpoints/auto_router_endpoints.py index 00083e01f54..ded971f6705 100644 --- a/litellm/types/management_endpoints/auto_router_endpoints.py +++ b/litellm/types/management_endpoints/auto_router_endpoints.py @@ -205,14 +205,18 @@ class AutoRouterCacheStats(BaseModel): class AutoRouterBenchmarkTotals(BaseModel): - """Session-shape and savings aggregates over auto-routed traffic in the window.""" + """Auto-routed traffic in the window. Turns, spend and savings count requests on the selected UTC days; + the session averages and cache stats describe every session overlapping the window, whole.""" - sessions: int - turns: int - avg_turns_per_session: float - avg_session_seconds: float - avg_tokens_per_session: float - spend: float = Field(description="What the routed traffic actually cost") + sessions: int = Field(description="Sessions overlapping the window, counted whole") + turns: int = Field(description="Auto-routed requests on the selected UTC days") + avg_turns_per_session: float | None = Field( + description="Lifetime turns per overlapping session; null when the window has routed requests but no session " + "rows for this router type, such as an alias whose router type changed mid-session" + ) + avg_session_seconds: float | None = Field(description="Lifetime seconds per overlapping session; null as above") + avg_tokens_per_session: float | None = Field(description="Lifetime tokens per overlapping session; null as above") + spend: float = Field(description="What the selected days' routed traffic actually cost") classifier_cost: float | None = Field( description="Recorded LLM classifier cost already included in spend; null when any session turns predate " "subtotal recording, and zero for an empty window" @@ -229,14 +233,19 @@ class AutoRouterBenchmarkTotals(BaseModel): "null when classification costs for those requests are unavailable", ) saved_spend: float | None = Field( - description="Recorded historical savings plus newer estimates; null when traffic has no recorded savings estimates" + description="Recorded savings on the selected UTC days; null when traffic has no recorded savings estimates. " + "On totals this is the same daily figure the Overall savings view reports" + ) + unattributed_saved_spend: float | None = Field( + default=None, + description="Part of saved_spend no router's daily rows account for, such as history recorded before " + "per-router daily tracking; when set, baseline_spend and saved_pct are null", ) baseline_spend: float | None = Field( description="Estimated single-model cost: compared actual spend plus recorded savings; " "null when traffic has no recorded savings" ) saved_pct: float | None = Field(description="Recorded savings over baseline_spend, as a percentage") - saved_per_session: float | None = Field(description="Recorded savings per session, including historical estimates") cache: AutoRouterCacheStats @@ -291,7 +300,7 @@ class AutoRouterSessionResponse(BaseModel): class AutoRouterBenchmarksResponse(BaseModel): - """Benchmarks for the auto-router dashboard, aggregated from the per-session rollup.""" + """Benchmarks for the auto-router dashboard, aggregated from the per-session and per-day rollups.""" start_date: str = Field(description="Window start day, YYYY-MM-DD UTC, inclusive") end_date: str = Field(description="Window end day, YYYY-MM-DD UTC, inclusive") diff --git a/litellm/types/mcp.py b/litellm/types/mcp.py index fec5e84c8df..da7401e2a2e 100644 --- a/litellm/types/mcp.py +++ b/litellm/types/mcp.py @@ -63,7 +63,14 @@ DEFAULT_SUBJECT_TOKEN_TYPE: Final = "urn:ietf:params:oauth:token-type:access_tok MCPTransportType = Literal[MCPTransport.sse, MCPTransport.http, MCPTransport.stdio] MCPLegacyVersion = Literal["2024-11-05", "2025-03-26", "2025-06-18", "2025-11-25"] MCP_LEGACY_VERSIONS: Final[tuple[MCPLegacyVersion, ...]] = ("2024-11-05", "2025-03-26", "2025-06-18", "2025-11-25") -MCPUpstreamProtocol = MCPLegacyVersion | Literal["auto"] +MCPUpstreamProtocol = MCPLegacyVersion | Literal["auto", "2026-07-28"] + + +def validate_mcp_protocol_transport(protocol_version: MCPUpstreamProtocol, transport: MCPTransportType) -> None: + if protocol_version == "2026-07-28" and transport == MCPTransport.sse: + raise ValueError("Modern MCP requires HTTP or stdio transport") + + MCPAdvertisedVersions = Annotated[tuple[MCPLegacyVersion, ...], Field(min_length=1)] MCPSpecVersionType = Literal[ MCPSpecVersion.nov_2024, diff --git a/litellm/types/mcp_server/mcp_server_manager.py b/litellm/types/mcp_server/mcp_server_manager.py index 91ae95eff48..2ee19b3e59a 100644 --- a/litellm/types/mcp_server/mcp_server_manager.py +++ b/litellm/types/mcp_server/mcp_server_manager.py @@ -13,13 +13,14 @@ from litellm.types.mcp import ( MCPTransportType, MCPUpstreamProtocol, normalize_upstream_header_name, + validate_mcp_protocol_transport, ) # MCPInfo now allows arbitrary additional fields for custom metadata def _validate_mcp_protocol_metadata(value: dict[str, object]) -> dict[str, object]: if "protocol_version" in value: - TypeAdapter(MCPUpstreamProtocol).validate_python(value["protocol_version"]) + TypeAdapter[MCPUpstreamProtocol](MCPUpstreamProtocol).validate_python(value["protocol_version"]) return value @@ -277,9 +278,10 @@ class MCPServer(BaseModel): @model_validator(mode="after") def resolve_protocol_version(self) -> Self: if "protocol_version" not in self.model_fields_set and self.mcp_info is not None: - self.protocol_version = TypeAdapter(MCPUpstreamProtocol).validate_python( + self.protocol_version = TypeAdapter[MCPUpstreamProtocol](MCPUpstreamProtocol).validate_python( self.mcp_info.get("protocol_version", "auto") ) + validate_mcp_protocol_transport(self.protocol_version, self.transport) return self @model_validator(mode="after") diff --git a/litellm/types/proxy/discovery_endpoints/ui_discovery_endpoints.py b/litellm/types/proxy/discovery_endpoints/ui_discovery_endpoints.py index d52877b7c1e..4e0faabc132 100644 --- a/litellm/types/proxy/discovery_endpoints/ui_discovery_endpoints.py +++ b/litellm/types/proxy/discovery_endpoints/ui_discovery_endpoints.py @@ -11,4 +11,5 @@ class UiDiscoveryEndpoints(BaseModel): sso_configured: bool hide_default_credentials_hint: bool = False is_control_plane: bool = False + mcp_stdio_enabled: bool = False workers: list[WorkerRegistryEntry] = [] diff --git a/model_prices_and_context_window.json b/model_prices_and_context_window.json index 6a548e7a82d..21bdee73464 100644 --- a/model_prices_and_context_window.json +++ b/model_prices_and_context_window.json @@ -386,16 +386,17 @@ "supports_vision": true }, "amazon.nova-2-pro-preview-20251202-v1:0": { - "cache_read_input_token_cost": 5.46875e-07, - "input_cost_per_token": 2.1875e-06, - "input_cost_per_image_token": 2.1875e-06, - "input_cost_per_audio_token": 2.1875e-06, + "cache_read_input_token_cost": 3.125e-07, + "input_cost_per_token": 1.25e-06, + "input_cost_per_image_token": 1.25e-06, + "input_cost_per_audio_token": 1.25e-06, "litellm_provider": "bedrock_converse", "max_input_tokens": 1000000, "max_output_tokens": 64000, "max_tokens": 64000, "mode": "chat", - "output_cost_per_token": 1.75e-05, + "output_cost_per_token": 1e-05, + "source": "https://aws.amazon.com/nova/pricing/", "supports_function_calling": true, "supports_pdf_input": true, "supports_prompt_caching": true, @@ -424,16 +425,17 @@ "supports_vision": true }, "apac.amazon.nova-2-pro-preview-20251202-v1:0": { - "cache_read_input_token_cost": 5.46875e-07, - "input_cost_per_token": 2.1875e-06, - "input_cost_per_image_token": 2.1875e-06, - "input_cost_per_audio_token": 2.1875e-06, + "cache_read_input_token_cost": 3.4375e-07, + "input_cost_per_token": 1.375e-06, + "input_cost_per_image_token": 1.375e-06, + "input_cost_per_audio_token": 1.375e-06, "litellm_provider": "bedrock_converse", "max_input_tokens": 1000000, "max_output_tokens": 64000, "max_tokens": 64000, "mode": "chat", - "output_cost_per_token": 1.75e-05, + "output_cost_per_token": 1.1e-05, + "source": "https://aws.amazon.com/nova/pricing/", "supports_function_calling": true, "supports_pdf_input": true, "supports_prompt_caching": true, @@ -462,16 +464,17 @@ "supports_vision": true }, "eu.amazon.nova-2-pro-preview-20251202-v1:0": { - "cache_read_input_token_cost": 5.46875e-07, - "input_cost_per_token": 2.1875e-06, - "input_cost_per_image_token": 2.1875e-06, - "input_cost_per_audio_token": 2.1875e-06, + "cache_read_input_token_cost": 3.4375e-07, + "input_cost_per_token": 1.375e-06, + "input_cost_per_image_token": 1.375e-06, + "input_cost_per_audio_token": 1.375e-06, "litellm_provider": "bedrock_converse", "max_input_tokens": 1000000, "max_output_tokens": 64000, "max_tokens": 64000, "mode": "chat", - "output_cost_per_token": 1.75e-05, + "output_cost_per_token": 1.1e-05, + "source": "https://aws.amazon.com/nova/pricing/", "supports_function_calling": true, "supports_pdf_input": true, "supports_prompt_caching": true, @@ -500,16 +503,17 @@ "supports_vision": true }, "us.amazon.nova-2-pro-preview-20251202-v1:0": { - "cache_read_input_token_cost": 5.46875e-07, - "input_cost_per_token": 2.1875e-06, - "input_cost_per_image_token": 2.1875e-06, - "input_cost_per_audio_token": 2.1875e-06, + "cache_read_input_token_cost": 3.4375e-07, + "input_cost_per_token": 1.375e-06, + "input_cost_per_image_token": 1.375e-06, + "input_cost_per_audio_token": 1.375e-06, "litellm_provider": "bedrock_converse", "max_input_tokens": 1000000, "max_output_tokens": 64000, "max_tokens": 64000, "mode": "chat", - "output_cost_per_token": 1.75e-05, + "output_cost_per_token": 1.1e-05, + "source": "https://aws.amazon.com/nova/pricing/", "supports_function_calling": true, "supports_pdf_input": true, "supports_prompt_caching": true, @@ -42258,14 +42262,14 @@ "supports_web_search": false }, "openrouter/deepseek/deepseek-v4.1-flash": { - "cache_read_input_token_cost": 1e-08, - "input_cost_per_token": 3e-08, + "cache_read_input_token_cost": 6e-09, + "input_cost_per_token": 3e-07, "litellm_provider": "openrouter", "max_input_tokens": 1048576, "max_output_tokens": 943718, "max_tokens": 943718, "mode": "chat", - "output_cost_per_token": 5e-07, + "output_cost_per_token": 1.2e-06, "source": "https://openrouter.ai/api/v1/models", "supports_audio_input": false, "supports_function_calling": true, @@ -43604,19 +43608,19 @@ "supports_web_search": false }, "openrouter/qwen/qwen3.5-35b-a3b": { - "input_cost_per_token": 1.625e-07, + "input_cost_per_token": 1.5e-07, "litellm_provider": "openrouter", "max_input_tokens": 262144, - "max_output_tokens": 16384, - "max_tokens": 16384, + "max_output_tokens": 235929, + "max_tokens": 235929, "mode": "chat", - "output_cost_per_token": 1.3e-06, + "output_cost_per_token": 1e-06, "source": "https://openrouter.ai/api/v1/models", "supports_function_calling": true, "supports_reasoning": true, "supports_tool_choice": true, "supports_vision": true, - "cache_read_input_token_cost": 1.5625e-07, + "cache_read_input_token_cost": 5e-08, "supports_audio_input": false, "supports_pdf_input": false, "supports_prompt_caching": true, @@ -43924,14 +43928,14 @@ }, "openrouter/z-ai/glm-5.1": { "cache_creation_input_token_cost": 0.0, - "cache_read_input_token_cost": 1.7914e-07, - "input_cost_per_token": 9.646e-07, + "cache_read_input_token_cost": 2.6e-07, + "input_cost_per_token": 1.4e-06, "litellm_provider": "openrouter", "max_input_tokens": 204800, "max_output_tokens": 131072, "max_tokens": 131072, "mode": "chat", - "output_cost_per_token": 3.0316e-06, + "output_cost_per_token": 4.4e-06, "source": "https://openrouter.ai/api/v1/models", "supports_audio_input": false, "supports_function_calling": true, @@ -64014,6 +64018,7 @@ ] }, "xai/grok-voice-transcribe-1.0": { + "deprecation_date": "2026-10-02", "input_cost_per_second": 2.778e-05, "litellm_provider": "xai", "metadata": { @@ -67418,13 +67423,13 @@ "supports_web_search": false }, "openrouter/z-ai/glm-5.3": { - "input_cost_per_token": 2.219e-07, - "output_cost_per_token": 3.39e-06, - "cache_read_input_token_cost": 1.775e-07, + "input_cost_per_token": 1.4e-06, + "output_cost_per_token": 4.4e-06, + "cache_read_input_token_cost": 1.4e-07, "litellm_provider": "openrouter", "max_input_tokens": 1048576, - "max_output_tokens": 943718, - "max_tokens": 943718, + "max_output_tokens": 131072, + "max_tokens": 131072, "mode": "chat", "source": "https://openrouter.ai/api/v1/models", "supports_audio_input": false, @@ -67555,8 +67560,8 @@ "supports_prompt_caching": true }, "openrouter/deepseek/deepseek-v4-flash-0731": { - "cache_read_input_token_cost": 1.08e-08, - "input_cost_per_token": 1.08e-08, + "cache_read_input_token_cost": 5.1e-09, + "input_cost_per_token": 5.1e-09, "litellm_provider": "openrouter", "max_input_tokens": 1048576, "max_output_tokens": 943718, @@ -67608,6 +67613,7 @@ "input_cost_per_token": 9e-08, "output_cost_per_token": 1.8e-07, "cache_read_input_token_cost": 9e-09, + "deprecation_date": "2026-10-31", "litellm_provider": "openrouter", "max_input_tokens": 1048576, "max_output_tokens": 131072, @@ -67625,6 +67631,7 @@ "supports_web_search": false }, "openrouter/poolside/laguna-s-2.1:free": { + "deprecation_date": "2026-10-31", "input_cost_per_token": 0.0, "output_cost_per_token": 0.0, "litellm_provider": "openrouter", @@ -67644,14 +67651,14 @@ "supports_web_search": false }, "openrouter/moonshotai/kimi-k3": { - "cache_read_input_token_cost": 4.357e-07, - "input_cost_per_token": 4.357e-07, + "cache_read_input_token_cost": 2.7e-07, + "input_cost_per_token": 2.7e-06, "litellm_provider": "openrouter", "max_input_tokens": 1048576, "max_output_tokens": 943718, "max_tokens": 943718, "mode": "chat", - "output_cost_per_token": 1e-05, + "output_cost_per_token": 1.35e-05, "source": "https://openrouter.ai/api/v1/models", "supports_audio_input": false, "supports_function_calling": true, @@ -67667,6 +67674,7 @@ "input_cost_per_token": 6e-08, "output_cost_per_token": 1.2e-07, "cache_read_input_token_cost": 3e-08, + "deprecation_date": "2026-10-31", "litellm_provider": "openrouter", "max_input_tokens": 262144, "max_output_tokens": 32768, @@ -67684,6 +67692,7 @@ "supports_web_search": false }, "openrouter/poolside/laguna-xs-2.1:free": { + "deprecation_date": "2026-10-31", "input_cost_per_token": 0.0, "output_cost_per_token": 0.0, "litellm_provider": "openrouter", @@ -67866,13 +67875,13 @@ "supports_web_search": false }, "openrouter/nvidia/nemotron-3-ultra-550b-a55b": { - "input_cost_per_token": 6e-07, - "output_cost_per_token": 2.4e-06, - "cache_read_input_token_cost": 1.2e-07, + "input_cost_per_token": 5e-07, + "output_cost_per_token": 2.2e-06, + "cache_read_input_token_cost": 1e-07, "litellm_provider": "openrouter", "max_input_tokens": 262144, - "max_output_tokens": 182520, - "max_tokens": 182520, + "max_output_tokens": 16384, + "max_tokens": 16384, "mode": "chat", "source": "https://openrouter.ai/api/v1/models", "supports_audio_input": false, @@ -68130,14 +68139,14 @@ "supports_web_search": true }, "openrouter/deepseek/deepseek-v4-flash": { - "cache_read_input_token_cost": 8.372e-09, - "input_cost_per_token": 4.186e-08, + "cache_read_input_token_cost": 5.6e-09, + "input_cost_per_token": 2.8e-08, "litellm_provider": "openrouter", "max_input_tokens": 1048576, - "max_output_tokens": 131072, - "max_tokens": 131072, + "max_output_tokens": 384000, + "max_tokens": 384000, "mode": "chat", - "output_cost_per_token": 8.372e-08, + "output_cost_per_token": 5.6e-08, "source": "https://openrouter.ai/api/v1/models", "supports_audio_input": false, "supports_function_calling": true, @@ -68171,14 +68180,14 @@ "supports_web_search": false }, "openrouter/google/gemma-4-26b-a4b-it": { - "cache_read_input_token_cost": 4.25e-08, - "input_cost_per_token": 7.65e-08, + "cache_read_input_token_cost": 3.75e-08, + "input_cost_per_token": 6.75e-08, "litellm_provider": "openrouter", "max_input_tokens": 262144, "max_output_tokens": 235929, "max_tokens": 235929, "mode": "chat", - "output_cost_per_token": 2.55e-07, + "output_cost_per_token": 2.25e-07, "source": "https://openrouter.ai/api/v1/models", "supports_audio_input": false, "supports_function_calling": true, @@ -68957,11 +68966,11 @@ "openrouter/deepseek/deepseek-v3.1-terminus": { "cache_read_input_token_cost": 1.35e-07, "deprecation_date": "2026-09-28", - "input_cost_per_token": 3e-07, + "input_cost_per_token": 2.7e-07, "litellm_provider": "openrouter", "max_input_tokens": 163840, - "max_output_tokens": 65536, - "max_tokens": 65536, + "max_output_tokens": 147456, + "max_tokens": 147456, "mode": "chat", "output_cost_per_token": 1e-06, "source": "https://openrouter.ai/api/v1/models", @@ -69005,12 +69014,13 @@ "supports_web_search": false }, "openrouter/qwen/qwen3-next-80b-a3b-thinking": { + "deprecation_date": "2026-10-09", "input_cost_per_token": 1.5e-07, "output_cost_per_token": 1.2e-06, "litellm_provider": "openrouter", "max_input_tokens": 262144, - "max_output_tokens": 235929, - "max_tokens": 235929, + "max_output_tokens": 32768, + "max_tokens": 32768, "mode": "chat", "source": "https://openrouter.ai/api/v1/models", "supports_audio_input": false, @@ -69186,13 +69196,13 @@ "supports_web_search": false }, "openrouter/qwen/qwen3-30b-a3b-instruct-2507": { - "input_cost_per_token": 4.815e-08, + "input_cost_per_token": 1e-07, "litellm_provider": "openrouter", "max_input_tokens": 262144, - "max_output_tokens": 32000, - "max_tokens": 32000, + "max_output_tokens": 235929, + "max_tokens": 235929, "mode": "chat", - "output_cost_per_token": 1.9305e-07, + "output_cost_per_token": 3e-07, "source": "https://openrouter.ai/api/v1/models", "supports_audio_input": false, "supports_function_calling": true, @@ -72622,6 +72632,87 @@ "supports_audio_input": true, "supports_video_input": true }, + "bespoke/nimble-latest": { + "input_cost_per_token": 0.0, + "litellm_provider": "bespoke", + "max_input_tokens": 8192, + "mode": "evaluation", + "output_cost_per_token": 0.0, + "source": "https://github.com/bespokelabsai/nimble", + "supported_endpoints": [ + "/v1/systemone" + ], + "metadata": { + "notes": "Self-hosted decision model; infrastructure costs are paid separately" + } + }, + "bespoke/nimble": { + "input_cost_per_token": 0.0, + "litellm_provider": "bespoke", + "max_input_tokens": 8192, + "mode": "evaluation", + "output_cost_per_token": 0.0, + "source": "https://ollama.com/library/nimble", + "supported_endpoints": [ + "/v1/systemone" + ], + "metadata": { + "notes": "Self-hosted decision model under the name Ollama serves it as; infrastructure costs are paid separately" + } + }, + "bespoke/bespokelabs/Bespoke-Nimble-9B": { + "input_cost_per_token": 0.0, + "litellm_provider": "bespoke", + "max_input_tokens": 8192, + "mode": "evaluation", + "output_cost_per_token": 0.0, + "source": "https://github.com/bespokelabsai/nimble", + "supported_endpoints": [ + "/v1/systemone" + ], + "metadata": { + "notes": "Self-hosted decision model; infrastructure costs are paid separately" + } + }, + "laya/english": { + "input_cost_per_token": 0.0, + "litellm_provider": "laya", + "mode": "evaluation", + "output_cost_per_token": 0.0, + "source": "https://github.com/NandhaKishorM/laya", + "supported_endpoints": [ + "/v1/systemone" + ], + "metadata": { + "notes": "Self-hosted decision model; infrastructure costs are paid separately" + } + }, + "laya/multilingual": { + "input_cost_per_token": 0.0, + "litellm_provider": "laya", + "mode": "evaluation", + "output_cost_per_token": 0.0, + "source": "https://github.com/NandhaKishorM/laya", + "supported_endpoints": [ + "/v1/systemone" + ], + "metadata": { + "notes": "Self-hosted decision model; infrastructure costs are paid separately" + } + }, + "laya/typed-decisions": { + "input_cost_per_token": 0.0, + "litellm_provider": "laya", + "mode": "evaluation", + "output_cost_per_token": 0.0, + "source": "https://github.com/NandhaKishorM/laya", + "supported_endpoints": [ + "/v1/systemone" + ], + "metadata": { + "notes": "Self-hosted decision model; infrastructure costs are paid separately" + } + }, "typesafe/jev-1.13.0": { "input_cost_per_token": 4.2e-08, "litellm_provider": "typesafe", @@ -74279,14 +74370,14 @@ "supports_web_search": false }, "openrouter/inclusionai/ling-3.0-flash-fin": { - "cache_read_input_token_cost": 1.2e-08, - "input_cost_per_token": 6e-08, + "cache_read_input_token_cost": 8.4e-09, + "input_cost_per_token": 4.2e-08, "litellm_provider": "openrouter", "max_input_tokens": 262144, - "max_output_tokens": 235929, - "max_tokens": 235929, + "max_output_tokens": 32768, + "max_tokens": 32768, "mode": "chat", - "output_cost_per_token": 1.8e-07, + "output_cost_per_token": 1.232e-07, "source": "https://openrouter.ai/api/v1/models", "supports_audio_input": false, "supports_function_calling": true, @@ -76366,12 +76457,12 @@ "supports_web_search": false }, "openrouter/thinkingmachines/inkling": { - "cache_read_input_token_cost": 1.7e-07, - "input_cost_per_token": 1e-06, + "cache_read_input_token_cost": 1.6e-07, + "input_cost_per_token": 9.5e-07, "litellm_provider": "openrouter", "max_input_tokens": 524288, - "max_output_tokens": 471859, - "max_tokens": 471859, + "max_output_tokens": 262144, + "max_tokens": 262144, "mode": "chat", "output_cost_per_token": 4.05e-06, "source": "https://openrouter.ai/api/v1/models", @@ -79635,5 +79726,24 @@ "supports_response_schema": true, "supports_tool_choice": true, "supports_vision": true + }, + "openrouter/inclusionai/ling-3.1-flash": { + "input_cost_per_token": 0.0, + "litellm_provider": "openrouter", + "max_input_tokens": 262144, + "max_output_tokens": 32768, + "max_tokens": 32768, + "mode": "chat", + "output_cost_per_token": 0.0, + "source": "https://openrouter.ai/api/v1/models", + "supports_audio_input": false, + "supports_function_calling": true, + "supports_pdf_input": false, + "supports_prompt_caching": false, + "supports_reasoning": true, + "supports_response_schema": false, + "supports_tool_choice": true, + "supports_vision": false, + "supports_web_search": false } } diff --git a/provider_endpoints_support.json b/provider_endpoints_support.json index 9cbd326277e..223711a92b6 100644 --- a/provider_endpoints_support.json +++ b/provider_endpoints_support.json @@ -6,6 +6,7 @@ "url": "Link to provider documentation", "endpoints": { "chat_completions": "Supports /chat/completions endpoint", + "systemone": "Supports native System One typed decisions", "messages": "Supports /messages endpoint (Anthropic format)", "responses": "Supports /responses endpoint (OpenAI/Anthropic unified)", "embeddings": "Supports /embeddings endpoint", @@ -1476,6 +1477,20 @@ "rerank": false } }, + "bespoke": { + "display_name": "Bespoke Nimble (`bespoke`)", + "url": "https://docs.litellm.ai/docs/auto_router/decision_classifiers", + "endpoints": { + "systemone": true + } + }, + "laya": { + "display_name": "Laya (`laya`)", + "url": "https://docs.litellm.ai/docs/auto_router/decision_classifiers", + "endpoints": { + "systemone": true + } + }, "lambda_ai": { "display_name": "Lambda AI (`lambda_ai`)", "url": "https://docs.litellm.ai/docs/providers/lambda_ai", @@ -3354,6 +3369,13 @@ "provider_json_field": "skills", "url": "https://docs.litellm.ai/docs/skills" }, + "systemone": { + "docs_label": "systemone", + "display_name": "System One Decision API", + "leftnav_label": "/laya/v1/systemone", + "provider_json_field": "systemone", + "url": "https://docs.litellm.ai/docs/auto_router/decision_classifiers" + }, "text_completion": { "docs_label": "text_completion", "display_name": "OpenAI Completions API", diff --git a/pyproject.toml b/pyproject.toml index ad1750fac0b..9203c2a0d4a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,7 +75,7 @@ proxy = [ "mcp>=2.2.0,<3", "httpx2>=2.5.0,<3", "pydantic>=2.12.0,<3", - "litellm-proxy-extras==0.4.104", + "litellm-proxy-extras==0.4.105", "litellm-enterprise==0.1.73", "RestrictedPython>=8.5,<9.0", "rich>=13.9.4,<14.0", diff --git a/schema.prisma b/schema.prisma index 6f285e9dc39..aba89526cf6 100644 --- a/schema.prisma +++ b/schema.prisma @@ -1744,6 +1744,27 @@ model LiteLLM_AutoRouterUserSession { @@index([user_id, last_turn_at], map: "idx_autorouter_user_session_user_last_turn") } +// Auto-routed requests per UTC request day and router: the selected-day money behind the +// auto-router usage view. Written in the same statement as the session rollup, so a day row +// and its session row never disagree; corrected in the same transaction as late baselines. +model LiteLLM_AutoRouterDailySpend { + date String + api_key String + user_id String + router_name String + router_type String + turns Int @default(0) + spend Float @default(0) + saved_spend Float @default(0) + savings_estimated_turns Int @default(0) + savings_estimated_actual_spend Float @default(0) + savings_estimated_saved_spend Float @default(0) + classifier_cost Float @default(0) + classifier_cost_recorded_turns Int @default(0) + + @@id([date, api_key, user_id, router_name, router_type]) +} + // Shadow eval: evaluation of an auto-router against one or more keys' live traffic, in // either direction. forward duplicates the requests the keys did not route through the // router through it, answering whether they should adopt it; reverse duplicates the diff --git a/tests/code_coverage_tests/unbounded_in_baseline.txt b/tests/code_coverage_tests/unbounded_in_baseline.txt index c42a6b0ddf5..01d8760855f 100644 --- a/tests/code_coverage_tests/unbounded_in_baseline.txt +++ b/tests/code_coverage_tests/unbounded_in_baseline.txt @@ -125,9 +125,8 @@ litellm/proxy/proxy_server.py _fetch_db_models_for_search prisma not.in `list(db litellm/proxy/proxy_server.py _gather_team_accessible_model_ids prisma model_name.in `_resolved_names` 0 litellm/proxy/proxy_server.py get_all_team_models prisma team_id.in `user_teams` 0 litellm/proxy/spend_tracking/ptu_flat_cost_rollup.py _prune_filter prisma model.in `chunk` 0 -litellm/proxy/spend_tracking/spend_management_endpoints.py _find_team_rows prisma team_id.in `team_ids` 0 -litellm/proxy/spend_tracking/spend_management_endpoints.py ui_view_session_spend_logs prisma team_id.in `permitted_team_ids` 0 -litellm/proxy/spend_tracking/spend_management_endpoints.py ui_view_spend_logs prisma team_id.in `permitted_team_ids` 0 +litellm/proxy/auth/authorization_dependencies.py load_permitted_log_team_ids prisma team_id.in `user_obj.teams` 0 +litellm/proxy/spend_tracking/spend_management_endpoints.py _read_scope_where prisma team_id.in `list(scope.team_ids)` 0 litellm/proxy/ui_crud_endpoints/proxy_setting_endpoints.py _validate_default_teams_exist prisma team_id.in `list(team_ids)` 0 litellm/proxy/utils.py PrismaClient.check_view_exists raw-sql viewname.IN `IN ( {expected_views_str} )` 0 litellm/proxy/utils.py PrismaClient.delete_data prisma team_id.in `team_id_list` 0 diff --git a/tests/e2e/conftest.py b/tests/e2e/conftest.py index 62153e38a83..37f0bf00da6 100644 --- a/tests/e2e/conftest.py +++ b/tests/e2e/conftest.py @@ -32,6 +32,7 @@ from e2e_config import ( MCP_OAUTH_LIVE_OPT_IN_ENV, OTEL_TLS_OPT_IN_ENV, OTEL_V2_OPT_IN_ENV, + OWNED_GATEWAY_OPT_IN_ENV, PROMPT_CACHING_OPT_IN_ENV, PROVIDER_EDGE_HOST_OPT_IN_ENV, PROXY_BASE_URL, @@ -70,6 +71,7 @@ OPT_IN_MARKERS: Final = MappingProxyType( "cli_determinism": CLI_DETERMINISM_OPT_IN_ENV, "mcp_oauth_live": MCP_OAUTH_LIVE_OPT_IN_ENV, "provider_edge_host": PROVIDER_EDGE_HOST_OPT_IN_ENV, + "owned_gateway": OWNED_GATEWAY_OPT_IN_ENV, "otel_v2": OTEL_V2_OPT_IN_ENV, "otel_tls": OTEL_TLS_OPT_IN_ENV, "secret_manager": SECRET_MANAGER_OPT_IN_ENV, @@ -172,6 +174,11 @@ def pytest_configure(config: pytest.Config) -> None: "provider_edge_host: routes provider traffic through the pytest host's edge in every fixture mode, so the " "gateway must reach the pytest host; deselected unless E2E_PROVIDER_EDGE_HOST_REACHABLE is set", ) + config.addinivalue_line( + "markers", + "owned_gateway: boots its own proxy from source against the stack's Postgres, so it needs DATABASE_URL " + "on the pytest host; deselected unless E2E_OWNED_GATEWAY is set", + ) config.addinivalue_line( "markers", "otel_v2: needs a proxy running with LITELLM_OTEL_V2=true; deselected unless E2E_OTEL_V2 is set", diff --git a/tests/e2e/coverage_registry/other.yaml b/tests/e2e/coverage_registry/other.yaml index 0b9249d7420..39747607531 100644 --- a/tests/e2e/coverage_registry/other.yaml +++ b/tests/e2e/coverage_registry/other.yaml @@ -60,6 +60,9 @@ - {id: other.auth.jwt.wrong_issuer_denied, module: other, tier: P0, area: auth, assertions: [wrong_issuer_denied], source: "auth/handle_jwt.py", rationale: "A signed token with the correct audience and an unexpected issuer is rejected"} - {id: other.auth.jwt.wrong_audience_denied, module: other, tier: P0, area: auth, assertions: [wrong_audience_denied], source: "auth/handle_jwt.py", rationale: "A signed token from the trusted issuer intended for another app is rejected"} +- {id: other.auth.jwt.auto_register_maps_existing_key, module: other, tier: P0, area: auth, assertions: [maps_existing_key], source: "user_api_key_auth.py _auto_register_jwt_mapping", rationale: "With auto_register_map_existing_key: true, the first JWT call of a user who already owns a key writes the sub-claim mapping to that existing key hash and mints nothing; the spend row lands on the pre-existing key (LIT-5378)", fail_before_fix: proven} +- {id: other.auth.jwt.auto_register_mints_when_keyless, module: other, tier: P0, area: auth, assertions: [mints_when_keyless], source: "user_api_key_auth.py _auto_register_jwt_mapping", rationale: "With auto_register_map_existing_key: true, a user with no keys still gets exactly one minted key and a sub-claim mapping on their first JWT call (LIT-5378)", fail_before_fix: proven} +- {id: other.auth.jwt.auto_register_default_mints, module: other, tier: P0, area: auth, assertions: [default_mints], source: "user_api_key_auth.py _auto_register_jwt_mapping", rationale: "Without auto_register_map_existing_key, auto_register keeps the current behavior: it mints a second key for a user who already has one and bills the minted key (LIT-5378)", fail_before_fix: proven} - {id: other.auth.session_token.valid_allows, module: other, tier: P0, area: auth, assertions: [valid_allows], source: "auth/auth_checks.py ExperimentalUIJWTToken", rationale: "An unexpired LiteLLM-minted session token authenticates with the role it carries"} - {id: other.auth.session_token.expired_denied, module: other, tier: P0, area: auth, assertions: [expired_denied], source: "auth/user_api_key_auth.py expiry check", rationale: "An expired session token is rejected with the expired-key error"} - {id: other.auth.session_token.encrypted_value_denied, module: other, tier: P0, area: auth, assertions: [encrypted_value_denied], source: "auth/auth_checks.py ExperimentalUIJWTToken", rationale: "An encrypted value read back from a management route is not accepted as a bearer token"} diff --git a/tests/e2e/e2e_config.py b/tests/e2e/e2e_config.py index 3fa9f534ffd..e88bfad8388 100644 --- a/tests/e2e/e2e_config.py +++ b/tests/e2e/e2e_config.py @@ -7,6 +7,7 @@ environment so the same tests run against localhost or a deployed proxy. from __future__ import annotations import os +import socket from dataclasses import dataclass import time import uuid @@ -16,6 +17,7 @@ from typing import Final from dotenv import load_dotenv from fixture_mode import deterministic_marker, parse_fixture_mode, registration_owner from provider_edge import provider_edge_api_base +from pydantic import TypeAdapter # Local runs keep provider / DataDog keys in tests/e2e/.env (see CONTRIBUTING.md). # Compose injects them into the proxy container, but pytest on the host does not @@ -206,6 +208,7 @@ REDIS_CHAOS_OPT_IN_ENV = "E2E_REDIS_CHAOS" CLI_DETERMINISM_OPT_IN_ENV = "E2E_CLI_DETERMINISM" MCP_OAUTH_LIVE_OPT_IN_ENV: Final = "E2E_MCP_OAUTH_LIVE" PROVIDER_EDGE_HOST_OPT_IN_ENV: Final = "E2E_PROVIDER_EDGE_HOST_REACHABLE" +OWNED_GATEWAY_OPT_IN_ENV: Final = "E2E_OWNED_GATEWAY" OTEL_V2_OPT_IN_ENV: Final = "E2E_OTEL_V2" OTEL_TLS_OPT_IN_ENV: Final = "E2E_OTEL_EXPORTER_ENDPOINT" SECRET_MANAGER_OPT_IN_ENV: Final = "E2E_SECRET_MANAGER" @@ -296,6 +299,15 @@ def unique_marker() -> str: return uuid.uuid4().hex[:12] +INHERITED_ENV_PREFIXES: Final = ("REDIS_", "MICROSOFT_", "GOOGLE_", "GENERIC_", "PROXY_") + + +def available_port() -> int: + with socket.socket() as listener: + listener.bind(("127.0.0.1", 0)) + return TypeAdapter(tuple[str, int]).validate_python(listener.getsockname())[1] + + def settle_propagation(written_at: float) -> None: """Block until PROPAGATION_TIMEOUT has elapsed since `written_at`, a `time.monotonic()` stamp taken the moment a control-plane write returned. diff --git a/tests/e2e/guardrails/test_presidio_masking_e2e.py b/tests/e2e/guardrails/test_presidio_masking_e2e.py index 1fbc4a68bbd..35eb4884ddf 100644 --- a/tests/e2e/guardrails/test_presidio_masking_e2e.py +++ b/tests/e2e/guardrails/test_presidio_masking_e2e.py @@ -29,15 +29,14 @@ this suite deliberately requires the detected-entity details to remain visible. from __future__ import annotations -import json import os import re import time -from collections.abc import Callable +from collections.abc import Callable, Iterator from typing import Final, Literal import pytest -from pydantic import BaseModel, TypeAdapter +from pydantic import BaseModel, JsonValue, TypeAdapter from e2e_config import unique_marker from e2e_http import Result, StreamingResponse, Success @@ -74,6 +73,7 @@ FAKE_PHONE = "+1 415-555-0134" FAKE_VISA_TEST_CARD = "4111 1111 1111 1111" _CARD_DIGIT_RUN: Final = re.compile(r"(?:\d[ -]?){13,19}") +_CONTENT_KEYS: Final = frozenset({"content", "text"}) def _presidio_bases() -> tuple[str, str]: @@ -516,7 +516,21 @@ def _spend_log_response_text(client: GuardrailsClient, key: str, call_id: str) - ) row = next((row for row in rows if row.litellm_call_id == call_id), None) assert row is not None, f"no spend log row ever appeared for x-litellm-call-id {call_id}" - return json.dumps(row.response) + return "\n".join(_stored_content(row.response)) + + +def _stored_content(node: JsonValue, key: str | None = None) -> Iterator[str]: + match node: + case str() if key in _CONTENT_KEYS: + yield node + case dict(): + for child_key, child in node.items(): + yield from _stored_content(child, child_key) + case list(): + for item in node: + yield from _stored_content(item, key) + case _: + return class TestPresidioSpendLogStoresMaskedOutput: diff --git a/tests/e2e/llm_translation/test_chat_completions_regression_e2e.py b/tests/e2e/llm_translation/test_chat_completions_regression_e2e.py index b6b0ef3ebbe..90ac18c16ac 100644 --- a/tests/e2e/llm_translation/test_chat_completions_regression_e2e.py +++ b/tests/e2e/llm_translation/test_chat_completions_regression_e2e.py @@ -53,7 +53,18 @@ OPENAI_BACKEND = "openai/gpt-5.6" ANTHROPIC_BACKEND = "anthropic/claude-haiku-4-5-20251001" BEDROCK_CONVERSE_BACKEND = "bedrock/us.anthropic.claude-haiku-4-5-20251001-v1:0" BEDROCK_NOVA_BACKEND: Final = "bedrock/us.amazon.nova-2-lite-v1:0" -VERTEX_PARTNER_BACKENDS: Final = ("vertex_ai/mistral-small-2503", "vertex_ai/openai/gpt-oss-120b-maas") +VERTEX_PARTNER_BACKENDS: Final = ( + pytest.param( + "vertex_ai/mistral-small-2503", + marks=pytest.mark.skip( + reason="the e2e Vertex project has no access to mistral-small-2503 (404 publisher model not found)" + ), + ), + pytest.param( + "vertex_ai/openai/gpt-oss-120b-maas", + marks=pytest.mark.skip(reason="never served by the e2e Vertex project (60s read timeout, no headers)"), + ), +) PDF_DOCUMENT_URL: Final = ( "https://cdn.jsdelivr.net/gh/BerriAI/litellm" "@d769e81c90d453240c61fc572cdb27fae06a89d0" diff --git a/tests/e2e/llm_translation/test_chat_tool_round_trip_e2e.py b/tests/e2e/llm_translation/test_chat_tool_round_trip_e2e.py index c6e9f813099..75ba5c23ff8 100644 --- a/tests/e2e/llm_translation/test_chat_tool_round_trip_e2e.py +++ b/tests/e2e/llm_translation/test_chat_tool_round_trip_e2e.py @@ -28,6 +28,7 @@ GEMINI_BACKEND: Final = "gemini/gemini-3.5-flash-lite" MISTRAL_BACKEND: Final = "mistral/mistral-medium-3.5" ANTHROPIC_BACKEND: Final = "anthropic/claude-haiku-4-5" BEDROCK_CONVERSE_BACKEND: Final = "bedrock/converse/us.anthropic.claude-sonnet-5-5" +BEDROCK_LEGACY_THINKING_BACKEND: Final = "bedrock/converse/us.anthropic.claude-sonnet-4-6" PROMPT: Final = "What is the weather in Paris and in Tokyo? Use the get_weather tool for each city." CITY_TEMPERATURES: Final = MappingProxyType({"paris": "22", "tokyo": "31"}) @@ -54,9 +55,9 @@ def _api_key_params(backend: str, env: str) -> LiteLLMParamsBody: return LiteLLMParamsBody(model=backend, api_key=f"os.environ/{env}") -def _bedrock_params() -> LiteLLMParamsBody: +def _bedrock_params(backend: str) -> LiteLLMParamsBody: return LiteLLMParamsBody( - model=BEDROCK_CONVERSE_BACKEND, + model=backend, aws_access_key_id="os.environ/AWS_ACCESS_KEY_ID", aws_secret_access_key="os.environ/AWS_SECRET_ACCESS_KEY", aws_region_name="os.environ/AWS_REGION", @@ -146,7 +147,7 @@ class TestChatToolResultRoundTrip: _assert_tool_results_reach_the_model(client, key, model, thinking=None, tool_choice="required") def test_bedrock_converse(self, client: PassthroughClient, resources: ResourceManager) -> None: - model, key = _register(client, resources, _bedrock_params()) + model, key = _register(client, resources, _bedrock_params(BEDROCK_CONVERSE_BACKEND)) _assert_tool_results_reach_the_model(client, key, model, thinking=None, tool_choice="required") def test_anthropic_with_extended_thinking(self, client: PassthroughClient, resources: ResourceManager) -> None: @@ -156,5 +157,5 @@ class TestChatToolResultRoundTrip: def test_bedrock_converse_with_extended_thinking( self, client: PassthroughClient, resources: ResourceManager ) -> None: - model, key = _register(client, resources, _bedrock_params()) + model, key = _register(client, resources, _bedrock_params(BEDROCK_LEGACY_THINKING_BACKEND)) _assert_tool_results_reach_the_model(client, key, model, thinking=THINKING, tool_choice=None) diff --git a/tests/e2e/llm_translation/test_embeddings_endpoint_e2e.py b/tests/e2e/llm_translation/test_embeddings_endpoint_e2e.py index 42fe6537d4f..0e5bac556cc 100644 --- a/tests/e2e/llm_translation/test_embeddings_endpoint_e2e.py +++ b/tests/e2e/llm_translation/test_embeddings_endpoint_e2e.py @@ -10,6 +10,7 @@ SDK refuses to build stay on the shared transport. from __future__ import annotations +import math from typing import Final import pytest @@ -25,6 +26,10 @@ pytestmark = pytest.mark.e2e VERTEX_TEXT_EMBEDDING: Final = "vertex_ai/text-embedding-005" VERTEX_MULTIMODAL_EMBEDDING: Final = "vertex_ai/multimodalembedding@001" +TOKENS_TEXT: Final = "The quick brown fox jumps over the lazy dog" +# tiktoken 0.12.0 cl100k_base encoding of TOKENS_TEXT (checked 2026-10-02), the vocabulary behind the proxy's +# litellm.decode(model="gpt-3.5-turbo") token-array decode +TOKENS: Final = (791, 4062, 14198, 39935, 35308, 927, 279, 16053, 5679) class _OptionalEmbeddingsBody(BaseModel): @@ -32,9 +37,19 @@ class _OptionalEmbeddingsBody(BaseModel): input: str | list[str] | None = None -class _TokenEmbeddingsBody(BaseModel): - model: str - input: list[list[int]] +def _cosine(left: list[float], right: list[float]) -> float: + dot: Final = sum(a * b for a, b in zip(left, right, strict=True)) + norms: Final = math.sqrt(sum(a * a for a in left)) * math.sqrt(sum(b * b for b in right)) + return dot / norms + + +def _titan_params() -> LiteLLMParamsBody: + return LiteLLMParamsBody( + model="bedrock/amazon.titan-embed-text-v2:0", + aws_access_key_id="os.environ/AWS_ACCESS_KEY_ID", + aws_secret_access_key="os.environ/AWS_SECRET_ACCESS_KEY", + aws_region_name="os.environ/AWS_REGION", + ) def _vertex_params(model: str) -> LiteLLMParamsBody: @@ -94,12 +109,7 @@ class TestEmbeddingsEndpoint: resources, sdk, "e2e-embeddings-bedrock", - LiteLLMParamsBody( - model="bedrock/amazon.titan-embed-text-v2:0", - aws_access_key_id="os.environ/AWS_ACCESS_KEY_ID", - aws_secret_access_key="os.environ/AWS_SECRET_ACCESS_KEY", - aws_region_name="os.environ/AWS_REGION", - ), + _titan_params(), ) @pytest.mark.covers("llm.embeddings.cohere.basic.nonstream.works") @@ -171,28 +181,20 @@ class TestEmbeddingsEndpoint: cost = response_header(raw.headers, "x-litellm-response-cost") assert cost is not None and float(cost) > 0, f"multimodal embedding was not costed: {cost!r}" - def test_bedrock_titan_rejects_token_array_input_as_bad_request( - self, proxy: ProxyClient, resources: ResourceManager + def test_bedrock_titan_embeds_token_array_input_as_its_decoded_text( + self, proxy: ProxyClient, resources: ResourceManager, sdk: SdkClients ) -> None: - model, key = _register( - proxy, - resources, - "e2e-embeddings-titan-tokens", - LiteLLMParamsBody( - model="bedrock/amazon.titan-embed-text-v2:0", - aws_access_key_id="os.environ/AWS_ACCESS_KEY_ID", - aws_secret_access_key="os.environ/AWS_SECRET_ACCESS_KEY", - aws_region_name="os.environ/AWS_REGION", - ), - ) - result = proxy.transport.send( - "/embeddings", - headers=proxy.transport.bearer(key), - json=_TokenEmbeddingsBody(model=model, input=[[1]]), - ) - assert result.status_code == 400, ( - f"titan cannot embed token arrays, so the caller must get a 400, got {result.status_code}: " - f"{result.body[:300]}" + model, key = _register(proxy, resources, "e2e-embeddings-titan-tokens", _titan_params()) + client: Final = sdk.openai(key) + + from_tokens: Final = client.embeddings.create(model=model, input=[TOKENS], extra_body=NO_PROXY_CACHE) + from_text: Final = client.embeddings.create(model=model, input=TOKENS_TEXT, extra_body=NO_PROXY_CACHE) + + assert len(from_tokens.data) == 1, f"one token array must yield one vector: {from_tokens!r}" + similarity: Final = _cosine(from_tokens.data[0].embedding, from_text.data[0].embedding) + assert similarity > 0.99, ( + f"titan cannot embed token ids, so the proxy must decode them to {TOKENS_TEXT!r} first; " + f"the token-array vector only has cosine {similarity:.4f} with that text's vector" ) @pytest.mark.replayable diff --git a/tests/e2e/mcp/oauth_gateway.py b/tests/e2e/mcp/oauth_gateway.py index b328c81687b..029b0135900 100644 --- a/tests/e2e/mcp/oauth_gateway.py +++ b/tests/e2e/mcp/oauth_gateway.py @@ -8,7 +8,6 @@ The optional live edge measures headers without recording credentials or bodies. from __future__ import annotations import os -import socket import subprocess import sys import threading @@ -20,13 +19,12 @@ from pathlib import Path from typing import Final import psycopg +from e2e_config import INHERITED_ENV_PREFIXES, available_port from e2e_http import NoBody from idp import Keycloak, stop_process_group from proxy_client import ProxyClient, build_proxy_client from psycopg.rows import class_row -from pydantic import BaseModel, SecretStr, TypeAdapter, ValidationError - -INHERITED_ENV_PREFIXES: Final = ("REDIS_", "MICROSOFT_", "GOOGLE_", "GENERIC_", "PROXY_") +from pydantic import BaseModel, SecretStr, ValidationError class StoredOAuth(BaseModel): @@ -101,12 +99,6 @@ class OAuthObservation: assert all(not item[2] for item in snapshot), "gateway bearer leaked to the upstream" -def available_port() -> int: - with socket.socket() as listener: - listener.bind(("127.0.0.1", 0)) - return TypeAdapter(tuple[str, int]).validate_python(listener.getsockname())[1] - - @dataclass(slots=True) class OAuthGateway: base_url: str diff --git a/tests/e2e/migrations/conftest.py b/tests/e2e/migrations/conftest.py index b7604a4fdda..ac3618a4542 100644 --- a/tests/e2e/migrations/conftest.py +++ b/tests/e2e/migrations/conftest.py @@ -8,7 +8,7 @@ from urllib.parse import urlsplit import pytest from _pytest.fixtures import SubRequest -from .containers import Containers, docker, ready +from .containers import Containers, docker, ready, seeded from .database import Database, Databases @@ -44,7 +44,7 @@ def migrated_template( output: Final = Path(os.environ.get("MIGRATION_TEST_OUTPUT", str(tmp_path_factory.getbasetemp()))) / "seed" with databases.create() as database: with Containers(migration_image, output).start(database) as replica: - ready((replica,), database) + seeded(replica, database) yield database diff --git a/tests/e2e/migrations/containers.py b/tests/e2e/migrations/containers.py index dd126b994d3..30374dedcf3 100644 --- a/tests/e2e/migrations/containers.py +++ b/tests/e2e/migrations/containers.py @@ -97,6 +97,19 @@ def ready(replicas: tuple[Replica, ...], database: Database) -> None: replica.usable(database) +def seeded(seed: Replica, database: Database) -> None: + ready((seed,), database) + until("the seed replica to finish its request-log indexes", lambda: request_log_indexes_built(database)) + + +def request_log_indexes_built(database: Database) -> bool: + return database.query( + "SELECT count(*) FROM pg_index x JOIN pg_class i ON i.oid = x.indexrelid " + "JOIN pg_namespace n ON n.oid = i.relnamespace WHERE n.nspname = current_schema() AND x.indisvalid " + "AND i.relname IN ('LiteLLM_SpendLogs_api_key_startTime_idx', 'LiteLLM_SpendLogs_litellm_call_id_idx')" + ) == ((2,),) + + def failed(replicas: tuple[Replica, ...], marker: str) -> None: def all_stopped() -> bool: observations: Final = tuple(replica.observe() for replica in replicas) diff --git a/tests/e2e/migrations/test_legacy.py b/tests/e2e/migrations/test_legacy.py index ba5e77a3070..4cad808d3ff 100644 --- a/tests/e2e/migrations/test_legacy.py +++ b/tests/e2e/migrations/test_legacy.py @@ -5,7 +5,7 @@ from typing import Final, Literal import pytest from .checks import COMPLETE, assert_completed, confirmed_history, assert_original_proof, start_replicas -from .containers import Containers, failed, ready +from .containers import Containers, failed, ready, seeded from .database import Database, Databases pytestmark: Final = [pytest.mark.e2e, pytest.mark.migration_startup] @@ -69,7 +69,7 @@ class TestLegacyMigrations: ) -> None: with databases.create(schema="migration tenant") as database: with containers.start(database) as seed: - ready((seed,), database) + seeded(seed, database) match scenario: case "upgrade": with ExitStack() as stack: diff --git a/tests/e2e/models.py b/tests/e2e/models.py index 83d8a9884a3..e027c410e44 100644 --- a/tests/e2e/models.py +++ b/tests/e2e/models.py @@ -1537,6 +1537,7 @@ class UserNewBody(BaseModel): class UserNewResponse(BaseModel): user_id: str + key: str | None = None class UserUpdateBody(BaseModel): @@ -1580,6 +1581,40 @@ class UserListResponse(BaseModel): total: int +class UserKeyRow(BaseModel): + token: str + key_alias: str | None = None + + +class UserInfoWithKeysResponse(BaseModel): + user_id: str | None = None + keys: list[UserKeyRow] = [] + + +class JwtKeyMappingRow(BaseModel): + id: str + jwt_claim_name: str + jwt_claim_value: str + created_by: str | None = None + + +class JwtKeyMappingListParams(BaseModel): + size: int = 100 + + +class JwtKeyMappingListResponse(BaseModel): + mappings: list[JwtKeyMappingRow] + total_count: int + + +class JwtKeyMappingDeleteBody(BaseModel): + id: str + + +class JwtKeyMappingDeleteResponse(BaseModel): + status: str + + class OrgNewBody(BaseModel): organization_alias: str models: list[str] = [] diff --git a/tests/e2e/other/other_client.py b/tests/e2e/other/other_client.py index 93c198586f6..d7bad4f1ed1 100644 --- a/tests/e2e/other/other_client.py +++ b/tests/e2e/other/other_client.py @@ -21,12 +21,20 @@ from idp import Keycloak, keycloak_from_env from models import ( ChatBody, ChatResponse, + JwtKeyMappingDeleteBody, + JwtKeyMappingDeleteResponse, + JwtKeyMappingListParams, + JwtKeyMappingListResponse, ModelsListParams, ModelsListResponse, ReadinessDetailsResponse, ReadinessResponse, + UserInfoParams, + UserInfoWithKeysResponse, UserListParams, UserListResponse, + UserNewBody, + UserNewResponse, ) from proxy_client import ProxyClient from pydantic import Field @@ -79,6 +87,44 @@ class OtherClient: response_type=ReadinessDetailsResponse, ) + def user_new(self, body: UserNewBody) -> Result[UserNewResponse]: + """POST /user/new under the master key: seed the litellm user a JWT + `sub` claim resolves to, before that token ever reaches the proxy.""" + return self.proxy.transport.post( + "/user/new", + headers=self.proxy.transport.master, + json=body, + response_type=UserNewResponse, + ) + + def user_info(self, user_id: str) -> Result[UserInfoWithKeysResponse]: + """GET /user/info under the master key. Only the user's key rows are + modelled: `token` is the stored key hash, never the plaintext key.""" + return self.proxy.transport.get( + "/user/info", + headers=self.proxy.transport.master, + params=UserInfoParams(user_id=user_id), + response_type=UserInfoWithKeysResponse, + ) + + def jwt_mapping_list(self) -> Result[JwtKeyMappingListResponse]: + """GET /jwt/key/mapping/list under the master key.""" + return self.proxy.transport.get( + "/jwt/key/mapping/list", + headers=self.proxy.transport.master, + params=JwtKeyMappingListParams(size=100), + response_type=JwtKeyMappingListResponse, + ) + + def jwt_mapping_delete(self, mapping_id: str) -> Result[JwtKeyMappingDeleteResponse]: + """POST /jwt/key/mapping/delete under the master key.""" + return self.proxy.transport.post( + "/jwt/key/mapping/delete", + headers=self.proxy.transport.master, + json=JwtKeyMappingDeleteBody(id=mapping_id), + response_type=JwtKeyMappingDeleteResponse, + ) + def chat_as_team(self, token: str, team: str, body: ChatBody) -> Result[ChatResponse]: """POST /chat/completions under `token` with `x-litellm-team-id: team`.""" return self.proxy.transport.post( diff --git a/tests/e2e/other/owned_jwt_gateway.py b/tests/e2e/other/owned_jwt_gateway.py new file mode 100644 index 00000000000..1af348cac60 --- /dev/null +++ b/tests/e2e/other/owned_jwt_gateway.py @@ -0,0 +1,108 @@ +"""An owned, source-built proxy whose `litellm_jwtauth` block a test controls. + +The shared proxy on :4000 runs the CONTRIBUTING.md JWT block, so a test that +needs a different `litellm_jwtauth` config boots its own gateway on a free port +against the same database and the same Keycloak realm. The caller supplies the +`litellm_jwtauth` mapping verbatim, which is exactly what makes a config an +unfixed proxy rejects observable as a boot failure in this gateway's own log. +""" + +from __future__ import annotations + +import os +import subprocess +import sys +import time +from collections.abc import Mapping +from contextlib import ExitStack +from dataclasses import dataclass, field +from pathlib import Path +from typing import Final + +from e2e_config import INHERITED_ENV_PREFIXES, available_port +from e2e_http import NoBody +from idp import Keycloak, stop_process_group +from proxy_client import ProxyClient, build_proxy_client + +MODEL_NAME: Final = "gemini-3.8-flash" + + +@dataclass(slots=True) +class OwnedJwtGateway: + base_url: str + proxy: ProxyClient + _environment: Mapping[str, str] = field(repr=False) + _command: tuple[str, ...] = field(repr=False) + _log_path: Path + _child: subprocess.Popen[bytes] | None = field(default=None, init=False, repr=False) + + def start(self) -> None: + with self._log_path.open("ab") as log: + self._child = subprocess.Popen( + self._command, + env=self._environment, + stdout=log, + stderr=log, + start_new_session=True, + ) + deadline: Final = time.monotonic() + 120 + while time.monotonic() < deadline: + assert self._child.poll() is None, "owned JWT gateway exited; inspect its private log" + result = self.proxy.transport.probe("/health/liveliness", params=NoBody()) + if result.status_code == 200: + return + time.sleep(0.5) + raise AssertionError("owned JWT gateway did not become ready") + + def stop(self) -> None: + if self._child is not None: + stop_process_group(self._child) + assert self._child.poll() is not None, "old gateway process is still alive" + + +def owned_jwt_gateway( + idp: Keycloak, directory: Path, cleanup: ExitStack, *, litellm_jwtauth: str, name: str +) -> OwnedJwtGateway: + for env_name in ("DATABASE_URL", "LITELLM_LICENSE", "LITELLM_MASTER_KEY"): + assert os.environ.get(env_name), f"{env_name} is required for the owned JWT gateway" + port: Final = available_port() + base_url: Final = f"http://127.0.0.1:{port}" + config: Final = directory / f"{name}.yaml" + config.write_text( + "model_list:\n" + f" - model_name: {MODEL_NAME}\n" + " litellm_params:\n" + f" model: gemini/{MODEL_NAME}\n" + " api_key: os.environ/GEMINI_API_KEY\n" + "general_settings:\n" + " master_key: os.environ/LITELLM_MASTER_KEY\n" + " database_url: os.environ/DATABASE_URL\n" + " proxy_batch_write_at: 5\n" + " enable_jwt_auth: true\n" + " litellm_jwtauth:\n" + "".join(f" {line}\n" for line in litellm_jwtauth.strip().splitlines()) + ) + environment: Final = { + **{key: value for key, value in os.environ.items() if not key.startswith(INHERITED_ENV_PREFIXES)}, + "JWT_PUBLIC_KEY_URL": idp.jwks_url, + "JWT_ISSUER": idp.issuer, + "JWT_AUDIENCE": "litellm-e2e", + "LITELLM_DANGEROUSLY_PERMIT_WEAK_OR_UNSET_MASTER_KEY": "true", + "DISABLE_SCHEMA_UPDATE": "true", + "STORE_MODEL_IN_DB": "True", + "PYTHONPATH": str(Path(__file__).resolve().parents[3]), + } + gateway: Final = OwnedJwtGateway( + base_url=base_url, + proxy=build_proxy_client( + base_url=base_url, + control_plane_base_url=base_url, + replica_urls=(base_url,), + master_key=os.environ["LITELLM_MASTER_KEY"], + ), + _environment=environment, + _command=(sys.executable, "-m", "litellm.proxy.proxy_cli", "--config", str(config), "--port", str(port)), + _log_path=directory / f"{name}.log", + ) + cleanup.callback(gateway.stop) + gateway.start() + return gateway diff --git a/tests/e2e/other/test_jwt_auto_register_e2e.py b/tests/e2e/other/test_jwt_auto_register_e2e.py new file mode 100644 index 00000000000..8f7c2c6a693 --- /dev/null +++ b/tests/e2e/other/test_jwt_auto_register_e2e.py @@ -0,0 +1,182 @@ +"""auto_register with auto_register_map_existing_key binds the JWT claim to the user's existing key. + +`unregistered_jwt_client_behavior: auto_register` on `virtual_key_claim_field: sub` mints a fresh +virtual key on the user's first JWT call. With `auto_register_map_existing_key: true` the proxy must +instead point the new JWT mapping at a key the resolved user already owns, and mint only when the +user has none. Each behavior gets its own gateway because the flag lives in `litellm_jwtauth`, so +this file boots two owned proxies against the shared database and Keycloak realm. +""" + +from __future__ import annotations + +import hashlib +from collections.abc import Iterator +from contextlib import ExitStack +from typing import Final + +import pytest +from e2e_config import unique_marker +from e2e_http import unwrap +from idp import Identity, Keycloak +from lifecycle import ResourceManager +from models import ChatBody, ChatMessage, JwtKeyMappingRow, KeyGenerateBody, TeamNewBody, UserNewBody +from other_client import OtherClient +from owned_jwt_gateway import MODEL_NAME, OwnedJwtGateway, owned_jwt_gateway + +pytestmark = pytest.mark.e2e + +_JWT_COMMON: Final = ( + "user_id_jwt_field: sub\n" + "user_email_jwt_field: email\n" + "team_ids_jwt_field: groups\n" + "user_id_upsert: true\n" + "virtual_key_claim_field: sub\n" + "unregistered_jwt_client_behavior: auto_register" +) + + +def _key_hash(key: str) -> str: + return hashlib.sha256(key.encode()).hexdigest() + + +def _ping() -> ChatBody: + return ChatBody( + model=MODEL_NAME, + messages=[ChatMessage(role="user", content=f"Reply with the single word ok. {unique_marker()}")], + max_tokens=5, + ) + + +def _identity_with_user(idp: Keycloak, client: OtherClient, resources: ResourceManager) -> Identity: + """An IdP identity plus the litellm user and team its claims resolve to, with + teardown that also sweeps the user's keys and JWT mapping rows the proxy + wrote, since those outlive the user row itself.""" + marker: Final = unique_marker() + identity: Final = idp.provision(marker=marker, group=f"e2e-jwt-team-{marker}", defer=resources.defer) + resources.defer(lambda: client.proxy.delete_user(identity.user_id)) + team_id: Final = client.proxy.create_team(TeamNewBody(team_alias=f"e2e-jwt-{marker}", team_id=identity.group)) + resources.defer(lambda: client.proxy.delete_team(team_id)) + unwrap( + client.user_new( + UserNewBody( + user_id=identity.user_id, + user_email=f"{identity.username}@example.com", + user_role="internal_user", + auto_create_key=False, + ) + ) + ) + + def delete_user_keys() -> None: + for row in unwrap(client.user_info(identity.user_id)).keys: + client.proxy.delete_key(row.token) + + def delete_user_mappings() -> None: + for mapping in unwrap(client.jwt_mapping_list()).mappings: + if mapping.jwt_claim_value == identity.user_id: + _ = client.jwt_mapping_delete(mapping.id) + + resources.defer(delete_user_keys) + resources.defer(delete_user_mappings) + return identity + + +def _mapping_for(client: OtherClient, claim_value: str) -> JwtKeyMappingRow | None: + return next( + (row for row in unwrap(client.jwt_mapping_list()).mappings if row.jwt_claim_value == claim_value), + None, + ) + + +@pytest.fixture(scope="module") +def mapping_gateway(idp: Keycloak, tmp_path_factory: pytest.TempPathFactory) -> Iterator[OwnedJwtGateway]: + with ExitStack() as cleanup: + yield owned_jwt_gateway( + idp, + tmp_path_factory.mktemp("jwt-mapping"), + cleanup, + litellm_jwtauth=f"{_JWT_COMMON}\nauto_register_map_existing_key: true", + name="jwt-mapping-gateway", + ) + + +@pytest.fixture(scope="module") +def minting_gateway(idp: Keycloak, tmp_path_factory: pytest.TempPathFactory) -> Iterator[OwnedJwtGateway]: + with ExitStack() as cleanup: + yield owned_jwt_gateway( + idp, + tmp_path_factory.mktemp("jwt-minting"), + cleanup, + litellm_jwtauth=_JWT_COMMON, + name="jwt-minting-gateway", + ) + + +@pytest.mark.owned_gateway +class TestJwtAutoRegisterMapExistingKey: + @pytest.mark.covers("other.auth.jwt.auto_register_maps_existing_key") + def test_first_jwt_call_maps_to_the_users_existing_key_and_mints_none( + self, client: OtherClient, idp: Keycloak, resources: ResourceManager, mapping_gateway: OwnedJwtGateway + ) -> None: + identity: Final = _identity_with_user(idp, client, resources) + existing_key: Final = client.proxy.generate_key( + KeyGenerateBody( + user_id=identity.user_id, team_id=identity.group, key_alias=f"e2e-jwt-existing-{unique_marker()}" + ) + ) + resources.defer(lambda: client.proxy.delete_key(existing_key)) + + response: Final = unwrap(mapping_gateway.proxy.chat(idp.access_token(identity), _ping())) + + keys: Final = unwrap(client.user_info(identity.user_id)).keys + assert [row.token for row in keys] == [_key_hash(existing_key)], ( + f"map_existing_key must leave the user with only their pre-existing key, got {keys}" + ) + mapping: Final = _mapping_for(client, identity.user_id) + assert mapping is not None, ( + f"no JWT mapping row for sub={identity.user_id}: {unwrap(client.jwt_mapping_list())}" + ) + assert mapping.jwt_claim_name == "sub", f"mapping must bind the sub claim, got {mapping}" + assert mapping.created_by == "auto_register", f"mapping must be written by auto_register, got {mapping}" + rows: Final = client.proxy.poll_logs_for_key(existing_key) + assert any(row.request_id == response.id for row in rows), ( + f"the JWT chat must be billed to the user's existing key, spend rows for it: {rows}" + ) + + @pytest.mark.covers("other.auth.jwt.auto_register_mints_when_keyless") + def test_first_jwt_call_mints_a_key_when_the_user_has_none( + self, client: OtherClient, idp: Keycloak, resources: ResourceManager, mapping_gateway: OwnedJwtGateway + ) -> None: + identity: Final = _identity_with_user(idp, client, resources) + + response: Final = unwrap(mapping_gateway.proxy.chat(idp.access_token(identity), _ping())) + assert response.choices, f"JWT chat returned no completion: {response}" + + keys: Final = unwrap(client.user_info(identity.user_id)).keys + assert len(keys) == 1, f"a keyless user must get exactly one minted key, got {keys}" + mapping: Final = _mapping_for(client, identity.user_id) + assert mapping is not None and mapping.jwt_claim_name == "sub", ( + f"the minted key must be recorded as a sub-claim mapping, mappings: {unwrap(client.jwt_mapping_list())}" + ) + + @pytest.mark.covers("other.auth.jwt.auto_register_default_mints") + def test_default_behavior_still_mints_when_the_user_already_has_a_key( + self, client: OtherClient, idp: Keycloak, resources: ResourceManager, minting_gateway: OwnedJwtGateway + ) -> None: + identity: Final = _identity_with_user(idp, client, resources) + existing_key: Final = client.proxy.generate_key( + KeyGenerateBody(user_id=identity.user_id, key_alias=f"e2e-jwt-existing-{unique_marker()}") + ) + resources.defer(lambda: client.proxy.delete_key(existing_key)) + + response: Final = unwrap(minting_gateway.proxy.chat(idp.access_token(identity), _ping())) + assert response.id is not None, f"JWT chat returned no response id: {response}" + + keys: Final = unwrap(client.user_info(identity.user_id)).keys + assert len(keys) == 2, ( + f"default auto_register must mint a second key for a user who already has one, got {keys}" + ) + rows: Final = client.proxy.poll_logs_for_request_id(response.id) + assert rows and all(row.api_key != _key_hash(existing_key) for row in rows), ( + f"the default path must bill the minted key, not the user's existing one: {rows}" + ) diff --git a/tests/e2e/pytest.ini b/tests/e2e/pytest.ini index e795ebe5721..dbd3ff47daa 100644 --- a/tests/e2e/pytest.ini +++ b/tests/e2e/pytest.ini @@ -16,6 +16,7 @@ markers = quiet_stack: measures the proxy itself, so it runs while no other test on this host is hitting the stack; every other test waits for it to finish mcp_oauth_live: real Linear OAuth consent via a captured browser session; deselected unless E2E_MCP_OAUTH_LIVE is set provider_edge_host: routes provider traffic through the pytest host's edge in every fixture mode, so the gateway must reach the pytest host; deselected unless E2E_PROVIDER_EDGE_HOST_REACHABLE is set + owned_gateway: boots its own proxy from source against the stack's Postgres, so it needs DATABASE_URL on the pytest host; deselected unless E2E_OWNED_GATEWAY is set otel_v2: needs a proxy running with LITELLM_OTEL_V2=true; deselected unless E2E_OTEL_V2 is set otel_tls: needs a stack whose gateway exports OTLP over TLS signed by the CA in SSL_CERT_FILE; deselected unless E2E_OTEL_EXPORTER_ENDPOINT is set secret_manager: needs a proxy booted from gateway/secret_manager__ci_config.yml against that live secret manager; deselected unless E2E_SECRET_MANAGER names the backend (see secret_manager/secret_backends.py) diff --git a/tests/e2e/ui/helpers/userOnboarding.ts b/tests/e2e/ui/helpers/userOnboarding.ts index 14e2b0257b2..9db3bb0867b 100644 --- a/tests/e2e/ui/helpers/userOnboarding.ts +++ b/tests/e2e/ui/helpers/userOnboarding.ts @@ -47,6 +47,7 @@ export async function expectUnrestrictedDashboard(page: Page): Promise { const session = await readDashboardSession(page); expect(session.password_reset_required === true, "login must not require a password reset").toBe(false); await virtualKeys.click(); + await expect(page).toHaveURL(/\/ui\/api-keys\/?$/); await expect(page.getByRole("main").getByRole("heading", { name: "Virtual Keys", exact: true })).toBeVisible({ timeout: 30_000, }); diff --git a/tests/e2e/ui/tests/integrationCritical/expected.json b/tests/e2e/ui/tests/integrationCritical/expected.json index c6ee6051cd4..81d3be247fd 100644 --- a/tests/e2e/ui/tests/integrationCritical/expected.json +++ b/tests/e2e/ui/tests/integrationCritical/expected.json @@ -9,5 +9,6 @@ "tests/e2e/ui/tests/integrationCritical/mcpUserEnvVars.spec.ts::clearing credentials for a server deleted underneath the modal reports the failure without losing the page", "tests/e2e/ui/tests/integrationCritical/costOptimizationModelGroups.spec.ts::cache leakage by model merges a deployment's resolved and requested model names into its model group", "tests/e2e/ui/tests/integrationCritical/logsDrawerCredentialCanary.spec.ts::the Logs drawer renders the stored request without the deployment api_key", - "tests/e2e/ui/tests/integrationCritical/toolPoliciesUserColumn.spec.ts::the Tool Policies page names the user behind the key that discovered a tool" + "tests/e2e/ui/tests/integrationCritical/toolPoliciesUserColumn.spec.ts::the Tool Policies page names the user behind the key that discovered a tool", + "tests/e2e/ui/tests/integrationCritical/teamMetadataEmptyKey.spec.ts::the team settings form skips a metadata row with an empty key and saving drops the key" ] diff --git a/tests/e2e/ui/tests/integrationCritical/teamMetadataEmptyKey.spec.ts b/tests/e2e/ui/tests/integrationCritical/teamMetadataEmptyKey.spec.ts new file mode 100644 index 00000000000..d24ba963f05 --- /dev/null +++ b/tests/e2e/ui/tests/integrationCritical/teamMetadataEmptyKey.spec.ts @@ -0,0 +1,90 @@ +import { + test, + expect, + APIRequestContext, + Page as PlaywrightPage, +} from "@playwright/test"; +import { randomUUID } from "node:crypto"; + +const master = process.env.LITELLM_MASTER_KEY ?? "sk-integration-master"; +const headers = { Authorization: `Bearer ${master}` }; + +async function createTeamCarryingAnEmptyMetadataKey( + request: APIRequestContext, +): Promise { + const created = await request.post("/team/new", { + headers, + data: { + team_alias: `int_empty_key_${randomUUID().replace(/-/g, "").slice(0, 12)}`, + }, + }); + expect(created.ok(), await created.text()).toBe(true); + const teamId = (await created.json()).team_id as string; + const seeded = await request.post("/team/update", { + headers, + data: { + team_id: teamId, + metadata: { "": { displayName: "stale" }, env: "staging" }, + }, + }); + expect(seeded.ok(), await seeded.text()).toBe(true); + return teamId; +} + +async function teamMetadata( + request: APIRequestContext, + teamId: string, +): Promise> { + const response = await request.get(`/team/info?team_id=${teamId}`, { + headers, + }); + expect(response.ok(), await response.text()).toBe(true); + const json = await response.json(); + return (json.team_info?.metadata ?? {}) as Record; +} + +async function loginAsAdmin(page: PlaywrightPage): Promise { + await page.goto("/ui/login"); + await page.getByPlaceholder("Enter your username").fill("admin"); + await page.getByPlaceholder("Enter your password").fill(master); + await page.getByRole("button", { name: "Login", exact: true }).click(); + await expect(page).toHaveURL( + (url) => url.pathname.startsWith("/ui") && !url.pathname.includes("login"), + ); +} + +test("the team settings form skips a metadata row with an empty key and saving drops the key", async ({ + page, + request, +}) => { + const teamId = await createTeamCarryingAnEmptyMetadataKey(request); + try { + expect(Object.keys(await teamMetadata(request, teamId))).toContain(""); + await loginAsAdmin(page); + await page.goto(`/ui/models-and-endpoints?team=${teamId}`); + await page.getByRole("tab", { name: "Settings" }).click(); + await page.getByRole("button", { name: /edit settings/i }).click(); + await expect(page.getByLabel(/Team Name/)).toBeVisible(); + const keys = page.getByPlaceholder("Key", { exact: true }); + await expect(keys).toHaveCount(1); + await expect(keys.first()).toHaveValue("env"); + await expect( + page.getByPlaceholder("Value", { exact: true }).first(), + ).toHaveValue("staging"); + await page.getByRole("button", { name: "Save Changes" }).click(); + await expect + .poll(async () => { + const metadata = await teamMetadata(request, teamId); + return { hasEmptyKey: "" in metadata, env: metadata.env }; + }) + .toEqual({ hasEmptyKey: false, env: "staging" }); + } finally { + const removed = await request.post("/team/delete", { + headers, + data: { team_ids: [teamId] }, + }); + expect(removed.ok() || removed.status() === 404, await removed.text()).toBe( + true, + ); + } +}); diff --git a/tests/e2e/ui/tests/logs/logs.spec.ts b/tests/e2e/ui/tests/logs/logs.spec.ts index 60b547ccda0..688bfe8e3b4 100644 --- a/tests/e2e/ui/tests/logs/logs.spec.ts +++ b/tests/e2e/ui/tests/logs/logs.spec.ts @@ -54,6 +54,75 @@ test.describe("Logs page", () => { permissions: ["clipboard-read", "clipboard-write"], }); + test("log tables fill the available height and empty requests stay centered after resizing", async ({ + page, + }) => { + await navigateToPage(page, Page.Logs); + await dismissFeedbackPopup(page); + await visibleTestId(page, "datatable-search").fill( + `missing-request-${uniqueSuffix()}`, + ); + const emptyTitle = page.getByText("No matching requests", { exact: true }); + await expect(emptyTitle).toBeVisible(); + + for (const viewport of [ + { width: 1440, height: 900 }, + { width: 1024, height: 720 }, + ]) { + await page.setViewportSize(viewport); + await expect + .poll(async () => { + const frame = await visibleTestId( + page, + "data-table-frame", + ).boundingBox(); + return frame + ? Math.abs(viewport.height - frame.y - frame.height - 24) + : Infinity; + }) + .toBeLessThanOrEqual(2); + await expect + .poll(async () => { + const body = await page + .locator("table") + .filter({ visible: true }) + .first() + .locator("tbody") + .boundingBox(); + const scroller = await visibleTestId( + page, + "data-table-scroller", + ).boundingBox(); + const message = await emptyTitle.locator("..").boundingBox(); + if (!body || !message || !scroller) return Infinity; + return Math.max( + Math.abs( + message.x + message.width / 2 - scroller.x - scroller.width / 2, + ), + Math.abs(message.y + message.height / 2 - body.y - body.height / 2), + ); + }) + .toBeLessThanOrEqual(4); + for (const tab of ["Deleted Keys", "Deleted Teams"]) { + await page.getByRole("tab", { name: tab, exact: true }).click(); + await expect + .poll(async () => { + const frame = await visibleTestId( + page, + "data-table-frame", + ).boundingBox(); + return frame + ? Math.abs(viewport.height - frame.y - frame.height - 24) + : Infinity; + }) + .toBeLessThanOrEqual(2); + } + await page + .getByRole("tab", { name: "Request Logs", exact: true }) + .click(); + } + }); + test("a chat sent from the Playground lands in Logs with its content", async ({ page, request }) => { const prompt = `logs-playground-prompt-${uniqueSuffix()}`; await openPlayground(page); diff --git a/tests/integration/_support/anthropic_thinking.py b/tests/integration/_support/anthropic_thinking.py new file mode 100644 index 00000000000..e055451320e --- /dev/null +++ b/tests/integration/_support/anthropic_thinking.py @@ -0,0 +1,239 @@ +import base64 +import json +import re +from collections.abc import Mapping, Sequence +from dataclasses import dataclass +from functools import reduce +from itertools import chain +from typing import Final + +from integration._support.claude_code import sse_frame +from integration._support.upstream import _aws_event_frame +from integration._support.wire import Reply, Request +from pydantic import JsonValue, TypeAdapter + +MODEL: Final = "claude-sonnet-5-5" +BEDROCK_MODEL: Final = "anthropic.claude-sonnet-5-5" +THINKING_PARTS: Final = ("alpha ", "beta") +THINKING: Final = "alpha beta" +SIGNATURE: Final = "scripted-signature-" + "s" * 32 +NO_CACHE: Final = {"cache": {"no-cache": True}} +EVENT_STREAM: Final = "application/vnd.amazon.eventstream" +JSON_OBJECT: Final = TypeAdapter(dict[str, JsonValue]) +JSON_LIST: Final = TypeAdapter(list[JsonValue]) +BLOCKS: Final = TypeAdapter(list[dict[str, JsonValue]]) +_MARKER: Final = re.compile(r"marker-([0-9a-f]{32})") +_STREAMING_TARGETS: Final = ("/invoke-with-response-stream", ":streamRawPredict") + +Event = dict[str, JsonValue] + + +def prompt(marker: str) -> str: + return f"think it through for marker-{marker}" + + +def answer(marker: str) -> str: + return f"answer marker-{marker}" + + +def identity(marker: str) -> str: + return f"msg_{marker}" + + +def marker_of(request: Request) -> str: + found: Final = _MARKER.findall(request.body.decode()) + assert found, request.body + return found[-1] + + +def _event(**fields: JsonValue) -> Event: + return dict(fields) + + +def thinking_events(index: int, parts: Sequence[JsonValue], signatures: Sequence[JsonValue]) -> tuple[Event, ...]: + start: Final = _event( + type="content_block_start", index=index, content_block={"type": "thinking", "thinking": "", "signature": ""} + ) + thought: Final = tuple( + _event(type="content_block_delta", index=index, delta={"type": "thinking_delta", "thinking": part}) + for part in parts + ) + signed: Final = tuple( + _event(type="content_block_delta", index=index, delta={"type": "signature_delta", "signature": signature}) + for signature in signatures + ) + return (start, *thought, *signed, _event(type="content_block_stop", index=index)) + + +def redacted_events(index: int, data: str) -> tuple[Event, ...]: + return ( + _event(type="content_block_start", index=index, content_block={"type": "redacted_thinking", "data": data}), + _event(type="content_block_stop", index=index), + ) + + +def text_events(index: int, text: str) -> tuple[Event, ...]: + return ( + _event(type="content_block_start", index=index, content_block={"type": "text", "text": ""}), + _event(type="content_block_delta", index=index, delta={"type": "text_delta", "text": text}), + _event(type="content_block_stop", index=index), + ) + + +def message_events(marker: str, blocks: Sequence[Sequence[Event]]) -> tuple[Event, ...]: + start: Final = _event( + type="message_start", + message={ + "id": identity(marker), + "type": "message", + "role": "assistant", + "model": MODEL, + "content": [], + "stop_reason": None, + "stop_sequence": None, + "usage": {"input_tokens": 12, "output_tokens": 1}, + }, + ) + delta: Final = _event( + type="message_delta", delta={"stop_reason": "end_turn", "stop_sequence": None}, usage={"output_tokens": 9} + ) + return (start, *chain.from_iterable(blocks), delta, _event(type="message_stop")) + + +def standard_events( + marker: str, + *, + parts: Sequence[JsonValue] = THINKING_PARTS, + signatures: Sequence[JsonValue] = (SIGNATURE,), +) -> tuple[Event, ...]: + return message_events(marker, (thinking_events(0, parts, signatures), text_events(1, answer(marker)))) + + +def sse_chunks(events: Sequence[Event]) -> tuple[bytes, ...]: + return tuple(sse_frame(str(event["type"]), event) for event in events) + + +def aws_chunks(events: Sequence[Event]) -> tuple[bytes, ...]: + return tuple( + _aws_event_frame( + "chunk", + {"bytes": base64.b64encode(json.dumps(event, separators=(",", ":")).encode()).decode()}, + "sc", + "u", + ) + for event in events + ) + + +def message_body(marker: str) -> bytes: + return json.dumps( + { + "id": identity(marker), + "type": "message", + "role": "assistant", + "model": MODEL, + "content": [ + {"type": "thinking", "thinking": THINKING, "signature": SIGNATURE}, + {"type": "text", "text": answer(marker)}, + ], + "stop_reason": "end_turn", + "stop_sequence": None, + "usage": {"input_tokens": 12, "output_tokens": 9}, + } + ).encode() + + +def streams(request: Request) -> bool: + if request.target.endswith(_STREAMING_TARGETS): + return True + return JSON_OBJECT.validate_json(request.body).get("stream") is True + + +def stream_reply(request: Request, events: Sequence[Event], *, abort_after: int | None = None) -> Reply: + if request.target.endswith("/invoke-with-response-stream"): + return Reply(content_type=EVENT_STREAM, chunks=aws_chunks(events), abort_after=abort_after) + return Reply(content_type="text/event-stream", chunks=sse_chunks(events), abort_after=abort_after) + + +def standard_peer(request: Request) -> Reply: + marker: Final = marker_of(request) + if streams(request): + return stream_reply(request, standard_events(marker)) + return Reply(body=message_body(marker)) + + +def chunks_of(text: str) -> tuple[Event, ...]: + return tuple( + JSON_OBJECT.validate_json(line.removeprefix("data: ")) + for line in text.splitlines() + if line.startswith("data: {") + ) + + +def delta_of(chunk: Mapping[str, JsonValue]) -> Event: + choices: Final = JSON_LIST.validate_python(chunk.get("choices") or []) + if not choices: + return {} + return JSON_OBJECT.validate_python(JSON_OBJECT.validate_python(choices[0]).get("delta") or {}) + + +def deltas_of(chunks: Sequence[Mapping[str, JsonValue]]) -> tuple[Event, ...]: + return tuple(delta_of(chunk) for chunk in chunks) + + +def blocks_of(delta: Mapping[str, JsonValue]) -> tuple[Event, ...]: + return tuple(BLOCKS.validate_python(delta.get("thinking_blocks") or [])) + + +def all_blocks(deltas: Sequence[Mapping[str, JsonValue]]) -> tuple[Event, ...]: + return tuple(chain.from_iterable(blocks_of(delta) for delta in deltas)) + + +def signed_blocks(deltas: Sequence[Mapping[str, JsonValue]]) -> tuple[Event, ...]: + return tuple(block for block in all_blocks(deltas) if block.get("signature")) + + +def reasoning_text(deltas: Sequence[Mapping[str, JsonValue]]) -> str: + return "".join(str(delta.get("reasoning_content") or "") for delta in deltas) + + +def content_text(deltas: Sequence[Mapping[str, JsonValue]]) -> str: + return "".join(str(delta.get("content") or "") for delta in deltas) + + +def thinking_block(thinking: str, signature: JsonValue) -> Event: + return {"type": "thinking", "thinking": thinking, "signature": signature} + + +def signature_only(signature: JsonValue = SIGNATURE) -> Event: + return thinking_block("", signature) + + +@dataclass(frozen=True, slots=True) +class _Accumulated: + closed: tuple[Event, ...] + text: str + + +def _fold(state: _Accumulated, block: Mapping[str, JsonValue]) -> _Accumulated: + if block.get("type") == "redacted_thinking": + redacted: Event = {"type": "redacted_thinking", "data": block.get("data")} + return _Accumulated((*state.closed, redacted), state.text) + text: Final = state.text + str(block.get("thinking") or "") + signature: Final = block.get("signature") + if not signature: + return _Accumulated(state.closed, text) + return _Accumulated((*state.closed, thinking_block(text, signature)), "") + + +def accumulate(deltas: Sequence[Mapping[str, JsonValue]]) -> tuple[Event, ...]: + return reduce(_fold, all_blocks(deltas), _Accumulated((), "")).closed + + +def logged_thinking(response: Mapping[str, JsonValue]) -> tuple[Event, ...]: + if "choices" in response: + choice: Final = JSON_OBJECT.validate_python(JSON_LIST.validate_python(response["choices"])[0]) + message: Final = JSON_OBJECT.validate_python(choice.get("message") or {}) + return tuple(BLOCKS.validate_python(message.get("thinking_blocks") or [])) + content: Final = BLOCKS.validate_python(response.get("content") or []) + return tuple(block for block in content if block.get("type") in ("thinking", "redacted_thinking")) diff --git a/tests/integration/_support/database_relay.py b/tests/integration/_support/database_relay.py index 1b3bc0183a5..cb8b9098a64 100644 --- a/tests/integration/_support/database_relay.py +++ b/tests/integration/_support/database_relay.py @@ -88,15 +88,89 @@ class DatabaseRelay: ) +class HeldStatementRelay: + def __init__(self, upstream_host: str, upstream_port: int, trigger: bytes) -> None: + self.port: Final = _free_port() + self._upstream_host: Final = upstream_host + self._upstream_port: Final = upstream_port + self._trigger: Final = trigger + self._loop: Final = asyncio.new_event_loop() + self._released: Final = asyncio.Event() + self.held: Final = threading.Event() + self._ready: Final = threading.Event() + self._thread: Final = threading.Thread(target=self._run, daemon=True) + + def release(self) -> None: + self._loop.call_soon_threadsafe(self._released.set) + + def start(self) -> None: + self._thread.start() + assert self._ready.wait(10), "Database relay did not start" + + def stop(self) -> None: + self.release() + self._loop.call_soon_threadsafe(self._loop.stop) + self._thread.join(10) + + def _run(self) -> None: + asyncio.set_event_loop(self._loop) + self._loop.run_until_complete(asyncio.start_server(self._serve, "127.0.0.1", self.port)) + self._ready.set() + self._loop.run_forever() + + def _holds(self, window: bytes) -> bool: + return not self.held.is_set() and self._trigger in window + + async def _serve(self, client_reader: asyncio.StreamReader, client_writer: asyncio.StreamWriter) -> None: + server_reader, server_writer = await asyncio.open_connection(self._upstream_host, self._upstream_port) + + async def forward(reader: asyncio.StreamReader, writer: asyncio.StreamWriter, inspect: bool) -> None: + tail = b"" # rebind-ok: carries the previous read's end so a trigger split across reads still matches + try: + while chunk := await reader.read(65536): + window: Final = tail + chunk + if inspect and self._holds(window): + self.held.set() + await self._released.wait() + tail = window[-(len(self._trigger) - 1) :] + writer.write(chunk) + await writer.drain() + except (ConnectionError, asyncio.IncompleteReadError): + return + finally: + writer.close() + + await asyncio.gather( + forward(client_reader, server_writer, True), + forward(server_reader, client_writer, False), + ) + + +def _relayed_url(database_url: str, port: int) -> str: + parts: Final = urlsplit(database_url) + credentials: Final = f"{parts.username}:{parts.password}@" if parts.username else "" + return urlunsplit(parts._replace(netloc=f"{credentials}127.0.0.1:{port}")) + + @contextmanager def database_relay(database_url: str, trigger: bytes) -> Generator[tuple[DatabaseRelay, str]]: parts: Final = urlsplit(database_url) assert parts.hostname is not None and parts.port is not None, database_url relay: Final = DatabaseRelay(parts.hostname, parts.port, trigger) relay.start() - credentials: Final = f"{parts.username}:{parts.password}@" if parts.username else "" - relayed: Final = urlunsplit(parts._replace(netloc=f"{credentials}127.0.0.1:{relay.port}")) try: - yield relay, relayed + yield relay, _relayed_url(database_url, relay.port) + finally: + relay.stop() + + +@contextmanager +def held_statement_relay(database_url: str, trigger: bytes) -> Generator[tuple[HeldStatementRelay, str]]: + parts: Final = urlsplit(database_url) + assert parts.hostname is not None and parts.port is not None, database_url + relay: Final = HeldStatementRelay(parts.hostname, parts.port, trigger) + relay.start() + try: + yield relay, _relayed_url(database_url, relay.port) finally: relay.stop() diff --git a/tests/integration/_support/redis_process.py b/tests/integration/_support/redis_process.py index 86abcbe024e..3a5bf8bec23 100644 --- a/tests/integration/_support/redis_process.py +++ b/tests/integration/_support/redis_process.py @@ -25,8 +25,16 @@ class OwnedRedis: process: subprocess.Popen | None = None server_pid: int | None = None + def serves(self) -> bool: + with Redis(host=self.host, port=self.port, socket_connect_timeout=0.2, socket_timeout=0.2) as client: + try: + return bool(client.ping()) + except RedisConnectionError: + return False + def start(self) -> None: assert self.process is None + assert not self.serves(), f"Another Redis already serves {self.host}:{self.port} before the owned one starts" self.process = subprocess.Popen(self.command, stdout=self.log, stderr=subprocess.STDOUT, start_new_session=True) deadline: Final = time.monotonic() + 8 with Redis(host=self.host, port=self.port, socket_connect_timeout=0.2, socket_timeout=0.2) as client: @@ -68,14 +76,7 @@ class OwnedRedis: self.process.wait(timeout=3) self.process = None self.server_pid = None - with Redis(host=self.host, port=self.port, socket_connect_timeout=0.2, socket_timeout=0.2) as client: - try: - client.ping() - except RedisConnectionError: - stopped = True - else: - stopped = False - assert stopped, "Owned Redis still serves after shutdown" + assert not self.serves(), "Owned Redis still serves after shutdown" assert failure is None and not forced, f"Owned Redis required shutdown recovery: {failure!r}" def signal(self, action: signal.Signals) -> None: @@ -92,16 +93,15 @@ class OwnedRedis: @contextmanager def owned_redis(directory: Path) -> Iterator[OwnedRedis]: binary: Final = shutil.which("redis-server") + with socket.socket() as reservation: + reservation.bind(("127.0.0.1", 0)) + port: Final = reservation.getsockname()[1] if binary: - with socket.socket() as reservation: - reservation.bind(("127.0.0.1", 0)) - port = reservation.getsockname()[1] host = "127.0.0.1" prefix = (binary,) else: host = subprocess.check_output(["docker", "inspect", "--format", "{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}", "redis-cache"], text=True).strip() assert host, "CircleCI owned Redis container has no address" - port = 16379 prefix = ("docker", "exec", "redis-cache", "redis-server") output: Final = Path(os.environ.get("INTEGRATION_RESULTS_DIR", str(directory))) output.mkdir(parents=True, exist_ok=True) diff --git a/tests/integration/_support/responses_vendor.py b/tests/integration/_support/responses_vendor.py new file mode 100644 index 00000000000..35f3ee28368 --- /dev/null +++ b/tests/integration/_support/responses_vendor.py @@ -0,0 +1,271 @@ +from __future__ import annotations + +import base64 +import json +import os +import re +import uuid +from collections import deque +from collections.abc import Mapping, Sequence +from dataclasses import dataclass, field +from typing import Final +from urllib.parse import urlsplit + +from integration._support import claude_code as cc +from integration._support.wire import Reply, Request +from pydantic import JsonValue, TypeAdapter + +MARKER: Final = re.compile(r"marker-([0-9a-f]{32})") +THOUGHT: Final = "plan the answer" +USAGE: Final[dict[str, JsonValue]] = {"input_tokens": 30, "output_tokens": 5, "total_tokens": 35} +CHAT_USAGE: Final[dict[str, JsonValue]] = {"prompt_tokens": 30, "completion_tokens": 5, "total_tokens": 35} +CLAUDE_USAGE: Final[dict[str, JsonValue]] = {"input_tokens": 20, "output_tokens": 7} +JSON_OBJECT: Final = TypeAdapter(dict[str, JsonValue]) +ITEMS: Final = TypeAdapter(list[dict[str, JsonValue]]) +MINTED_ID: Final = re.compile(r"^rs_[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$") +_INNER_ID: Final = re.compile(r"response_id:([^;]+)") +_WRAPPER_PREFIX: Final = "litellm:custom_llm_provider:" +_PROXY_WRAPPED_PREFIX: Final = "litellm_proxy:responses_api:response_id:" + + +def signature(marker: str) -> str: + return f"sig-{marker}" + + +def answer(marker: str | None) -> str: + return "ok" if marker is None else f"answer marker-{marker}" + + +def newest_marker(text: str) -> str | None: + found: Final = MARKER.findall(text) + return str(found[-1]) if found else None + + +def error(status: int, message: str, code: str) -> Reply: + body: Final = {"error": {"message": message, "type": "invalid_request_error", "param": None, "code": code}} + return Reply(status=status, body=json.dumps(body).encode()) + + +def sse(event: Mapping[str, JsonValue]) -> bytes: + return f"event: {event['type']}\ndata: {json.dumps(event)}\n\n".encode() + + +def chat_sse(frame: Mapping[str, JsonValue]) -> bytes: + return b"data: " + json.dumps(frame).encode() + b"\n\n" + + +def thinking_json(marker: str) -> str: + return json.dumps([{"type": "thinking", "thinking": THOUGHT, "signature": signature(marker)}]) + + +def minted_item(marker: str, **extra: JsonValue) -> dict[str, JsonValue]: + return {"type": "reasoning", "id": f"rs_{uuid.uuid4()}", "encrypted_content": thinking_json(marker), **extra} + + +def agents_sdk_history(marker: str, *reasoning: dict[str, JsonValue]) -> list[dict[str, JsonValue]]: + return [ + {"role": "user", "content": "Pick a city and look up its weather."}, + *reasoning, + { + "type": "message", + "id": f"msg_{uuid.uuid4()}", + "role": "assistant", + "status": "completed", + "content": [{"type": "output_text", "text": "Prague", "annotations": []}], + }, + {"type": "function_call", "call_id": "call_weather", "name": "weather", "arguments": '{"city": "Prague"}'}, + {"type": "function_call_output", "call_id": "call_weather", "output": '{"celsius": 18}'}, + {"role": "user", "content": f"Now answer marker-{marker}"}, + ] + + +def without(history: Sequence[dict[str, JsonValue]], dropped: Sequence[dict[str, JsonValue]]) -> list[JsonValue]: + return [item for item in history if all(item is not gone for gone in dropped)] + + +def reasoning_items(body: Mapping[str, JsonValue]) -> list[dict[str, JsonValue]]: + return [item for item in ITEMS.validate_python(body["input"]) if item.get("type") == "reasoning"] + + +def _decoded_wrapper(value: str) -> str | None: + try: + decoded: Final = base64.b64decode(value.removeprefix("resp_"), validate=True).decode() + except (ValueError, UnicodeDecodeError): + return None + return decoded if decoded.startswith(_WRAPPER_PREFIX) else None + + +def response_identities(value: str) -> frozenset[str]: + from litellm.proxy.common_utils.encrypt_decrypt_utils import decrypt_if_encrypted_with + + salt: Final = os.environ.get("LITELLM_SALT_KEY", "sk-integration-salt") + opened: Final = decrypt_if_encrypted_with(value.removeprefix("resp_"), salt) + sealed: Final = opened is not None and opened.startswith(_PROXY_WRAPPED_PREFIX) + wrapped: Final = opened.removeprefix(_PROXY_WRAPPED_PREFIX).split(";", 1)[0] if sealed and opened else value + decoded: Final = _decoded_wrapper(wrapped) + if decoded is None: + return frozenset({wrapped}) + inner: Final = _INNER_ID.search(decoded) + assert inner is not None, decoded + return frozenset({wrapped, inner.group(1)}) + + +def same_response(left: str, right: str) -> bool: + return bool(response_identities(left) & response_identities(right)) + + +@dataclass(frozen=True, slots=True) +class ResponsesVendor: + claude_model: str = cc.OPUS + pause_between_chunks: float = 0 + minted: deque[str] = field(default_factory=deque) + + def respond(self, request: Request) -> Reply: + path: Final = urlsplit(request.target).path + if request.method == "GET": + return Reply(body=json.dumps({"object": "list", "data": [{"id": "gpt-5.6", "object": "model"}]}).encode()) + body: Final = JSON_OBJECT.validate_json(request.body) + if path.endswith("/messages"): + return self._claude(body) + if path.endswith("/chat/completions"): + return self._chat(body) + assert path.endswith("/responses"), request.target + verdict: Final = self._verdict(body) + return verdict if verdict is not None else self._responses(body) + + def _verdict(self, body: Mapping[str, JsonValue]) -> Reply | None: + received: Final = body.get("input") + if isinstance(received, str): + return None + items: Final = ITEMS.validate_python(received) + if not items and "previous_response_id" not in body: + return error( + 400, 'One of "input" or "previous_response_id" must be provided.', "missing_required_parameter" + ) + for index, item in enumerate(items): + if item.get("type") != "reasoning": + continue + item_id: Final = item.get("id") + if item_id is not None and not isinstance(item_id, str): + return error(400, f"Invalid type for 'input[{index}].id': expected a string.", "invalid_type") + if "summary" not in item: + return error( + 400, f"Missing required parameter: 'input[{index}].summary'.", "missing_required_parameter" + ) + if item_id == "": + return error(400, f"Invalid 'input[{index}].id': empty string.", "invalid_value") + if isinstance(item_id, str) and item_id not in self.minted: + return error(404, f"Item with id '{item_id}' not found.", "invalid_request_error") + return None + + def _responses(self, body: Mapping[str, JsonValue]) -> Reply: + marker: Final = newest_marker(json.dumps(body)) + tag: Final = uuid.uuid4().hex + self.minted.append(f"rs_{tag}") + reasoning: Final[dict[str, JsonValue]] = { + "id": f"rs_{tag}", + "type": "reasoning", + "summary": [], + "encrypted_content": f"gAAAAA-vendor-{tag}", + } + message: Final[dict[str, JsonValue]] = { + "id": f"msg_{tag}", + "type": "message", + "role": "assistant", + "status": "completed", + "content": [{"type": "output_text", "text": answer(marker), "annotations": []}], + } + response: Final[dict[str, JsonValue]] = { + "id": f"resp_{tag}", + "object": "response", + "created_at": 1, + "status": "completed", + "model": body["model"], + "output": [reasoning, message], + "usage": USAGE, + } + if body.get("stream") is not True: + return Reply(body=json.dumps(response).encode()) + events: Final[tuple[dict[str, JsonValue], ...]] = ( + { + "type": "response.created", + "sequence_number": 0, + "response": {**response, "status": "in_progress", "output": []}, + }, + {"type": "response.output_item.added", "sequence_number": 1, "output_index": 0, "item": reasoning}, + {"type": "response.output_item.done", "sequence_number": 2, "output_index": 0, "item": reasoning}, + { + "type": "response.output_item.added", + "sequence_number": 3, + "output_index": 1, + "item": {**message, "content": []}, + }, + { + "type": "response.output_text.delta", + "sequence_number": 4, + "item_id": f"msg_{tag}", + "output_index": 1, + "content_index": 0, + "delta": answer(marker), + }, + {"type": "response.output_item.done", "sequence_number": 5, "output_index": 1, "item": message}, + {"type": "response.completed", "sequence_number": 6, "response": response}, + ) + return Reply( + content_type="text/event-stream", + chunks=tuple(sse(event) for event in events), + pause_between_chunks=self.pause_between_chunks, + ) + + def _chat(self, body: Mapping[str, JsonValue]) -> Reply: + marker: Final = newest_marker(json.dumps(body)) + tag: Final = uuid.uuid4().hex + if body.get("stream") is not True: + return Reply( + body=json.dumps( + { + "id": f"chatcmpl-{tag}", + "object": "chat.completion", + "created": 1, + "model": body["model"], + "choices": [ + { + "index": 0, + "message": {"role": "assistant", "content": answer(marker)}, + "finish_reason": "stop", + } + ], + "usage": CHAT_USAGE, + } + ).encode() + ) + chunk: Final[dict[str, JsonValue]] = { + "id": f"chatcmpl-{tag}", + "object": "chat.completion.chunk", + "created": 1, + "model": body["model"], + } + frames: Final[tuple[dict[str, JsonValue], ...]] = ( + {**chunk, "choices": [{"index": 0, "delta": {"role": "assistant", "content": answer(marker)}}]}, + {**chunk, "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}], "usage": CHAT_USAGE}, + ) + return Reply( + content_type="text/event-stream", + chunks=(*(chat_sse(frame) for frame in frames), b"data: [DONE]\n\n"), + pause_between_chunks=self.pause_between_chunks, + ) + + def _claude(self, body: Mapping[str, JsonValue]) -> Reply: + marker: Final = newest_marker(json.dumps(body)) + content: Final = ( + {"type": "thinking", "thinking": THOUGHT, "signature": signature(marker or "")}, + {"type": "text", "text": answer(marker)}, + ) + identity: Final = f"msg_{uuid.uuid4().hex}" + if body.get("stream") is True: + return Reply( + content_type="text/event-stream", + chunks=cc.message_stream(identity, self.claude_model, content, CLAUDE_USAGE), + pause_between_chunks=self.pause_between_chunks, + ) + return Reply(body=cc.message_reply(identity, self.claude_model, content, CLAUDE_USAGE)) diff --git a/tests/integration/_support/upstream.py b/tests/integration/_support/upstream.py index e645126032b..a06c6099f0e 100644 --- a/tests/integration/_support/upstream.py +++ b/tests/integration/_support/upstream.py @@ -83,25 +83,39 @@ def _aws_str_header(name: str, value: str) -> bytes: ) +def _aws_int_header(name: str, value: int) -> bytes: + name_bytes: Final = name.encode() + return struct.pack("!B", len(name_bytes)) + name_bytes + struct.pack("!B", 4) + struct.pack("!i", value) + + +def aws_event_stream_frame(headers: Mapping[str, str | int], payload: bytes) -> bytes: + """One AWS event-stream frame: a string header is wire type 7, an int header wire type 4 (int32).""" + headers_bytes: Final = b"".join( + _aws_str_header(name, value) if isinstance(value, str) else _aws_int_header(name, value) + for name, value in headers.items() + ) + total_length: Final = 12 + len(headers_bytes) + len(payload) + 4 + prelude: Final = struct.pack("!II", total_length, len(headers_bytes)) + prelude_crc: Final = struct.pack("!I", zlib.crc32(prelude) & 0xFFFFFFFF) + message: Final = prelude + prelude_crc + headers_bytes + payload + return message + struct.pack("!I", zlib.crc32(message) & 0xFFFFFFFF) + + def _aws_event_frame( event_type: str, payload: Mapping[str, JsonValue], scenario_id: str, unique_id: str, ) -> bytes: - payload_bytes: Final = json.dumps(payload, separators=(",", ":")).replace( - "$REQUEST_ID", scenario_id - ).replace("$UNIQUE_ID", unique_id).encode() - headers_bytes: Final = ( - _aws_str_header(":event-type", event_type) - + _aws_str_header(":content-type", "application/json") - + _aws_str_header(":message-type", "event") + payload_bytes: Final = ( + json.dumps(payload, separators=(",", ":")) + .replace("$REQUEST_ID", scenario_id) + .replace("$UNIQUE_ID", unique_id) + .encode() + ) + return aws_event_stream_frame( + {":event-type": event_type, ":content-type": "application/json", ":message-type": "event"}, payload_bytes ) - total_length: Final = 12 + len(headers_bytes) + len(payload_bytes) + 4 - prelude: Final = struct.pack("!II", total_length, len(headers_bytes)) - prelude_crc: Final = struct.pack("!I", zlib.crc32(prelude) & 0xFFFFFFFF) - message: Final = prelude + prelude_crc + headers_bytes + payload_bytes - return message + struct.pack("!I", zlib.crc32(message) & 0xFFFFFFFF) class ScenarioStore: @@ -147,7 +161,13 @@ class Provider: status: Final = script.popleft() if status != 200: return JSONResponse( - {"error": {"message": "Controlled provider failure", "type": error_type(status), "code": str(status)}}, + { + "error": { + "message": "Controlled provider failure", + "type": error_type(status), + "code": str(status), + } + }, status_code=status, ) return await chat_completions(request) @@ -244,9 +264,7 @@ class Provider: if raw_body: body: Final = JSON_OBJECT.validate_json(raw_body) if isinstance(body, dict): - self.observations.put( - Observation(request.url.path, request.headers.get("authorization", ""), body) - ) + self.observations.put(Observation(request.url.path, request.headers.get("authorization", ""), body)) if isinstance(response, RoutedResponse): route_key: Final = f"{request.method} /{'/'.join(segments[1:])}" route: Final = next( @@ -300,11 +318,10 @@ class Provider: match response: case JsonResponse(): return Response( - content=json.dumps(response.body, separators=(",", ":")).replace( - "$REQUEST_ID", scenario_id - ).replace( - "$UNIQUE_ID", unique_id - ).encode(), + content=json.dumps(response.body, separators=(",", ":")) + .replace("$REQUEST_ID", scenario_id) + .replace("$UNIQUE_ID", unique_id) + .encode(), media_type=response.content_type, status_code=response.status, ) @@ -321,6 +338,7 @@ class Provider: ) case SseResponse(): if response.frame_delay_ms > 0: + async def stream() -> AsyncIterator[bytes]: for frame in response.frames: yield ( @@ -329,9 +347,11 @@ class Provider: await asyncio.sleep(response.frame_delay_ms / 1000) return StreamingResponse(stream(), media_type=response.content_type) - stream_body: Final = ("\n\n".join(response.frames) + "\n\n").replace( - "$REQUEST_ID", scenario_id - ).replace("$UNIQUE_ID", unique_id) + stream_body: Final = ( + ("\n\n".join(response.frames) + "\n\n") + .replace("$REQUEST_ID", scenario_id) + .replace("$UNIQUE_ID", unique_id) + ) return Response(content=stream_body.encode(), media_type=response.content_type) case EventStreamResponse(): events: Final = ( diff --git a/tests/integration/authorization/test_jwt_auto_register_map_existing_key.py b/tests/integration/authorization/test_jwt_auto_register_map_existing_key.py new file mode 100644 index 00000000000..5215054a364 --- /dev/null +++ b/tests/integration/authorization/test_jwt_auto_register_map_existing_key.py @@ -0,0 +1,219 @@ +import json +import os +import time +import uuid +from collections.abc import Iterator +from concurrent.futures import ThreadPoolExecutor +from contextlib import contextmanager +from hashlib import sha256 +from pathlib import Path +from typing import Final + +import httpx +import jwt +import pytest +import yaml +from cryptography.hazmat.primitives.asymmetric import rsa + +from tests.integration._support.client import Gateway, eventually, string_value +from tests.integration._support.database import read_rows +from tests.integration._support.database_relay import held_statement_relay +from tests.integration._support.process import owned_proxy +from tests.integration._support.wire import Reply, Request, wire_server + +KEY_ID: Final = "integration-jwt-map-existing-key" +MAPPING_INSERT: Final = b'INSERT INTO "public"."LiteLLM_JWTKeyMapping"' + +pytestmark = pytest.mark.timeout(240) + + +def _hash(key: str) -> str: + return sha256(key.encode()).hexdigest() + + +def _config(directory: Path, claim_field: str) -> Path: + config: Final = yaml.safe_load(Path("tests/integration/proxy_config.yaml").read_text()) + config["general_settings"] = { + **config["general_settings"], + "enable_jwt_auth": True, + "litellm_jwtauth": { + "user_id_jwt_field": "sub", + "user_email_jwt_field": "email", + "virtual_key_claim_field": claim_field, + "unregistered_jwt_client_behavior": "auto_register", + "auto_register_map_existing_key": True, + }, + } + path: Final = directory / f"jwt_map_existing_key_{claim_field}.yaml" + path.write_text(yaml.safe_dump(config)) + return path + + +@contextmanager +def _issuer() -> Iterator[tuple[rsa.RSAPrivateKey, str]]: + private_key: Final = rsa.generate_private_key(public_exponent=65537, key_size=2048) + public_jwk: Final = json.loads(jwt.algorithms.RSAAlgorithm.to_jwk(private_key.public_key())) + jwks: Final = json.dumps({"keys": [{**public_jwk, "kid": KEY_ID, "use": "sig", "alg": "RS256"}]}).encode() + + def respond(request: Request) -> Reply: + assert request.method == "GET", request + return Reply(body=jwks) + + with wire_server(respond) as server: + yield private_key, server.url + + +def _token(private_key: rsa.RSAPrivateKey, subject: str, **claims: str) -> str: + now: Final = int(time.time()) + return jwt.encode( + {"sub": subject, **claims, "iat": now, "exp": now + 300}, + private_key, + algorithm="RS256", + headers={"kid": KEY_ID}, + ) + + +def _chat(candidate: Gateway, model: str, token: str) -> httpx.Response: + return candidate.request( + "POST", + "/v1/chat/completions", + {"model": model, "messages": [{"role": "user", "content": "map existing key control"}]}, + key=token, + ) + + +def _mapped_token(claim_name: str, claim_value: str) -> str: + rows: Final = read_rows( + 'SELECT token FROM "LiteLLM_JWTKeyMapping" WHERE jwt_claim_name = %s AND jwt_claim_value = %s', + (claim_name, claim_value), + ) + assert len(rows) == 1, rows + return string_value(rows[0]["token"]) + + +def _user_key_hashes(user: str) -> frozenset[str]: + rows: Final = read_rows('SELECT token FROM "LiteLLM_VerificationToken" WHERE user_id = %s', (user,)) + return frozenset(string_value(row["token"]) for row in rows) + + +def _billed_key(response: httpx.Response) -> str: + rows: Final = eventually( + lambda: read_rows( + 'SELECT api_key FROM "LiteLLM_SpendLogs" WHERE request_id = %s', (str(response.json()["id"]),) + ), + lambda values: len(values) == 1, + seconds=70, + ) + return string_value(rows[0]["api_key"]) + + +def test_first_jwt_call_reuses_the_newest_durable_llm_key_and_skips_every_ineligible_newer_key( + gateway: Gateway, tmp_path: Path +) -> None: + with _issuer() as (private_key, jwks_url), gateway.scenario() as scenario: + model: Final = scenario.model() + user: Final = scenario.user(user_role="internal_user") + older_durable: Final = scenario.key(user_id=user) + durable: Final = scenario.key(user_id=user) + skipped: Final = { + "older_durable": older_durable, + "expiring": scenario.key(user_id=user, duration="1h"), + "management_only": scenario.key(user_id=user, allowed_routes=["management_routes"]), + "auto_registered_look_alike": scenario.key(user_id=user, metadata={"auto_registered": True}), + "other_team": scenario.key(user_id=user, team_id=scenario.team()), + "blocked": scenario.key(user_id=user), + } + gateway.post("/key/block", {"key": skipped["blocked"]}) + keys_before: Final = _user_key_hashes(user) + + with owned_proxy( + gateway, tmp_path, {"JWT_PUBLIC_KEY_URL": jwks_url}, config=_config(tmp_path, "sub") + ) as candidate: + response: Final = _chat(candidate, model, _token(private_key, user)) + + assert response.status_code == 200, response.text + mapped: Final = _mapped_token("sub", user) + assert mapped == _hash(durable), { + "mapped_to": next((name for name, key in skipped.items() if _hash(key) == mapped), mapped) + } + assert _user_key_hashes(user) == keys_before, "a key was minted although a reusable one existed" + assert _billed_key(response) == _hash(durable) + + +def test_user_matched_by_email_instead_of_sub_still_reuses_their_existing_key(gateway: Gateway, tmp_path: Path) -> None: + with _issuer() as (private_key, jwks_url), gateway.scenario() as scenario: + model: Final = scenario.model() + email: Final = f"integration-{uuid.uuid4().hex}@example.com" + user: Final = scenario.user(user_role="internal_user", user_email=email) + existing: Final = scenario.key(user_id=user) + subject: Final = f"integration-idp-subject-{uuid.uuid4().hex}" + + with owned_proxy( + gateway, tmp_path, {"JWT_PUBLIC_KEY_URL": jwks_url}, config=_config(tmp_path, "sub") + ) as candidate: + response: Final = _chat(candidate, model, _token(private_key, subject, email=email.upper())) + + assert response.status_code == 200, response.text + assert _mapped_token("sub", subject) == _hash(existing) + assert _user_key_hashes(user) == frozenset({_hash(existing)}), "a key was minted for an email-matched user" + assert _billed_key(response) == _hash(existing) + + +def test_shared_client_claim_never_maps_a_second_user_onto_the_first_users_personal_key( + gateway: Gateway, tmp_path: Path +) -> None: + with _issuer() as (private_key, jwks_url), gateway.scenario() as scenario: + model: Final = scenario.model() + first_user: Final = scenario.user(user_role="internal_user") + second_user: Final = scenario.user(user_role="internal_user") + personal: Final = scenario.key(user_id=first_user) + client_id: Final = f"integration-shared-client-{uuid.uuid4().hex}" + + with owned_proxy( + gateway, tmp_path, {"JWT_PUBLIC_KEY_URL": jwks_url}, config=_config(tmp_path, "client_id") + ) as candidate: + first: Final = _chat(candidate, model, _token(private_key, first_user, client_id=client_id)) + second: Final = _chat(candidate, model, _token(private_key, second_user, client_id=client_id)) + + assert first.status_code == 200, first.text + assert second.status_code == 200, second.text + mapped: Final = _mapped_token("client_id", client_id) + assert mapped != _hash(personal), "the shared client claim was mapped to the first user's personal key" + assert (_billed_key(first), _billed_key(second)) == (mapped, mapped) + assert read_rows('SELECT request_id FROM "LiteLLM_SpendLogs" WHERE api_key = %s', (_hash(personal),)) == [] + + +def test_concurrent_first_jwt_calls_of_a_keyless_user_both_succeed_on_one_surviving_mapped_key( + gateway: Gateway, tmp_path: Path +) -> None: + writer_url: Final = os.environ.get("INTEGRATION_PROXY_DATABASE_URL") or os.environ["DATABASE_URL"] + with ( + _issuer() as (private_key, jwks_url), + gateway.scenario() as scenario, + held_statement_relay(writer_url, MAPPING_INSERT) as (relay, relayed_url), + ): + model: Final = scenario.model() + user: Final = scenario.user(user_role="internal_user") + token: Final = _token(private_key, user) + overrides: Final = { + "JWT_PUBLIC_KEY_URL": jwks_url, + "DATABASE_URL": relayed_url, + "PRISMA_HEALTH_WATCHDOG_ENABLED": "false", + } + + with ( + owned_proxy(gateway, tmp_path, overrides, config=_config(tmp_path, "sub")) as candidate, + ThreadPoolExecutor(max_workers=1) as pool, + ): + held_call: Final = pool.submit(_chat, candidate, model, token) + assert relay.held.wait(60), "the first call never reached its mapping insert" + racing: Final = _chat(candidate, model, token) + relay.release() + held: Final = held_call.result(timeout=60) + + assert racing.status_code == 200, racing.text + assert held.status_code == 200, held.text + keys: Final = _user_key_hashes(user) + assert len(keys) == 1, keys + assert _mapped_token("sub", user) in keys + assert (_billed_key(held), _billed_key(racing)) == (_mapped_token("sub", user),) * 2 diff --git a/tests/integration/management/test_scim_group_pathless_patch.py b/tests/integration/management/test_scim_group_pathless_patch.py new file mode 100644 index 00000000000..5ed784d2bb8 --- /dev/null +++ b/tests/integration/management/test_scim_group_pathless_patch.py @@ -0,0 +1,549 @@ +import os +import signal +import threading +import uuid +from collections.abc import Callable, Mapping, Sequence +from concurrent.futures import ThreadPoolExecutor +from dataclasses import dataclass +from itertools import chain +from pathlib import Path +from types import MappingProxyType +from typing import Final + +import httpx +import psutil +import pytest +from integration._support.client import Gateway, Scenario, eventually, object_value, string_value +from integration._support.process import owned_proxy_process +from integration.authorization._guardrail_opt_out import upstream_hits +from pydantic import JsonValue + +PATCH_OP_SCHEMA: Final = "urn:ietf:params:scim:api:messages:2.0:PatchOp" +GROUP_SCHEMA: Final = "urn:ietf:params:scim:schemas:core:2.0:Group" +BURST_TEAMS: Final = 10 +BURST_REQUESTS_PER_TEAM: Final = 3 +KILL_AFTER_RESPONSES: Final = 5 + + +def patch_group( + candidate: Gateway, team: str, operations: Sequence[JsonValue], *, key: str | None = None +) -> httpx.Response: + return candidate.request( + "PATCH", + f"/scim/v2/Groups/{team}", + {"schemas": [PATCH_OP_SCHEMA], "Operations": list(operations)}, + key=key, + ) + + +def pathless(op: str, value: JsonValue) -> dict[str, JsonValue]: + return {"op": op, "value": value} + + +def pathed(op: str, path: str, value: JsonValue | None = None) -> dict[str, JsonValue]: + return {"op": op, "path": path, **({} if value is None else {"value": value})} + + +def team_info(candidate: Gateway, team: str) -> dict[str, JsonValue]: + return object_value(candidate.get("/team/info", {"team_id": team})["team_info"]) + + +def team_metadata(candidate: Gateway, team: str) -> dict[str, JsonValue]: + return object_value(team_info(candidate, team).get("metadata") or {}) + + +def team_alias(candidate: Gateway, team: str) -> JsonValue: + return team_info(candidate, team).get("team_alias") + + +def alias_and_metadata(candidate: Gateway, team: str) -> tuple[JsonValue, dict[str, JsonValue]]: + info: Final = team_info(candidate, team) + return info.get("team_alias"), object_value(info.get("metadata") or {}) + + +def member_ids(candidate: Gateway, team: str) -> frozenset[str]: + members: Final = team_info(candidate, team).get("members_with_roles") or [] + assert isinstance(members, list), members + return frozenset(string_value(object_value(member)["user_id"]) for member in members) + + +def group_member_ids(candidate: Gateway, team: str) -> frozenset[str]: + members: Final = candidate.get(f"/scim/v2/Groups/{team}").get("members") or [] + assert isinstance(members, list), members + return frozenset(string_value(object_value(member)["value"]) for member in members) + + +def scim_data(metadata: Mapping[str, JsonValue]) -> dict[str, JsonValue]: + return object_value(metadata["scim_data"]) + + +def scim_group(scenario: Scenario, members: Sequence[str]) -> str: + created: Final = scenario.gateway.request( + "POST", + "/scim/v2/Groups", + { + "schemas": [GROUP_SCHEMA], + "displayName": f"integration-{uuid.uuid4().hex}", + "members": [{"value": member} for member in members], + }, + ) + assert created.status_code == 201, created.text + team: Final = string_value(object_value(created.json())["id"]) + scenario.cleanups.callback(scenario.delete_team, team) + return team + + +def model_names(candidate: Gateway) -> frozenset[str]: + entries: Final = candidate.get("/model/info")["data"] + assert isinstance(entries, list), entries + return frozenset(string_value(object_value(entry)["model_name"]) for entry in entries) + + +def test_pathless_replace_renames_the_team_and_keeps_the_resource_under_scim_data(gateway: Gateway) -> None: + renamed: Final = f"okta-renamed-{uuid.uuid4().hex}" + external: Final = f"ext-{uuid.uuid4().hex}" + with gateway.scenario() as scenario: + team: Final = scenario.team() + response: Final = patch_group( + gateway, team, [pathless("replace", {"id": team, "displayName": renamed, "externalId": external})] + ) + assert response.status_code == 200, response.text + assert response.json()["displayName"] == renamed, response.text + assert team_alias(gateway, team) == renamed + metadata: Final = team_metadata(gateway, team) + assert set(metadata) == {"externalId", "scim_data", "scim_managed"}, metadata + assert metadata["externalId"] == external and metadata["scim_managed"] is True, metadata + assert scim_data(metadata) == {"id": team, "displayName": renamed, "externalId": external}, metadata + + +def test_pathless_replace_with_members_is_an_absolute_roster(gateway: Gateway) -> None: + renamed: Final = f"roster-{uuid.uuid4().hex}" + with gateway.scenario() as scenario: + first: Final = scenario.user(user_role="internal_user") + second: Final = scenario.user(user_role="internal_user") + team: Final = scim_group(scenario, [first]) + assert member_ids(gateway, team) == {first} + response: Final = patch_group( + gateway, team, [pathless("replace", {"displayName": renamed, "members": [{"value": second}]})] + ) + assert response.status_code == 200, response.text + assert member_ids(gateway, team) == {second} + assert group_member_ids(gateway, team) == {second} + assert team_alias(gateway, team) == renamed + metadata: Final = team_metadata(gateway, team) + assert "" not in metadata, metadata + assert "members" not in scim_data(metadata), metadata + + +def test_pathless_add_applies_the_attribute(gateway: Gateway) -> None: + external: Final = f"ext-{uuid.uuid4().hex}" + with gateway.scenario() as scenario: + team: Final = scenario.team() + alias: Final = team_alias(gateway, team) + response: Final = patch_group(gateway, team, [pathless("add", {"externalId": external})]) + assert response.status_code == 200, response.text + metadata: Final = team_metadata(gateway, team) + assert "" not in metadata, metadata + assert metadata["externalId"] == external, metadata + assert scim_data(metadata) == {"externalId": external}, metadata + assert team_alias(gateway, team) == alias + + +def test_pathed_operations_are_unchanged(gateway: Gateway) -> None: + renamed: Final = f"pathed-{uuid.uuid4().hex}" + external: Final = f"ext-{uuid.uuid4().hex}" + with gateway.scenario() as scenario: + first: Final = scenario.user(user_role="internal_user") + second: Final = scenario.user(user_role="internal_user") + team: Final = scim_group(scenario, [first]) + response: Final = patch_group( + gateway, + team, + [ + pathed("replace", "displayName", renamed), + pathed("replace", "externalId", external), + pathed("add", "members", [{"value": second}]), + pathed("remove", f'members[value eq "{first}"]'), + ], + ) + assert response.status_code == 200, response.text + assert response.json()["displayName"] == renamed, response.text + assert team_alias(gateway, team) == renamed + assert member_ids(gateway, team) == {second} + assert group_member_ids(gateway, team) == {second} + metadata: Final = team_metadata(gateway, team) + assert metadata["externalId"] == external, metadata + assert "" not in metadata, metadata + + +def test_pathless_patch_merges_into_the_put_snapshot(gateway: Gateway) -> None: + put_alias: Final = f"put-{uuid.uuid4().hex}" + with gateway.scenario() as scenario: + member: Final = scenario.user(user_role="internal_user") + team: Final = scim_group(scenario, [member]) + put: Final = gateway.request( + "PUT", + f"/scim/v2/Groups/{team}", + { + "schemas": [GROUP_SCHEMA], + "id": team, + "displayName": put_alias, + "externalId": "ext-v1", + "members": [{"value": member}], + }, + ) + assert put.status_code == 200, put.text + assert scim_data(team_metadata(gateway, team))["externalId"] == "ext-v1" + response: Final = patch_group(gateway, team, [pathless("add", {"externalId": "ext-v2"})]) + assert response.status_code == 200, response.text + metadata: Final = team_metadata(gateway, team) + snapshot: Final = scim_data(metadata) + assert "" not in metadata, metadata + assert metadata["externalId"] == "ext-v2", metadata + assert snapshot["displayName"] == put_alias and snapshot["externalId"] == "ext-v2", snapshot + assert team_alias(gateway, team) == put_alias + assert member_ids(gateway, team) == {member} + + +def test_group_patch_drops_the_empty_key_left_by_an_earlier_push(gateway: Gateway) -> None: + with gateway.scenario() as scenario: + team: Final = scenario.team() + gateway.post("/team/update", {"team_id": team, "metadata": {"": {"displayName": "stale"}, "env": "staging"}}) + assert "" in team_metadata(gateway, team) + response: Final = patch_group(gateway, team, [pathed("replace", "externalId", "ext-after")]) + assert response.status_code == 200, response.text + metadata: Final = team_metadata(gateway, team) + assert "" not in metadata, metadata + assert metadata["env"] == "staging" and metadata["externalId"] == "ext-after", metadata + + +def test_later_path_op_wins_over_the_pathless_value(gateway: Gateway) -> None: + first: Final = f"first-{uuid.uuid4().hex}" + second: Final = f"second-{uuid.uuid4().hex}" + with gateway.scenario() as scenario: + team: Final = scenario.team() + response: Final = patch_group( + gateway, team, [pathless("replace", {"displayName": first}), pathed("replace", "displayName", second)] + ) + assert response.status_code == 200, response.text + assert response.json()["displayName"] == second, response.text + assert team_alias(gateway, team) == second + metadata: Final = team_metadata(gateway, team) + assert "" not in metadata, metadata + assert scim_data(metadata)["displayName"] == second, metadata + + +def test_read_only_attributes_do_not_become_metadata_keys(gateway: Gateway) -> None: + renamed: Final = f"readonly-{uuid.uuid4().hex}" + with gateway.scenario() as scenario: + team: Final = scenario.team() + response: Final = patch_group( + gateway, + team, + [ + pathless( + "replace", + { + "id": team, + "schemas": [GROUP_SCHEMA], + "meta": {"resourceType": "Group"}, + "displayName": renamed, + }, + ) + ], + ) + assert response.status_code == 200, response.text + metadata: Final = team_metadata(gateway, team) + assert set(metadata) == {"scim_data", "scim_managed"}, metadata + assert team_alias(gateway, team) == renamed + + +def test_pathless_rename_is_visible_from_the_peer_proxy(gateway: Gateway, peer: Gateway) -> None: + renamed: Final = f"peer-{uuid.uuid4().hex}" + with gateway.scenario() as scenario: + team: Final = scenario.team() + response: Final = patch_group(gateway, team, [pathless("replace", {"displayName": renamed})]) + assert response.status_code == 200, response.text + group: Final = eventually( + lambda: peer.get(f"/scim/v2/Groups/{team}"), lambda observed: observed.get("displayName") == renamed + ) + assert group["displayName"] == renamed, group + assert team_alias(peer, team) == renamed + assert "" not in team_metadata(peer, team) + + +def test_pathless_remove_is_rejected(gateway: Gateway) -> None: + with gateway.scenario() as scenario: + team: Final = scenario.team() + before: Final = alias_and_metadata(gateway, team) + response: Final = patch_group(gateway, team, [pathless("remove", {"externalId": "ext-gone"})]) + assert response.status_code == 400, response.text + assert "RFC 7644 Section 3.5.2.2" in response.text, response.text + assert alias_and_metadata(gateway, team) == before + + +@pytest.mark.parametrize( + "operation", + [ + {"op": "replace", "value": "new-name"}, + {"op": "replace", "value": 7}, + {"op": "replace", "value": ["new-name"]}, + {"op": "replace", "value": ""}, + {"op": "replace", "value": "x" * 5120}, + {"op": "replace", "value": None}, + {"op": "replace"}, + {"op": "add", "value": "new-name"}, + ], + ids=["string", "int", "list", "empty-string", "5kb-string", "null", "missing", "add-string"], +) +def test_pathless_op_without_an_object_value_is_rejected(gateway: Gateway, operation: dict[str, JsonValue]) -> None: + with gateway.scenario() as scenario: + team: Final = scenario.team() + before: Final = alias_and_metadata(gateway, team) + response: Final = patch_group(gateway, team, [operation]) + assert response.status_code == 400, response.text + assert "RFC 7644 Section 3.5.2" in response.text, response.text + assert alias_and_metadata(gateway, team) == before + + +def test_pathless_empty_object_changes_nothing_but_marks_the_team(gateway: Gateway) -> None: + with gateway.scenario() as scenario: + team: Final = scenario.team() + alias: Final = team_alias(gateway, team) + response: Final = patch_group(gateway, team, [pathless("replace", {})]) + assert response.status_code == 200, response.text + assert team_alias(gateway, team) == alias + metadata: Final = team_metadata(gateway, team) + assert metadata == {"scim_managed": True, "scim_data": {}}, metadata + + +def test_duplicate_pathless_ops_are_idempotent(gateway: Gateway) -> None: + external: Final = f"ext-{uuid.uuid4().hex}" + with gateway.scenario() as scenario: + team: Final = scenario.team() + response: Final = patch_group(gateway, team, [pathless("add", {"externalId": external})] * 2) + assert response.status_code == 200, response.text + metadata: Final = team_metadata(gateway, team) + assert metadata == {"scim_managed": True, "externalId": external, "scim_data": {"externalId": external}}, ( + metadata + ) + + +def test_unauthenticated_patch_changes_nothing(gateway: Gateway) -> None: + with gateway.scenario() as scenario: + team: Final = scenario.team() + before: Final = alias_and_metadata(gateway, team) + response: Final = patch_group( + gateway, team, [pathless("replace", {"displayName": "intruder"})], key=f"sk-{uuid.uuid4().hex}" + ) + assert response.status_code == 401, response.text + assert alias_and_metadata(gateway, team) == before + + +def test_unknown_group_is_404(gateway: Gateway) -> None: + response: Final = patch_group( + gateway, f"missing-{uuid.uuid4().hex}", [pathless("replace", {"displayName": "ghost"})] + ) + assert response.status_code == 404, response.text + + +def test_malformed_patch_body_is_rejected(gateway: Gateway) -> None: + with gateway.scenario() as scenario: + team: Final = scenario.team() + before: Final = alias_and_metadata(gateway, team) + response: Final = gateway.request( + "PATCH", f"/scim/v2/Groups/{team}", {"schemas": [PATCH_OP_SCHEMA], "Operations": "nope"} + ) + assert response.status_code in (400, 422), response.text + assert alias_and_metadata(gateway, team) == before + + +def test_pathless_and_pathed_scalar_coercion_agree(gateway: Gateway) -> None: + with gateway.scenario() as scenario: + via_pathless: Final = scenario.team() + via_path: Final = scenario.team() + first: Final = patch_group(gateway, via_pathless, [pathless("replace", {"displayName": 7})]) + second: Final = patch_group(gateway, via_path, [pathed("replace", "displayName", 7)]) + assert first.status_code == second.status_code == 200, (first.text, second.text) + assert team_alias(gateway, via_pathless) == team_alias(gateway, via_path), (first.text, second.text) + assert "" not in team_metadata(gateway, via_pathless) + + +def _patched_state( + candidate: Gateway, team: str, operations: Sequence[JsonValue] +) -> tuple[JsonValue, dict[str, JsonValue]]: + response: Final = patch_group(candidate, team, operations) + assert response.status_code == 200, response.text + return alias_and_metadata(candidate, team) + + +def test_repeated_pathless_patch_is_idempotent(gateway: Gateway) -> None: + renamed: Final = f"repeat-{uuid.uuid4().hex}" + with gateway.scenario() as scenario: + team: Final = scenario.team() + operations: Final = [pathless("replace", {"displayName": renamed, "externalId": "ext-repeat"})] + states: Final = tuple(_patched_state(gateway, team, operations) for _ in range(3)) + assert all(state == states[0] for state in states), states + assert states[0][0] == renamed, states + assert "" not in states[0][1], states + + +def test_concurrent_pathless_renames_converge(gateway: Gateway, peer: Gateway) -> None: + with gateway.scenario() as scenario: + team: Final = scenario.team() + aliases: Final = tuple(f"race-{index}-{uuid.uuid4().hex}" for index in range(10)) + candidates: Final = (gateway, peer) + with ThreadPoolExecutor(max_workers=len(aliases)) as pool: + responses: Final = tuple( + pool.map( + lambda indexed: patch_group( + candidates[indexed[0] % 2], team, [pathless("replace", {"displayName": indexed[1]})] + ), + enumerate(aliases), + ) + ) + assert all(response.status_code == 200 for response in responses), [ + response.text for response in responses if response.status_code != 200 + ] + alias: Final = team_alias(gateway, team) + assert alias in aliases, alias + metadata: Final = team_metadata(gateway, team) + assert "" not in metadata, metadata + assert scim_data(metadata)["displayName"] == alias, metadata + + +def test_team_key_keeps_serving_after_the_rename(gateway: Gateway, peer: Gateway) -> None: + with gateway.scenario() as scenario: + model: Final = scenario.model() + team: Final = scenario.team() + key: Final = scenario.key(team_id=team, models=[model]) + response: Final = patch_group( + gateway, team, [pathless("replace", {"displayName": f"serving-{uuid.uuid4().hex}"})] + ) + assert response.status_code == 200, response.text + eventually(lambda: model_names(peer), lambda names: model in names, seconds=60) + for candidate, marker in ((gateway, f"primary-{uuid.uuid4().hex}"), (peer, f"peer-{uuid.uuid4().hex}")): + completion: Final = candidate.chat(model, key=key, text=marker) + assert completion["choices"], completion + assert upstream_hits(gateway, marker) == 1, marker + + +@dataclass(frozen=True, slots=True) +class _Attempt: + team: str + alias: str + response: httpx.Response | None + + +class _KillSwitch: + def __init__(self, after: int, action: Callable[[], None]) -> None: + self._after: Final = after + self._action: Final = action + self._lock: Final = threading.Lock() + self._responses = 0 + + def tick(self) -> None: + with self._lock: + self._responses += 1 + if self._responses == self._after: + self._action() + + +def _burst_operations(alias: str, index: int) -> Sequence[JsonValue]: + shapes: Final = ( + [pathless("replace", {"displayName": alias, "externalId": f"ext-{index}"})], + [pathless("add", {"displayName": alias})], + [pathed("replace", "displayName", alias)], + ) + return shapes[index % len(shapes)] + + +def _attempt(candidate: Gateway, team: str, index: int, switch: _KillSwitch) -> _Attempt: + alias: Final = f"burst-{index}-{uuid.uuid4().hex}" + try: + return _Attempt(team, alias, patch_group(candidate, team, _burst_operations(alias, index))) + except httpx.TransportError: + return _Attempt(team, alias, None) + finally: + switch.tick() + + +def _team_attempts(candidate: Gateway, team: str, offset: int, switch: _KillSwitch) -> tuple[_Attempt, ...]: + return tuple(_attempt(candidate, team, offset + index, switch) for index in range(BURST_REQUESTS_PER_TEAM)) + + +def _burst(candidate: Gateway, teams: Sequence[str], disruption: Callable[[], None]) -> tuple[_Attempt, ...]: + switch: Final = _KillSwitch(KILL_AFTER_RESPONSES, disruption) + with ThreadPoolExecutor(max_workers=len(teams)) as pool: + per_team: Final = tuple( + pool.submit(_team_attempts, candidate, team, index * BURST_REQUESTS_PER_TEAM, switch) + for index, team in enumerate(teams) + ) + return tuple(chain.from_iterable(future.result() for future in per_team)) + + +def _burst_teams(scenario: Scenario) -> Mapping[str, str]: + origins: Final = tuple(f"origin-{uuid.uuid4().hex}" for _ in range(BURST_TEAMS)) + return MappingProxyType({scenario.team(team_alias=origin): origin for origin in origins}) + + +def _assert_team_reflected(candidate: Gateway, team: str, origin: str, sent: Sequence[_Attempt]) -> None: + aliases: Final = frozenset(attempt.alias for attempt in sent) + alias, metadata = alias_and_metadata(candidate, team) + assert "" not in metadata, metadata + if alias == origin: + assert all(attempt.response is None for attempt in sent), (team, sent) + assert "scim_managed" not in metadata, metadata + return + assert alias in aliases, (alias, aliases) + assert metadata["scim_managed"] is True, metadata + if sent[-1].response is not None: + assert alias == sent[-1].alias, (alias, sent[-1].alias) + snapshot: Final = metadata.get("scim_data") + if snapshot is not None: + assert object_value(snapshot).get("displayName") in aliases, snapshot + + +def _assert_burst_reflected(candidate: Gateway, teams: Mapping[str, str], attempts: Sequence[_Attempt]) -> None: + answered: Final = tuple(attempt for attempt in attempts if attempt.response is not None) + assert answered, "The whole burst failed to reach the proxy" + for attempt in answered: + assert attempt.response is not None and attempt.response.status_code == 200, attempt.response + assert attempt.response.json()["displayName"] == attempt.alias, attempt.response.text + for team, origin in teams.items(): + _assert_team_reflected(candidate, team, origin, tuple(attempt for attempt in attempts if attempt.team == team)) + + +def _patch_status(candidate: Gateway, team: str) -> int | None: + operations: Final = [pathless("replace", {"displayName": f"probe-{uuid.uuid4().hex}"})] + try: + return patch_group(candidate, team, operations).status_code + except httpx.TransportError: + return None + + +def _serving_workers(root: int) -> tuple[psutil.Process, ...]: + return tuple(child for child in psutil.Process(root).children() if child.children()) + + +@pytest.mark.timeout(240) +def test_worker_kill_mid_burst_keeps_serving_and_leaves_no_empty_key(gateway: Gateway, tmp_path: Path) -> None: + with owned_proxy_process(gateway, tmp_path, {}, workers=2) as owned, gateway.scenario() as scenario: + teams: Final = _burst_teams(scenario) + workers: Final = eventually( + lambda: _serving_workers(owned.process.pid), lambda children: len(children) >= 2, seconds=30 + ) + attempts: Final = _burst(owned.gateway, tuple(teams), lambda: os.kill(workers[0].pid, signal.SIGKILL)) + assert not workers[0].is_running() or workers[0].status() == psutil.STATUS_ZOMBIE, workers[0] + probe: Final = scenario.team() + eventually(lambda: _patch_status(owned.gateway, probe), lambda status_code: status_code == 200, seconds=60) + _assert_burst_reflected(owned.gateway, teams, attempts) + + +@pytest.mark.timeout(300) +def test_rolling_restart_mid_burst_drains_without_empty_keys(gateway: Gateway, tmp_path: Path) -> None: + with gateway.scenario() as scenario, owned_proxy_process(gateway, tmp_path, {}, workers=2) as replacement: + teams: Final = _burst_teams(scenario) + with owned_proxy_process(gateway, tmp_path, {}, workers=2) as retiring: + attempts: Final = _burst(retiring.gateway, tuple(teams), retiring.process.terminate) + _assert_burst_reflected(replacement.gateway, teams, attempts) diff --git a/tests/integration/management/test_tool_policy_user.py b/tests/integration/management/test_tool_policy_user.py index 00b4605ee2e..b93fe190e28 100644 --- a/tests/integration/management/test_tool_policy_user.py +++ b/tests/integration/management/test_tool_policy_user.py @@ -462,7 +462,13 @@ def test_owner_lookup_failure_keeps_tools_listed_without_a_user(gateway: Gateway config: Final = _proxy_config(tmp_path, model, gateway.upstream_url, {}) with ( scratch_database() as database_url, - owned_proxy(gateway, tmp_path, {"DATABASE_URL": database_url}, config=config) as candidate, + owned_proxy( + gateway, + tmp_path, + {"DATABASE_URL": database_url}, + config=config, + remove_environment=("DATABASE_URL_READ_REPLICA",), + ) as candidate, ): alias: Final = "fault-owner-" + uuid.uuid4().hex user: Final = string_value( diff --git a/tests/integration/mcp/test_mcp_access_matrix.py b/tests/integration/mcp/test_mcp_access_matrix.py index 13759ce245c..e458911cc68 100644 --- a/tests/integration/mcp/test_mcp_access_matrix.py +++ b/tests/integration/mcp/test_mcp_access_matrix.py @@ -3,6 +3,7 @@ from typing import Final import pytest from integration._support.client import Gateway +from integration._support.database import read_rows from integration._support.mcp import ( ENTRY_POINTS, EntryPoint, @@ -27,6 +28,16 @@ def _name(entry: EntryPoint, alias: str, tool: str) -> str: return tool if entry == "rest" else f"{alias}-{tool}" +def _open_aliases() -> frozenset[str]: + rows: Final = read_rows('SELECT alias FROM "LiteLLM_MCPServerTable" WHERE allow_all_keys', ()) + return frozenset(str(row["alias"]) for row in rows) + + +def _without_foreign_open_servers(tools: tuple[str, ...], open_aliases: frozenset[str]) -> set[str]: + prefixes: Final = tuple(f"{alias}-" for alias in open_aliases) + return {tool for tool in tools if not tool.startswith(prefixes)} + + def _assert_denied(caller: McpCaller, peer: McpPeer, name: str, identity: str, entry: EntryPoint) -> None: peer.drain() outcome: Final = caller.call(name, CALLABLE["add"], _server_scoped(entry, identity)) @@ -51,14 +62,16 @@ def test_subject_grant_lists_only_reachable_tools_and_denies_the_rest( scenario, subject, (granted,), (granted, denied), access_group=group, allowed_tools={granted: ("add",)} ) reach: Final = McpCaller(gateway, caller.key, entry, granted_alias, caller.headers) + open_before: Final = _open_aliases() listed: Final = reach.list_tools(_server_scoped(entry, granted)) assert listed.ok, listed.raw + open_aliases: Final = open_before | _open_aliases() expected: Final = ( {_name(entry, granted_alias, "add")} if subject in ("toolset", "allowed_tools") else {_name(entry, granted_alias, tool) for tool in ("add", "multiply", "fail")} ) - assert set(listed.tools) == expected, listed.tools + assert _without_foreign_open_servers(listed.tools, open_aliases) == expected, listed.tools for tool, arguments in CALLABLE.items(): name: Final = _name(entry, granted_alias, tool) if name not in listed.tools: diff --git a/tests/integration/mcp/test_mcp_management.py b/tests/integration/mcp/test_mcp_management.py index 67cdbbff5a4..bef882eae31 100644 --- a/tests/integration/mcp/test_mcp_management.py +++ b/tests/integration/mcp/test_mcp_management.py @@ -390,3 +390,97 @@ def test_ui_session_lists_and_fetches_team_granted_config_server( assert detail.status_code == 200, f"Team-granted server detail access should succeed: {detail.text}" assert detail.json()["server_id"] == server_id, detail.text assert detail.json()["alias"] == alias, detail.text + + +@pytest.mark.parametrize("explicit_transport", [False, True]) +def test_modern_sse_registration_rejected_without_saving(gateway: Gateway, explicit_transport: bool) -> None: + identity: Final = str(uuid.uuid4()) + with mcp_peer() as peer: + response: Final = gateway.request("POST", "/v1/mcp/server", { + "server_id": identity, "server_name": "invalid" + uuid.uuid4().hex[:8], + "url": peer.url, "mcp_info": {"protocol_version": "2026-07-28"}, + **({"transport": "sse"} if explicit_transport else {}), + }) + try: + assert response.status_code == 422, response.text + assert "Modern MCP requires HTTP or stdio" in response.text + assert identity not in _servers(gateway) + assert peer.drain() == (), "Rejected configuration reached upstream" + finally: + if response.status_code == 201: + delete_mcp(gateway, identity) + + +def test_protocol_transport_updates_validate_effective_configuration(gateway: Gateway) -> None: + from integration._support.database import read_rows + + with mcp_peer() as peer, gateway.scenario() as scenario: + identity: Final = register_mcp(scenario, peer, "protocol" + uuid.uuid4().hex[:8]) + modern: Final = gateway.request("PUT", "/v1/mcp/server", { + "server_id": identity, "mcp_info": {"protocol_version": "2026-07-28"}, + }) + assert modern.status_code == 202, modern.text + assert modern.json()["transport"] == "http" + changed: Final = gateway.request("PUT", "/v1/mcp/server", {"server_id": identity, "description": "renamed"}) + assert changed.status_code == 202, changed.text + assert changed.json()["mcp_info"]["protocol_version"] == "2026-07-28" + snapshot: Final = read_rows('SELECT transport, mcp_info, updated_at::text FROM "LiteLLM_MCPServerTable" WHERE server_id=%s', (identity,)) + peer.drain() + rejected: Final = gateway.request("PUT", "/v1/mcp/server", {"server_id": identity, "transport": "sse", "url": peer.url}) + assert rejected.status_code == 400, rejected.text + assert "Modern MCP requires HTTP or stdio" in rejected.text + assert read_rows('SELECT transport, mcp_info, updated_at::text FROM "LiteLLM_MCPServerTable" WHERE server_id=%s', (identity,)) == snapshot + assert tool_calls(peer.drain()) == () + key: Final = scenario.key(object_permission={"mcp_servers": [identity]}) + assert call_tool(gateway, key, identity, "add", ADD).status_code == 200 + + legacy: Final = gateway.request("PUT", "/v1/mcp/server", {"server_id": identity, "transport": "sse", "url": peer.url, "mcp_info": {}}) + assert legacy.status_code == 202, legacy.text + legacy_snapshot: Final = read_rows('SELECT transport, mcp_info, updated_at::text FROM "LiteLLM_MCPServerTable" WHERE server_id=%s', (identity,)) + rejected_protocol: Final = gateway.request("PUT", "/v1/mcp/server", {"server_id": identity, "mcp_info": {"protocol_version": "2026-07-28"}}) + assert rejected_protocol.status_code == 400, rejected_protocol.text + assert read_rows('SELECT transport, mcp_info, updated_at::text FROM "LiteLLM_MCPServerTable" WHERE server_id=%s', (identity,)) == legacy_snapshot + repaired: Final = gateway.request("PUT", "/v1/mcp/server", {"server_id": identity, "transport": "http", "url": peer.url, "mcp_info": {"protocol_version": "2026-07-28"}}) + assert repaired.status_code == 202, repaired.text + assert call_tool(gateway, key, identity, "add", ADD).status_code == 200 + + +@pytest.mark.parametrize("rename", [False, True]) +def test_concurrent_protocol_transport_edits_cannot_save_incompatible_configuration( + gateway: Gateway, peer: Gateway, rename: bool +) -> None: + import os + from concurrent.futures import ThreadPoolExecutor + + import psycopg + from integration._support.database import read_rows + + with mcp_peer() as upstream, gateway.scenario() as scenario: + identity: Final = register_mcp(scenario, upstream, "race" + uuid.uuid4().hex[:8]) + with ThreadPoolExecutor(max_workers=2) as pool: + # Hold the row so both workers read the old configuration before either can write. + with psycopg.connect(os.environ["DATABASE_URL"]) as blocker: + blocker.execute('SELECT server_id FROM "LiteLLM_MCPServerTable" WHERE server_id=%s FOR UPDATE', (identity,)) + protocol = pool.submit(gateway.request, "PUT", "/v1/mcp/server", { + "server_id": identity, "mcp_info": {"protocol_version": "2026-07-28"}, + **({"alias": "renamed" + uuid.uuid4().hex[:8]} if rename else {}), + }) + transport = pool.submit(peer.request, "PUT", "/v1/mcp/server", { + "server_id": identity, "transport": "sse", "url": upstream.url, + }) + eventually( + lambda: read_rows("SELECT pid FROM pg_stat_activity WHERE datname=current_database() AND wait_event_type='Lock' AND query LIKE %s", ('%LiteLLM_MCPServerTable%',)), + lambda rows: len(rows) >= 2, + seconds=3, + ) + responses: Final = [protocol.result(), transport.result()] + assert sorted(response.status_code for response in responses) == [202, 400], [r.text for r in responses] + saved: Final = read_rows('SELECT transport, mcp_info FROM "LiteLLM_MCPServerTable" WHERE server_id=%s', (identity,))[0] + assert saved["transport"] == "http" or saved["mcp_info"] != {"protocol_version": "2026-07-28"} + repaired: Final = gateway.request("PUT", "/v1/mcp/server", { + "server_id": identity, "transport": "http", "url": upstream.url, + "mcp_info": {"protocol_version": "2026-07-28"}, + }) + assert repaired.status_code == 202, repaired.text + key: Final = scenario.key(object_permission={"mcp_servers": [identity]}) + assert call_tool(gateway, key, identity, "add", ADD).status_code == 200 diff --git a/tests/integration/mcp/test_mcp_transports.py b/tests/integration/mcp/test_mcp_transports.py index 16004cdd501..b448b8b4a64 100644 --- a/tests/integration/mcp/test_mcp_transports.py +++ b/tests/integration/mcp/test_mcp_transports.py @@ -195,3 +195,52 @@ def test_pinned_revision_pairs_list_and_call_through_gateway( assert negotiations, "The operation must reach the upstream negotiation" assert all(request["params"]["protocolVersion"] == upstream for request in negotiations), negotiations assert len(tool_calls(observed)) == 1 + + +@pytest.mark.parametrize("downstream", ("2024-11-05", "2025-03-26", "2025-06-18", "2025-11-25")) +@pytest.mark.parametrize("peer_kind", ("http", "stdio")) +def test_legacy_gateway_calls_modern_upstream_without_initialize( + gateway: Gateway, + downstream: str, + peer_kind: PeerKind, +) -> None: + import asyncio + + from mcp.types import CallToolRequestParams + + from litellm.experimental_mcp_client.client import MCPClient + from litellm.types.mcp import MCPTransport + + with peer_of(peer_kind) as peer, gateway.scenario() as scenario: + alias: Final = "modern" + uuid.uuid4().hex[:8] + identity: Final = register_mcp(scenario, peer, alias, mcp_info={"protocol_version": "2026-07-28"}) + key: Final = scenario.key(object_permission={"mcp_servers": [identity]}) + client: Final = MCPClient( + server_url=str(gateway.client.base_url).rstrip("/") + "/mcp", + transport_type=MCPTransport.http, + protocol_version=downstream, + extra_headers={"Authorization": f"Bearer {key}"}, + ) + + async def exercise() -> None: + listed: Final = await client.list_tools(raise_on_error=True) + assert f"{alias}-add" in tuple(tool.name for tool in listed) + called: Final = await client.call_tool( + CallToolRequestParams(name=f"{alias}-add", arguments={"a": 2, "b": 3}), + raise_on_error=True, + ) + assert called.is_error is False + assert called.content[0].text == "5" + + peer.drain() + asyncio.run(exercise()) + observed: Final = peer.drain() + assert len(tool_calls(observed)) == 1 + assert all(row["body"].get("method") not in ("initialize", "notifications/initialized") for row in observed) + requests: Final = tuple(row for row in observed if "id" in row["body"]) + assert requests + for row in requests: + metadata: Final = row["body"]["params"]["_meta"] + assert metadata["io.modelcontextprotocol/protocolVersion"] == "2026-07-28" + assert "io.modelcontextprotocol/clientCapabilities" in metadata + assert b"mcp-session-id" not in row.get("headers", {}) diff --git a/tests/integration/mcp/test_responses_mcp_mixed_tools.py b/tests/integration/mcp/test_responses_mcp_mixed_tools.py index 985a8b8a65e..91119f1c258 100644 --- a/tests/integration/mcp/test_responses_mcp_mixed_tools.py +++ b/tests/integration/mcp/test_responses_mcp_mixed_tools.py @@ -21,6 +21,8 @@ def test_responses_with_gateway_mcp_and_caller_function_tool_hands_both_to_model upstream_tools: list[tuple[str, ...]] = [] def respond(request: Request) -> Reply: + if request.method == "GET" and request.target.endswith("/models"): + return Reply(body=b'{"object":"list","data":[]}') assert request.target.endswith("/responses"), request.target body: Final = json.loads(request.body) upstream_tools.append(tuple(str(tool.get("name")) for tool in body.get("tools", ()))) diff --git a/tests/integration/observability/test_otel_excluded_services.py b/tests/integration/observability/test_otel_excluded_services.py index 48b20651b97..8768052b488 100644 --- a/tests/integration/observability/test_otel_excluded_services.py +++ b/tests/integration/observability/test_otel_excluded_services.py @@ -124,6 +124,20 @@ def _trace_spans(sink_url: str, trace_id: str, seconds: float = 30) -> tuple[Spa return group +def _trace_spans_when( + sink_url: str, + trace_id: str, + ready: Callable[[tuple[Span, ...]], bool], + seconds: float = 30, +) -> tuple[Span, ...]: + spans: Final = eventually( + lambda: spans_for_trace(recorded_spans(sink_url)[1], trace_id), + ready, + seconds=seconds, + ) + return spans + + def _await_db_span(sink_url: str, trace_id: str | None, needle: str, seconds: float = 40, since: int = 0) -> None: def seen() -> bool: _, spans = recorded_spans(sink_url, since) @@ -242,6 +256,196 @@ def test_without_excluded_services_the_tenant_still_gets_redis_and_postgres_span assert {"redis", "postgresql"} <= systems, f"datastore spans missing at tenant: {systems}" +@pytest.mark.parametrize("otel", [None, True, "on", "", []], ids=["null", "true", "on", "empty_string", "empty_list"]) +@pytest.mark.timeout(180) +def test_a_non_mapping_otel_block_still_publishes_the_tenant_fan_out( + gateway: Gateway, + audit_sinks: SpanSinks, + otel_audit_config: AuditConfigWriter, + langfuse_vars: dict[str, JsonValue], + tmp_path: Path, + otel: JsonValue, +) -> None: + def with_callback_settings(config: dict) -> None: + config["litellm_settings"]["callbacks"] = ["langfuse_otel"] + config["callback_settings"]["otel"] = otel + + config: Final = _config_with(tmp_path, otel_audit_config, extra=with_callback_settings) + overrides: Final = {"LITELLM_OTEL_V2": "1", **_operator_langfuse(audit_sinks)} + with owned_proxy(gateway, tmp_path, overrides, config=config, workers=2) as candidate: + traffic: Final = _drive(candidate, langfuse_vars) + tenant_trace: Final = _trace_id(audit_sinks.tenant, traffic) + _await_db_span(audit_sinks.tenant, tenant_trace, "redis") + tenant_spans: Final = _trace_spans_when( + audit_sinks.tenant, + tenant_trace, + lambda spans: any(span["kind"] == 2 for span in spans) and "redis" in _db_systems(spans), + seconds=15, + ) + assert any(span["kind"] == 2 for span in tenant_spans), "tenant SERVER root span missing" + assert "redis" in _db_systems(tenant_spans), f"tenant redis span missing: {_db_systems(tenant_spans)}" + operator_trace: Final = _trace_id(audit_sinks.operator, traffic) + operator_spans: Final = _trace_spans_when( + audit_sinks.operator, + operator_trace, + lambda spans: any(span["kind"] == 2 for span in spans), + seconds=15, + ) + assert any(span["kind"] == 2 for span in operator_spans), "operator SERVER root span missing" + + +@pytest.mark.parametrize("name", ["EXCLUDED_SERVICES", "excluded_services"]) +@pytest.mark.timeout(180) +def test_a_bare_excluded_services_env_var_is_ignored( + gateway: Gateway, + audit_sinks: SpanSinks, + otel_audit_config: AuditConfigWriter, + langfuse_vars: dict[str, JsonValue], + tmp_path: Path, + name: str, +) -> None: + config: Final = _config_with(tmp_path, otel_audit_config) + overrides: Final = {"LITELLM_OTEL_V2": "1", name: "redis,postgres"} + with owned_proxy(gateway, tmp_path, overrides, config=config, workers=2) as candidate: + tenant_start, _ = recorded_spans(audit_sinks.tenant) + traffic: Final = _drive(candidate, langfuse_vars) + tenant_trace: Final = _trace_id(audit_sinks.tenant, traffic) + _await_db_span(audit_sinks.tenant, tenant_trace, "redis") + tenant_spans: Final = _trace_spans_when( + audit_sinks.tenant, + tenant_trace, + lambda spans: "redis" in _db_systems(spans), + seconds=15, + ) + assert "redis" in _db_systems(tenant_spans), f"redis span missing at tenant: {_db_systems(tenant_spans)}" + _await_db_span(audit_sinks.tenant, None, "postgresql", since=tenant_start) + _, all_tenant = recorded_spans(audit_sinks.tenant, tenant_start) + systems: Final = _db_systems(all_tenant) + assert {"redis", "postgresql"} <= systems, f"datastore spans missing at tenant: {systems}" + + +@pytest.mark.timeout(180) +def test_the_documented_env_var_wins_over_a_bare_excluded_services( + gateway: Gateway, + audit_sinks: SpanSinks, + otel_audit_config: AuditConfigWriter, + langfuse_vars: dict[str, JsonValue], + tmp_path: Path, +) -> None: + config: Final = _config_with(tmp_path, otel_audit_config) + overrides: Final = { + "LITELLM_OTEL_V2": "1", + "LITELLM_OTEL_EXCLUDED_SERVICES": "redis", + "EXCLUDED_SERVICES": "postgres", + } + with owned_proxy(gateway, tmp_path, overrides, config=config, workers=2) as candidate: + tenant_start, _ = recorded_spans(audit_sinks.tenant) + traffic: Final = _drive(candidate, langfuse_vars) + tenant_trace: Final = _trace_id(audit_sinks.tenant, traffic) + _trace_spans(audit_sinks.tenant, tenant_trace, seconds=15) + _await_db_span(audit_sinks.tenant, None, "postgresql", since=tenant_start) + _, tenant_spans = recorded_spans(audit_sinks.tenant, tenant_start) + systems: Final = _db_systems(tenant_spans) + assert "postgresql" in systems, f"postgresql spans missing at tenant: {systems}" + assert "redis" not in systems, f"redis spans reached tenant: {systems}" + + +@pytest.mark.parametrize( + ("env_name", "redis_reaches_tenant"), + [ + pytest.param("LITELLM_OTEL_EXCLUDED_SERVICES", False, id="exact-case"), + pytest.param("litellm_otel_excluded_services", True, id="wrong-case"), + ], +) +@pytest.mark.timeout(180) +def test_case_sensitive_otel_settings_read_only_the_exact_env_name( + gateway: Gateway, + audit_sinks: SpanSinks, + otel_audit_config: AuditConfigWriter, + langfuse_vars: Mapping[str, JsonValue], + tmp_path: Path, + env_name: str, + redis_reaches_tenant: bool, +) -> None: + config: Final = _config_with(tmp_path, otel_audit_config, otel={"_case_sensitive": True}) + overrides: Final = {"LITELLM_OTEL_V2": "1", env_name: "redis"} + with owned_proxy( + gateway, + tmp_path, + overrides, + config=config, + remove_environment=("LITELLM_OTEL_EXCLUDED_SERVICES", "litellm_otel_excluded_services"), + workers=2, + ) as candidate: + tenant_start, _ = recorded_spans(audit_sinks.tenant) + traffic: Final = _drive(candidate, langfuse_vars) + tenant_trace: Final = _trace_id(audit_sinks.tenant, traffic) + if redis_reaches_tenant: + _await_db_span(audit_sinks.tenant, tenant_trace, "redis") + _trace_spans_when( + audit_sinks.tenant, + tenant_trace, + lambda spans: "redis" in _db_systems(spans), + seconds=15, + ) + else: + _trace_spans(audit_sinks.tenant, tenant_trace, seconds=15) + _await_db_span(audit_sinks.tenant, None, "postgresql", seconds=60, since=tenant_start) + _, tenant_spans = recorded_spans(audit_sinks.tenant, tenant_start) + systems: Final = _db_systems(tenant_spans) + assert "postgresql" in systems, f"postgresql spans missing at tenant: {systems}" + assert ("redis" in systems) is redis_reaches_tenant, ( + f"tenant redis presence={('redis' in systems)}; expected={redis_reaches_tenant}; systems={systems}" + ) + + +@pytest.mark.timeout(180) +def test_env_ignore_empty_keeps_the_default_service_name( + gateway: Gateway, + audit_sinks: SpanSinks, + otel_audit_config: AuditConfigWriter, + langfuse_vars: Mapping[str, JsonValue], + tmp_path: Path, +) -> None: + config: Final = _config_with(tmp_path, otel_audit_config, otel={"_env_ignore_empty": True}) + overrides: Final = {"LITELLM_OTEL_V2": "1", "OTEL_SERVICE_NAME": ""} + with owned_proxy(gateway, tmp_path, overrides, config=config, workers=2) as candidate: + traffic: Final = _drive(candidate, langfuse_vars) + operator_trace: Final = _trace_id(audit_sinks.operator, traffic) + operator_spans: Final = _trace_spans_when( + audit_sinks.operator, + operator_trace, + lambda spans: any(span["kind"] == 2 for span in spans), + seconds=15, + ) + service_names: Final = tuple(span["resource"].get("service.name") for span in operator_spans) + assert service_names and all(name == "litellm" for name in service_names), ( + f"operator service.name values={service_names}" + ) + + +@pytest.mark.timeout(180) +def test_env_parse_none_str_reads_a_null_traces_endpoint_as_unset( + gateway: Gateway, + audit_sinks: SpanSinks, + otel_audit_config: AuditConfigWriter, + langfuse_vars: Mapping[str, JsonValue], + tmp_path: Path, +) -> None: + config: Final = _config_with(tmp_path, otel_audit_config, otel={"_env_parse_none_str": "null"}) + overrides: Final = {"LITELLM_OTEL_V2": "1", "OTEL_TRACES_ENDPOINT": "null"} + with owned_proxy(gateway, tmp_path, overrides, config=config, workers=2) as candidate: + traffic: Final = _drive(candidate, langfuse_vars) + operator_trace: Final = _trace_id(audit_sinks.operator, traffic) + operator_spans: Final = _trace_spans_when( + audit_sinks.operator, + operator_trace, + lambda spans: any(span["kind"] == 2 for span in spans), + seconds=15, + ) + assert any(span["kind"] == 2 for span in operator_spans), "operator SERVER root span missing" + + def test_env_excluded_services_drops_only_redis( gateway: Gateway, audit_sinks: SpanSinks, diff --git a/tests/integration/observability/test_otel_excluded_services_matrix.py b/tests/integration/observability/test_otel_excluded_services_matrix.py index 0d4b5d087c6..7bca7ffc0dc 100644 --- a/tests/integration/observability/test_otel_excluded_services_matrix.py +++ b/tests/integration/observability/test_otel_excluded_services_matrix.py @@ -9,7 +9,8 @@ from concurrent.futures import ThreadPoolExecutor from contextlib import contextmanager from dataclasses import dataclass from pathlib import Path -from typing import Final, Literal +from types import MappingProxyType +from typing import Final, Literal, Protocol, cast import anthropic import httpx @@ -26,6 +27,9 @@ from pydantic import JsonValue, TypeAdapter MARKER: Final = re.compile(rb"excl-[0-9a-f]{32}") FAILING: Final = re.compile(rb"excl-fail-[0-9a-f]{32}") JSON: Final[TypeAdapter[JsonValue]] = TypeAdapter(JsonValue) +UNCONFIGURED_VARIANT: Final[TypeAdapter[Literal["null_block", "bare_env"]]] = TypeAdapter( + Literal["null_block", "bare_env"] +) REPLY_TEXT: Final = "excluded ok" SERVER: Final = 2 INVALID_NAME_LOG: Final = "is not a datastore service" @@ -37,6 +41,11 @@ CLIENTS: Final[tuple[Client, ...]] = ("raw", "sdk", "async_sdk") AuditConfigWriter = Callable[[Path, Mapping[str, JsonValue]], Path] +class _FixtureRequestParam(Protocol): + @property + def param(self) -> object: ... + + def _marker() -> str: return "excl-" + uuid.uuid4().hex @@ -339,6 +348,11 @@ def _db_systems(spans: tuple[Span, ...]) -> set[str]: } +def _post_auth_datastore_spans(spans: tuple[Span, ...]) -> tuple[Span, ...]: + auth_ids: Final = frozenset(span["span_id"] for span in spans if span["name"].startswith("auth ")) + return tuple(span for span in spans if _db_systems((span,)) and span["parent_span_id"] not in auth_ids) + + def _names(spans: tuple[Span, ...]) -> list[str]: return sorted(span["name"] for span in spans) @@ -408,6 +422,29 @@ def _config(directory: Path, otel_audit_config: AuditConfigWriter, otel: Mapping return path +def _null_otel_config(directory: Path, otel_audit_config: AuditConfigWriter, name: str) -> Path: + written: Final = otel_audit_config(directory, {}) + loaded: Final = object_value(JSON.validate_python(yaml.safe_load(written.read_text()))) + config: Final = { + **loaded, + "litellm_settings": {**object_value(loaded["litellm_settings"]), "callbacks": ["langfuse_otel"]}, + "callback_settings": {**object_value(loaded["callback_settings"]), "otel": None}, + } + path: Final = directory / f"{name}.yaml" + path.write_text(yaml.safe_dump(config)) + return path + + +def _operator_langfuse(sinks: SpanSinks) -> dict[str, str]: + return { + "LANGFUSE_HOST": sinks.operator, + "LANGFUSE_PUBLIC_KEY": "pk-lf-operator", + "LANGFUSE_SECRET_KEY": "sk-lf-operator", + "OTEL_EXPORTER": "http/json", + "OTEL_ENDPOINT": sinks.operator, + } + + @contextmanager def _started( provider: Wire, @@ -416,13 +453,14 @@ def _started( directory: Path, langfuse_vars: Mapping[str, JsonValue], workers: int, + environment: Mapping[str, str] = MappingProxyType({}), ) -> Generator[Rig]: with ( gateway_from_environment() as gateway, owned_proxy_process( gateway, directory, - {"LITELLM_OTEL_V2": "1", "OTEL_BSP_SCHEDULE_DELAY": "300"}, + {"LITELLM_OTEL_V2": "1", "OTEL_BSP_SCHEDULE_DELAY": "300", **environment}, config=config, remove_environment=("LITELLM_OTEL_EXCLUDED_SERVICES",), workers=workers, @@ -458,6 +496,32 @@ def rig( yield started +@pytest.fixture(scope="module", params=["null_block", "bare_env"], ids=["null_block", "bare_env"]) +def unconfigured_rig( + request: pytest.FixtureRequest, + provider: Wire, + audit_sinks: SpanSinks, + otel_audit_config: AuditConfigWriter, + langfuse_vars: dict[str, JsonValue], + tmp_path_factory: pytest.TempPathFactory, +) -> Iterator[Rig]: + parameter: Final = cast(_FixtureRequestParam, request).param + variant: Final = UNCONFIGURED_VARIANT.validate_python(parameter) + directory: Final = tmp_path_factory.mktemp(f"excluded-{variant}") + config: Final = ( + _null_otel_config(directory, otel_audit_config, variant) + if variant == "null_block" + else _config(directory, otel_audit_config, {}, variant) + ) + environment: Final = ( + _operator_langfuse(audit_sinks) if variant == "null_block" else {"EXCLUDED_SERVICES": "redis,postgres"} + ) + with _started( + provider, audit_sinks, config, directory, langfuse_vars, workers=2, environment=environment + ) as started: + yield started + + @pytest.mark.timeout(120) @pytest.mark.parametrize("stream", [False, True], ids=["unary", "stream"]) @pytest.mark.parametrize("client", CLIENTS) @@ -654,6 +718,237 @@ def test_killing_one_of_two_workers_mid_burst_keeps_the_filter_on_the_survivor(r _assert_withheld(rig, rig.raw("chat", _marker(), stream=False), after) +def _assert_tenant_kept( + rig: Rig, trace_id: str, cursors: Cursors, *, needs_model_span: bool = False +) -> tuple[Span, ...]: + def ready(spans: tuple[Span, ...]) -> bool: + return ( + sum(1 for span in spans if span["kind"] == SERVER) == 1 + and "redis" in _db_systems(spans) + and (not needs_model_span or any("gen_ai.operation.name" in span["attributes"] for span in spans)) + ) + + tenant: Final = eventually( + lambda: spans_for_trace(recorded_spans(rig.sinks.tenant, cursors.tenant)[1], trace_id), + ready, + seconds=40, + return_last_on_timeout=True, + ) + assert sum(1 for span in tenant if span["kind"] == SERVER) == 1, _names(tenant) + assert "redis" in _db_systems(tenant), f"redis spans missing at the tenant: {_names(tenant)}" + assert not needs_model_span or any("gen_ai.operation.name" in span["attributes"] for span in tenant), _names(tenant) + return tenant + + +def _assert_kept(rig: Rig, sent: Sent, cursors: Cursors) -> tuple[Span, ...]: + operator: Final = _operator_trace(rig, sent, cursors) + return _assert_tenant_kept(rig, operator[0]["trace_id"], cursors, needs_model_span=True) + + +@pytest.mark.timeout(120) +@pytest.mark.parametrize("stream", [False, True], ids=["unary", "stream"]) +@pytest.mark.parametrize("client", CLIENTS) +@pytest.mark.parametrize("endpoint", ENDPOINTS) +def test_unconfigured_tenant_trace_keeps_datastore_spans( + unconfigured_rig: Rig, endpoint: Endpoint, client: Client, stream: bool +) -> None: + cursors: Final = unconfigured_rig.cursors() + marker: Final = _marker() + sent: Final = unconfigured_rig.send(endpoint, client, marker, stream) + assert sent.text == REPLY_TEXT, sent + assert unconfigured_rig.upstream_hits(marker) == 1 + _assert_kept(unconfigured_rig, sent, cursors) + + +@pytest.mark.timeout(120) +@pytest.mark.parametrize("endpoint", ["chat", "messages"]) +def test_unconfigured_cache_hit_twin_keeps_datastore_spans(unconfigured_rig: Rig, endpoint: Endpoint) -> None: + cursors: Final = unconfigured_rig.cursors() + marker: Final = _marker() + first_result: Final = _traced_raw(unconfigured_rig, endpoint, marker) + first: Final = first_result[1] + assert first.text == REPLY_TEXT, first + assert unconfigured_rig.upstream_hits(marker) == 1 + _assert_kept(unconfigured_rig, first, cursors) + hit_cursors: Final = unconfigured_rig.cursors() + + def read_hit() -> tuple[str, Sent, tuple[Span, ...]]: + trace_id, sent = _traced_raw(unconfigured_rig, endpoint, marker) + return trace_id, sent, _operator_trace_by_id(unconfigured_rig, trace_id, hit_cursors) + + trace_id, hit, operator = eventually( + read_hit, + lambda result: ( + unconfigured_rig.upstream_hits(marker) == 0 + and "redis" in _db_systems(_post_auth_datastore_spans(result[2])) + ), + seconds=60, + ) + assert hit.text == REPLY_TEXT, hit + post_auth_datastore: Final = _post_auth_datastore_spans(operator) + post_auth_span_ids: Final = frozenset(span["span_id"] for span in post_auth_datastore) + non_datastore_names: Final = frozenset(span["name"] for span in operator if not _db_systems((span,))) + tenant: Final = eventually( + lambda: spans_for_trace(recorded_spans(unconfigured_rig.sinks.tenant, hit_cursors.tenant)[1], trace_id), + lambda spans: ( + non_datastore_names <= frozenset(span["name"] for span in spans) + and post_auth_span_ids <= frozenset(span["span_id"] for span in spans) + ), + seconds=40, + return_last_on_timeout=True, + ) + tenant_span_ids: Final = frozenset(span["span_id"] for span in tenant) + missing_post_auth_names: Final = tuple( + span["name"] for span in post_auth_datastore if span["span_id"] not in tenant_span_ids + ) + assert sum(1 for span in tenant if span["kind"] == SERVER) == 1, _names(tenant) + assert "redis" in _db_systems(tenant), ( + f"operator datastore systems={sorted(_db_systems(post_auth_datastore))}; " + f"tenant datastore systems={sorted(_db_systems(tenant))}; tenant spans={_names(tenant)}" + ) + assert not missing_post_auth_names, ( + f"missing post-auth datastore span names={missing_post_auth_names}; " + f"operator={_names(post_auth_datastore)}; tenant={_names(tenant)}" + ) + + +@pytest.mark.timeout(120) +@pytest.mark.parametrize("endpoint", ENDPOINTS) +def test_unconfigured_failed_upstream_keeps_datastore_spans(unconfigured_rig: Rig, endpoint: Endpoint) -> None: + cursors: Final = unconfigured_rig.cursors() + marker: Final = "excl-fail-" + uuid.uuid4().hex + trace_id: Final = uuid.uuid4().hex + path, body = _body(unconfigured_rig.model, endpoint, marker, stream=False) + failed: Final = unconfigured_rig.proxy.client.post( + path, + json=body, + headers={ + "Authorization": f"Bearer {unconfigured_rig.key}", + "traceparent": f"00-{trace_id}-{uuid.uuid4().hex[:16]}-01", + }, + ) + assert failed.status_code == 500, failed.text + assert unconfigured_rig.upstream_hits(marker) >= 1 + operator: Final = eventually( + lambda: spans_for_trace(recorded_spans(unconfigured_rig.sinks.operator, cursors.operator)[1], trace_id), + lambda spans: _has_root(spans) and "redis" in _db_systems(spans), + seconds=40, + ) + assert "redis" in _db_systems(operator), _names(operator) + _assert_tenant_kept(unconfigured_rig, trace_id, cursors) + + +@pytest.mark.timeout(120) +@pytest.mark.parametrize("status", [403, 404]) +def test_unconfigured_rejecting_tenant_destination_recovers(unconfigured_rig: Rig, status: int) -> None: + configure_sink(unconfigured_rig.sinks.tenant, status=status) + try: + cursors: Final = unconfigured_rig.cursors() + marker: Final = _marker() + sent: Final = unconfigured_rig.raw("chat", marker, stream=True) + assert sent.text == REPLY_TEXT, sent + assert unconfigured_rig.upstream_hits(marker) == 1 + _operator_trace(unconfigured_rig, sent, cursors) + finally: + configure_sink(unconfigured_rig.sinks.tenant, status=200) + after: Final = unconfigured_rig.cursors() + recovered: Final = unconfigured_rig.raw("responses", _marker(), stream=False) + assert recovered.text == REPLY_TEXT, recovered + _assert_kept(unconfigured_rig, recovered, after) + + +@pytest.mark.timeout(120) +def test_unconfigured_key_level_destination_keeps_datastore_spans( + unconfigured_rig: Rig, langfuse_vars: dict[str, JsonValue] +) -> None: + key: Final = unconfigured_rig.scenario.key( + metadata={ + "logging": [ + {"callback_name": "langfuse_otel", "callback_type": "success", "callback_vars": dict(langfuse_vars)} + ] + } + ) + cursors: Final = unconfigured_rig.cursors() + marker: Final = _marker() + sent: Final = unconfigured_rig.raw("chat", marker, stream=False, key=key) + assert sent.text == REPLY_TEXT, sent + assert unconfigured_rig.upstream_hits(marker) == 1 + _assert_kept(unconfigured_rig, sent, cursors) + + +def _assert_tenant_kept_the_burst(rig: Rig, cursors: Cursors, traces: set[str]) -> None: + def ready(spans: tuple[Span, ...]) -> bool: + def trace_kept(trace: str) -> bool: + trace_spans: Final = spans_for_trace(spans, trace) + return any(span["kind"] == SERVER for span in trace_spans) and "redis" in _db_systems(trace_spans) + + return all(trace_kept(trace) for trace in traces) + + tenant: Final = eventually( + lambda: recorded_spans(rig.sinks.tenant, cursors.tenant)[1], + ready, + seconds=90, + return_last_on_timeout=True, + ) + burst: Final = tuple(span for span in tenant if span["trace_id"] in traces) + missing_roots: Final = tuple( + trace for trace in traces if not any(span["kind"] == SERVER for span in spans_for_trace(burst, trace)) + ) + missing_redis: Final = tuple(trace for trace in traces if "redis" not in _db_systems(spans_for_trace(burst, trace))) + assert not missing_roots, f"SERVER root missing from tenant burst traces: {missing_roots}, {_names(burst)}" + assert not missing_redis, f"redis spans missing from tenant burst traces: {missing_redis}, {_names(burst)}" + + +@pytest.mark.timeout(300) +def test_unconfigured_tenant_outage_during_a_mixed_burst(unconfigured_rig: Rig) -> None: + cursors: Final = unconfigured_rig.cursors() + configure_sink(unconfigured_rig.sinks.tenant, status=503) + try: + results: Final = _burst(unconfigured_rig, 30) + finally: + configure_sink(unconfigured_rig.sinks.tenant, status=200) + served: Final = _served(results) + assert len(served) == 30, [result for result in results if isinstance(result, str)] + assert all(sent.text == REPLY_TEXT for sent in served), served + traces: Final = _assert_operator_exactly_once(unconfigured_rig, served, cursors) + _assert_tenant_kept_the_burst(unconfigured_rig, cursors, traces) + after: Final = unconfigured_rig.cursors() + _assert_kept(unconfigured_rig, unconfigured_rig.raw("messages", _marker(), stream=True), after) + + +@pytest.mark.timeout(300) +def test_unconfigured_killing_one_of_two_workers_keeps_the_fan_out(unconfigured_rig: Rig) -> None: + root: Final = psutil.Process(unconfigured_rig.owned.process.pid) + workers: Final = eventually( + lambda: tuple(child for child in root.children() if "resource_tracker" not in " ".join(child.cmdline())), + lambda found: len(found) == 2, + seconds=30, + ) + cursors: Final = unconfigured_rig.cursors() + + def one(index: int) -> Sent | str: + if index == 6: + os.kill(workers[0].pid, signal.SIGKILL) + try: + return unconfigured_rig.raw("chat", _marker(), stream=index % 2 == 0) + except (httpx.HTTPError, AssertionError) as error: + return repr(error) + + with ThreadPoolExecutor(max_workers=6) as pool: + results: Final = tuple(pool.map(one, range(18))) + assert unconfigured_rig.owned.process.poll() is None, "Proxy root exited after a worker was killed" + failures: Final = tuple(result for result in results if isinstance(result, str)) + assert all(failure.startswith(("ReadError(", "RemoteProtocolError(", "ConnectError(")) for failure in failures), ( + failures + ) + assert len(failures) <= 6, failures + settled: Final = tuple(result for index, result in enumerate(results) if index > 12 and isinstance(result, Sent)) + traces: Final = _assert_operator_exactly_once(unconfigured_rig, settled, cursors) + _assert_tenant_kept_the_burst(unconfigured_rig, cursors, traces) + after: Final = unconfigured_rig.cursors() + _assert_kept(unconfigured_rig, unconfigured_rig.raw("chat", _marker(), stream=False), after) + + @dataclass(frozen=True, slots=True) class Setting: otel: Mapping[str, JsonValue] diff --git a/tests/integration/observability/test_passthrough_upstream_error_visibility.py b/tests/integration/observability/test_passthrough_upstream_error_visibility.py index bb18add2f2f..c29ce4f1c62 100644 --- a/tests/integration/observability/test_passthrough_upstream_error_visibility.py +++ b/tests/integration/observability/test_passthrough_upstream_error_visibility.py @@ -238,6 +238,7 @@ def test_config_pass_through_route_logs_body_and_strips_query(gateway: Gateway, "target": f"{wire.url}/upstream?trace=secret-q", "include_subpath": True, "headers": {"Authorization": "Bearer scripted"}, + "auth": True, } ] path.write_text(yaml.safe_dump(config)) diff --git a/tests/integration/providers/test_anthropic_thinking_signature_logging_wire.py b/tests/integration/providers/test_anthropic_thinking_signature_logging_wire.py new file mode 100644 index 00000000000..6622bbb1c9a --- /dev/null +++ b/tests/integration/providers/test_anthropic_thinking_signature_logging_wire.py @@ -0,0 +1,246 @@ +import uuid +from collections.abc import Iterator +from pathlib import Path +from typing import Final +from urllib.parse import unquote + +import anthropic +import pytest +import yaml +from integration._support.anthropic_thinking import ( + BEDROCK_MODEL, + JSON_OBJECT, + MODEL, + NO_CACHE, + SIGNATURE, + THINKING, + THINKING_PARTS, + Event, + answer, + aws_chunks, + chunks_of, + deltas_of, + identity, + logged_thinking, + prompt, + reasoning_text, + signature_only, + signed_blocks, + sse_chunks, + standard_events, + standard_peer, + thinking_block, +) +from integration._support.client import Gateway, eventually, gateway_from_environment +from integration._support.database import read_rows +from integration._support.process import owned_proxy +from integration._support.wire import Wire, wire_server +from pydantic import JsonValue + +pytestmark = pytest.mark.timeout(240) + +_ANTHROPIC_KEY: Final = "scripted-anthropic-key" +_ANTHROPIC_BASE: Final = "http://api.anthropic.com" +_BY_REQUEST_ID: Final = 'SELECT response FROM "LiteLLM_SpendLogs" WHERE request_id=%s' +_BY_DEPLOYMENT: Final = 'SELECT response FROM "LiteLLM_SpendLogs" WHERE model_group=%s' + + +@pytest.fixture(scope="module") +def rig() -> Iterator[Gateway]: + with gateway_from_environment() as gateway: + yield gateway + + +@pytest.fixture(scope="module") +def wire() -> Iterator[Wire]: + with wire_server(standard_peer) as served: + yield served + + +@pytest.fixture(autouse=True) +def _drained_wire(wire: Wire) -> None: + wire.drain() + + +def _config_storing_prompts(directory: Path) -> Path: + config: Final = yaml.safe_load(Path("tests/integration/proxy_config.yaml").read_text()) + config["general_settings"]["store_prompts_in_spend_logs"] = True + path: Final = directory / "store-prompts.yaml" + path.write_text(yaml.safe_dump(config)) + return path + + +@pytest.fixture(scope="module") +def logged(rig: Gateway, wire: Wire, tmp_path_factory: pytest.TempPathFactory) -> Iterator[Gateway]: + directory: Final = tmp_path_factory.mktemp("anthropic-signature-logging") + overrides: Final = { + "ANTHROPIC_API_BASE": _ANTHROPIC_BASE, + "ANTHROPIC_API_KEY": _ANTHROPIC_KEY, + "AIOHTTP_TRUST_ENV": "True", + "HTTP_PROXY": wire.url, + "NO_PROXY": "127.0.0.1,localhost", + } + with owned_proxy(rig, directory, overrides, config=_config_storing_prompts(directory), workers=2) as owned: + yield owned + + +def _logged_response(query: str, value: str) -> dict[str, JsonValue]: + rows: Final = eventually(lambda: read_rows(query, (value,)), lambda found: len(found) == 1, seconds=70) + return JSON_OBJECT.validate_python(rows[0]["response"]) + + +def _logged_reasoning(response: dict[str, JsonValue]) -> JsonValue: + choice: Final = JSON_OBJECT.validate_python(JSON_OBJECT.validate_python(response["choices"][0])) + return JSON_OBJECT.validate_python(choice["message"]).get("reasoning_content") + + +def _messages_events(text: str) -> tuple[Event, ...]: + return tuple( + JSON_OBJECT.validate_json(line.removeprefix("data: ")) + for line in text.splitlines() + if line.startswith("data: ") + ) + + +def _block_deltas(events: tuple[Event, ...]) -> tuple[Event, ...]: + return tuple( + JSON_OBJECT.validate_python(event["delta"]) for event in events if event["type"] == "content_block_delta" + ) + + +def _assert_client_frames_signed_once(events: tuple[Event, ...], marker: str) -> None: + deltas: Final = _block_deltas(events) + assert tuple(delta["thinking"] for delta in deltas if delta["type"] == "thinking_delta") == THINKING_PARTS, events + assert tuple(delta["signature"] for delta in deltas if delta["type"] == "signature_delta") == (SIGNATURE,), events + assert "".join(str(delta["text"]) for delta in deltas if delta["type"] == "text_delta") == answer(marker), events + + +def test_chat_stream_spend_row_stores_the_thinking_once(logged: Gateway, wire: Wire) -> None: + marker: Final = uuid.uuid4().hex + with logged.scenario() as scenario: + model: Final = scenario.model(model=f"anthropic/{MODEL}", api_base=wire.url, api_key=_ANTHROPIC_KEY) + body: Final = { + "model": model, + "messages": [{"role": "user", "content": prompt(marker)}], + "stream": True, + "max_tokens": 64, + **NO_CACHE, + } + response: Final = logged.request("POST", "/v1/chat/completions", body) + assert response.status_code == 200, response.text + chunks: Final = chunks_of(response.text) + deltas: Final = deltas_of(chunks) + assert signed_blocks(deltas) == (signature_only(SIGNATURE),), deltas + assert reasoning_text(deltas) == THINKING, deltas + stored: Final = _logged_response(_BY_REQUEST_ID, str(chunks[0]["id"])) + assert logged_thinking(stored) == (thinking_block(THINKING, SIGNATURE),), stored + assert _logged_reasoning(stored) == THINKING, stored + assert len(wire.drain()) == 1 + + +def test_native_messages_stream_through_the_anthropic_sdk_logs_the_thinking_once(logged: Gateway, wire: Wire) -> None: + marker: Final = uuid.uuid4().hex + with logged.scenario() as scenario: + model: Final = scenario.model(model=f"anthropic/{MODEL}", api_base=wire.url, api_key=_ANTHROPIC_KEY) + client: Final = anthropic.Anthropic(base_url=str(logged.client.base_url), api_key=logged.key, max_retries=0) + events: Final = tuple( + JSON_OBJECT.validate_python(event.model_dump()) + for event in client.messages.create( + model=model, max_tokens=64, messages=[{"role": "user", "content": prompt(marker)}], stream=True + ) + ) + _assert_client_frames_signed_once(events, marker) + starts: Final = tuple(event for event in events if event["type"] == "message_start") + assert JSON_OBJECT.validate_python(starts[0]["message"])["id"] == identity(marker), events + stored: Final = _logged_response(_BY_REQUEST_ID, identity(marker)) + assert logged_thinking(stored) == (thinking_block(THINKING, SIGNATURE),), stored + assert len(wire.drain()) == 1 + + +def test_native_messages_stream_on_bedrock_mantle_logs_the_thinking_once(logged: Gateway, wire: Wire) -> None: + marker: Final = uuid.uuid4().hex + with logged.scenario() as scenario: + model: Final = scenario.model( + model=f"bedrock_mantle/{BEDROCK_MODEL}", + api_base=wire.url, + api_key="scripted-mantle-key", + aws_region_name="us-east-1", + ) + body: Final = { + "model": model, + "max_tokens": 64, + "stream": True, + "messages": [{"role": "user", "content": prompt(marker)}], + } + response: Final = logged.request("POST", "/v1/messages", body) + assert response.status_code == 200, response.text + _assert_client_frames_signed_once(_messages_events(response.text), marker) + stored: Final = _logged_response(_BY_REQUEST_ID, identity(marker)) + assert logged_thinking(stored) == (thinking_block(THINKING, SIGNATURE),), stored + assert [request.target for request in wire.drain()] == ["/anthropic/v1/messages"] + + +def test_adapter_messages_stream_on_snowflake_logs_the_thinking_once(logged: Gateway, wire: Wire) -> None: + marker: Final = uuid.uuid4().hex + with logged.scenario() as scenario: + model: Final = scenario.model(model=f"snowflake/{MODEL}", api_base=wire.url, api_key="scripted-snowflake-key") + body: Final = { + "model": model, + "max_tokens": 64, + "stream": True, + "messages": [{"role": "user", "content": prompt(marker)}], + } + response: Final = logged.request("POST", "/v1/messages", body) + assert response.status_code == 200, response.text + _assert_client_frames_signed_once(_messages_events(response.text), marker) + stored: Final = _logged_response(_BY_DEPLOYMENT, model) + assert logged_thinking(stored) == (thinking_block(THINKING, SIGNATURE),), stored + assert [request.target for request in wire.drain()] == ["/api/v2/cortex/v1/messages"] + + +def test_anthropic_passthrough_stream_relays_the_frames_and_logs_the_thinking_once(logged: Gateway, wire: Wire) -> None: + marker: Final = uuid.uuid4().hex + body: Final = { + "model": MODEL, + "max_tokens": 64, + "stream": True, + "messages": [{"role": "user", "content": prompt(marker)}], + } + response: Final = logged.request("POST", "/anthropic/v1/messages", body) + assert response.status_code == 200, response.text + assert response.content == b"".join(sse_chunks(standard_events(marker))), response.text + received: Final = wire.drain() + assert [request.target for request in received] == [f"{_ANTHROPIC_BASE}/v1/messages"], response.text + assert (received[0].headers.get("host"), received[0].headers.get("x-api-key")) == ( + "api.anthropic.com", + _ANTHROPIC_KEY, + ) + stored: Final = _logged_response(_BY_REQUEST_ID, identity(marker)) + assert logged_thinking(stored) == (thinking_block(THINKING, SIGNATURE),), stored + + +def test_bedrock_invoke_passthrough_stream_relays_the_frames_and_logs_the_thinking_once( + logged: Gateway, wire: Wire +) -> None: + marker: Final = uuid.uuid4().hex + with logged.scenario() as scenario: + deployment: Final = scenario.model( + model=f"bedrock/{BEDROCK_MODEL}", + api_base=wire.url, + aws_access_key_id="AKIASCRIPTEDPROVIDER", + aws_secret_access_key="scripted-secret", + aws_region_name="us-east-1", + aws_bedrock_runtime_endpoint=wire.url, + ) + body: Final = { + "anthropic_version": "bedrock-2023-05-31", + "max_tokens": 64, + "messages": [{"role": "user", "content": prompt(marker)}], + } + response: Final = logged.request("POST", f"/bedrock/model/{deployment}/invoke-with-response-stream", body) + assert response.status_code == 200, response.text + assert response.content == b"".join(aws_chunks(standard_events(marker))), response.text + targets: Final = [unquote(request.target) for request in wire.drain()] + assert targets == [f"/model/{BEDROCK_MODEL}/invoke-with-response-stream"], targets + stored: Final = _logged_response(_BY_DEPLOYMENT, deployment) + assert logged_thinking(stored) == (thinking_block(THINKING, SIGNATURE),), stored diff --git a/tests/integration/providers/test_anthropic_thinking_signature_stream_wire.py b/tests/integration/providers/test_anthropic_thinking_signature_stream_wire.py new file mode 100644 index 00000000000..cf46885b6b5 --- /dev/null +++ b/tests/integration/providers/test_anthropic_thinking_signature_stream_wire.py @@ -0,0 +1,783 @@ +import asyncio +import json +import re +import signal +import threading +import uuid +from collections.abc import Mapping, Sequence +from dataclasses import dataclass +from pathlib import Path +from queue import SimpleQueue +from types import MappingProxyType +from typing import Final, Literal +from urllib.parse import unquote, urlsplit + +import httpx +import openai +import psutil +import pytest +import yaml +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from integration._support.anthropic_thinking import ( + BEDROCK_MODEL, + JSON_LIST, + JSON_OBJECT, + MODEL, + NO_CACHE, + SIGNATURE, + THINKING, + THINKING_PARTS, + Event, + accumulate, + answer, + chunks_of, + content_text, + deltas_of, + identity, + marker_of, + message_body, + message_events, + prompt, + reasoning_text, + redacted_events, + signature_only, + signed_blocks, + standard_events, + standard_peer, + stream_reply, + streams, + text_events, + thinking_block, + thinking_events, +) +from integration._support.client import Gateway, Scenario, eventually +from integration._support.database import read_rows +from integration._support.process import owned_proxy_process +from integration._support.wire import Reply, Request, Wire, wire_server +from openai.types.chat import ChatCompletionChunk +from pydantic import JsonValue + +_SECOND_SIGNATURE: Final = "scripted-signature-" + "t" * 32 +_LONG_SIGNATURE: Final = "k" * 5120 +_REDACTED: Final = "scripted-redacted-" + "r" * 32 +_VERTEX_PROJECT: Final = "scripted-project" +_VERTEX_LOCATION: Final = "us-east5" +_VERTEX_MODEL_PATH: Final = ( + f"/v1/projects/{_VERTEX_PROJECT}/locations/{_VERTEX_LOCATION}/publishers/anthropic/models/{MODEL}" +) +_CONFIG_MODEL: Final = "anthropic-signature-chaos" +_STARTED_WORKER: Final = re.compile(r"Started server process \[(\d+)\]") + +Provider = Literal["anthropic", "bedrock_invoke", "claude_platform", "vertex_ai", "snowflake", "azure_ai"] +Endpoint = Literal["chat", "messages", "responses"] + +_TARGETS: Final = MappingProxyType( + { + "anthropic": "/v1/messages", + "bedrock_invoke": f"/model/{BEDROCK_MODEL}/invoke-with-response-stream", + "claude_platform": "/v1/messages", + "vertex_ai": f"{_VERTEX_MODEL_PATH}:streamRawPredict", + "snowflake": "/api/v2/cortex/v1/messages", + "azure_ai": "/anthropic/v1/messages", + } +) + + +def _service_account_json(token_url: str) -> str: + private_key: Final = ( + rsa.generate_private_key(public_exponent=65537, key_size=2048) + .private_bytes( + serialization.Encoding.PEM, + serialization.PrivateFormat.PKCS8, + serialization.NoEncryption(), + ) + .decode() + ) + return json.dumps( + { + "type": "service_account", + "project_id": _VERTEX_PROJECT, + "private_key_id": "scripted", + "private_key": private_key, + "client_email": f"scripted@{_VERTEX_PROJECT}.iam.gserviceaccount.com", + "client_id": "0", + "auth_uri": f"{token_url}/_oauth/authorize", + "token_uri": f"{token_url}/_oauth/token", + } + ) + + +def _deployment(scenario: Scenario, provider: Provider, wire_url: str, upstream_url: str) -> str: + match provider: + case "anthropic": + return scenario.model(model=f"anthropic/{MODEL}", api_base=wire_url, api_key="scripted-anthropic-key") + case "bedrock_invoke": + return scenario.model( + model=f"bedrock/invoke/{BEDROCK_MODEL}", + api_base=wire_url, + aws_access_key_id="AKIASCRIPTEDPROVIDER", + aws_secret_access_key="scripted-secret", + aws_region_name="us-east-1", + aws_bedrock_runtime_endpoint=wire_url, + ) + case "claude_platform": + return scenario.model( + model=f"bedrock/claude_platform/{MODEL}", + api_base=wire_url, + api_key="scripted-platform-key", + aws_region_name="us-east-1", + workspace_id="scripted-workspace", + ) + case "vertex_ai": + return scenario.model( + model=f"vertex_ai/{MODEL}", + api_base=f"{wire_url}{_VERTEX_MODEL_PATH}", + api_key=None, + vertex_project=_VERTEX_PROJECT, + vertex_location=_VERTEX_LOCATION, + vertex_credentials=_service_account_json(upstream_url.rstrip("/")), + ) + case "snowflake": + return scenario.model(model=f"snowflake/{MODEL}", api_base=wire_url, api_key="scripted-snowflake-key") + case "azure_ai": + return scenario.model(model=f"azure_ai/{MODEL}", api_base=wire_url, api_key="scripted-azure-key") + + +def _chat_body( + model: str, + marker: str, + *, + cache_control: Mapping[str, JsonValue] = NO_CACHE, + messages: Sequence[Mapping[str, JsonValue]] | None = None, +) -> dict[str, JsonValue]: + turn: Final = list(messages) if messages else [{"role": "user", "content": prompt(marker)}] + return {"model": model, "messages": turn, "stream": True, "max_tokens": 64, **cache_control} + + +def _stream_chat(gateway: Gateway, body: Mapping[str, JsonValue], *, key: str | None = None) -> httpx.Response: + return gateway.request("POST", "/v1/chat/completions", body, key=key) + + +def _sdk_delta(chunk: ChatCompletionChunk) -> Event: + if not chunk.choices: + return {} + return JSON_OBJECT.validate_python(chunk.choices[0].delta.model_dump(exclude_none=True)) + + +def _openai_client(gateway: Gateway) -> openai.OpenAI: + return openai.OpenAI(base_url=str(gateway.client.base_url) + "/v1", api_key=gateway.key, max_retries=0) + + +def _spend_row(request_id: str) -> dict[str, JsonValue]: + rows: Final = eventually( + lambda: read_rows( + 'SELECT request_id, status, model_group FROM "LiteLLM_SpendLogs" WHERE request_id=%s', (request_id,) + ), + lambda found: len(found) == 1, + seconds=70, + ) + return rows[0] + + +def _assert_signed_once(deltas: Sequence[Event], marker: str, *, signature: JsonValue = SIGNATURE) -> None: + assert signed_blocks(deltas) == (signature_only(signature),), deltas + assert accumulate(deltas) == (thinking_block(THINKING, signature),), deltas + assert reasoning_text(deltas) == THINKING, deltas + assert content_text(deltas) == answer(marker), deltas + + +def _replay_messages(marker: str, follow_up: str, deltas: Sequence[Event]) -> tuple[dict[str, JsonValue], ...]: + assistant: Event = { + "role": "assistant", + "content": content_text(deltas), + "thinking_blocks": list(accumulate(deltas)), + } + return ({"role": "user", "content": prompt(marker)}, assistant, {"role": "user", "content": prompt(follow_up)}) + + +def _assistant_turn(request: Request) -> tuple[Event, ...]: + messages: Final = JSON_LIST.validate_python(JSON_OBJECT.validate_json(request.body)["messages"]) + assistant: Final = JSON_OBJECT.validate_python(messages[1]) + assert assistant["role"] == "assistant", request.body + return tuple(JSON_OBJECT.validate_python(part) for part in JSON_LIST.validate_python(assistant["content"])) + + +@pytest.mark.parametrize( + "provider", + ["anthropic", "bedrock_invoke", "claude_platform", "vertex_ai", "snowflake", "azure_ai"], +) +def test_signature_chunk_carries_no_thinking_text_on_every_anthropic_wire_provider( + gateway: Gateway, provider: Provider +) -> None: + marker: Final = uuid.uuid4().hex + with wire_server(standard_peer) as wire, gateway.scenario() as scenario: + model: Final = _deployment(scenario, provider, wire.url, gateway.upstream_url) + response: Final = _stream_chat(gateway, _chat_body(model, marker)) + assert response.status_code == 200, response.text + assert response.text.rstrip().endswith("data: [DONE]"), response.text + chunks: Final = chunks_of(response.text) + _assert_signed_once(deltas_of(chunks), marker) + assert [urlsplit(unquote(request.target)).path for request in wire.drain()] == [_TARGETS[provider]], ( + response.text + ) + row: Final = _spend_row(str(chunks[0]["id"])) + assert (row["model_group"], row["status"]) == (model, "success"), row + + +def test_openai_sdk_sync_stream_accumulates_the_thinking_once(gateway: Gateway) -> None: + marker: Final = uuid.uuid4().hex + with wire_server(standard_peer) as wire, gateway.scenario() as scenario: + model: Final = _deployment(scenario, "anthropic", wire.url, gateway.upstream_url) + chunks: Final = tuple( + _openai_client(gateway).chat.completions.create( + model=model, + messages=[{"role": "user", "content": prompt(marker)}], + stream=True, + max_tokens=64, + extra_body=NO_CACHE, + ) + ) + _assert_signed_once(tuple(_sdk_delta(chunk) for chunk in chunks), marker) + assert len(wire.drain()) == 1 + assert _spend_row(chunks[0].id)["model_group"] == model + + +async def test_openai_sdk_async_stream_accumulates_the_thinking_once(gateway: Gateway) -> None: + marker: Final = uuid.uuid4().hex + with wire_server(standard_peer) as wire, gateway.scenario() as scenario: + model: Final = _deployment(scenario, "anthropic", wire.url, gateway.upstream_url) + client: Final = openai.AsyncOpenAI( + base_url=str(gateway.client.base_url) + "/v1", api_key=gateway.key, max_retries=0 + ) + stream: Final = await client.chat.completions.create( + model=model, + messages=[{"role": "user", "content": prompt(marker)}], + stream=True, + max_tokens=64, + extra_body=NO_CACHE, + ) + chunks: Final = tuple([chunk async for chunk in stream]) + _assert_signed_once(tuple(_sdk_delta(chunk) for chunk in chunks), marker) + assert len(wire.drain()) == 1 + assert (await asyncio.to_thread(_spend_row, chunks[0].id))["model_group"] == model + + +def test_non_streaming_completion_keeps_the_signed_thinking_block_intact(gateway: Gateway) -> None: + marker: Final = uuid.uuid4().hex + with wire_server(standard_peer) as wire, gateway.scenario() as scenario: + model: Final = _deployment(scenario, "anthropic", wire.url, gateway.upstream_url) + completion: Final = _openai_client(gateway).chat.completions.create( + model=model, messages=[{"role": "user", "content": prompt(marker)}], max_tokens=64, extra_body=NO_CACHE + ) + message: Final = JSON_OBJECT.validate_python(completion.choices[0].message.model_dump(exclude_none=True)) + assert message["thinking_blocks"] == [thinking_block(THINKING, SIGNATURE)], message + assert message["reasoning_content"] == THINKING, message + assert message["content"] == answer(marker), message + received: Final = wire.drain() + assert len(received) == 1 and not streams(received[0]), received + assert _spend_row(completion.id)["model_group"] == model + + +def _reasoning_item(output: Sequence[Event]) -> Event: + reasoning: Final = tuple(item for item in output if item["type"] == "reasoning") + assert len(reasoning) == 1, output + return reasoning[0] + + +def _reasoning_text(item: Mapping[str, JsonValue]) -> str: + parts: Final = tuple(JSON_OBJECT.validate_python(part) for part in JSON_LIST.validate_python(item["content"])) + return "".join(str(part["text"]) for part in parts) + + +def test_responses_stream_encrypts_the_thinking_once(gateway: Gateway) -> None: + marker: Final = uuid.uuid4().hex + with wire_server(standard_peer) as wire, gateway.scenario() as scenario: + model: Final = _deployment(scenario, "anthropic", wire.url, gateway.upstream_url) + events: Final = tuple( + _openai_client(gateway).responses.create( + model=model, + input=prompt(marker), + stream=True, + include=["reasoning.encrypted_content"], + max_output_tokens=64, + extra_body=NO_CACHE, + ) + ) + completed: Final = tuple(event for event in events if event.type == "response.completed") + assert len(completed) == 1, [event.type for event in events] + output: Final = tuple(JSON_OBJECT.validate_python(item.model_dump()) for item in completed[0].response.output) + item: Final = _reasoning_item(output) + assert json.loads(str(item["encrypted_content"])) == [thinking_block(THINKING, SIGNATURE)], item + assert _reasoning_text(item) == THINKING, item + received: Final = wire.drain() + assert len(received) == 1 and streams(received[0]), received + + +def test_responses_non_stream_encrypts_the_signed_block_as_received(gateway: Gateway) -> None: + marker: Final = uuid.uuid4().hex + with wire_server(standard_peer) as wire, gateway.scenario() as scenario: + model: Final = _deployment(scenario, "anthropic", wire.url, gateway.upstream_url) + response: Final = _openai_client(gateway).responses.create( + model=model, + input=prompt(marker), + include=["reasoning.encrypted_content"], + max_output_tokens=64, + extra_body=NO_CACHE, + ) + output: Final = tuple(JSON_OBJECT.validate_python(item.model_dump()) for item in response.output) + item: Final = _reasoning_item(output) + assert json.loads(str(item["encrypted_content"])) == [thinking_block(THINKING, SIGNATURE)], item + assert _reasoning_text(item) == THINKING, item + received: Final = wire.drain() + assert len(received) == 1 and not streams(received[0]), received + + +def test_cache_hit_replays_the_answer_from_one_upstream_call_and_never_doubles_the_thinking(gateway: Gateway) -> None: + marker: Final = uuid.uuid4().hex + with wire_server(standard_peer) as wire, gateway.scenario() as scenario: + model: Final = _deployment(scenario, "anthropic", wire.url, gateway.upstream_url) + body: Final = _chat_body(model, marker, cache_control={}) + first: Final = _stream_chat(gateway, body) + assert first.status_code == 200, first.text + first_chunks: Final = chunks_of(first.text) + _assert_signed_once(deltas_of(first_chunks), marker) + assert _spend_row(str(first_chunks[0]["id"]))["model_group"] == model + second: Final = _stream_chat(gateway, body) + assert second.status_code == 200, second.text + second_deltas: Final = deltas_of(chunks_of(second.text)) + assert content_text(second_deltas) == answer(marker), second.text + assert accumulate(second_deltas) in ((), (thinking_block(THINKING, SIGNATURE),)), second.text + assert len(wire.drain()) == 1, second.text + + +def test_replaying_the_accumulated_turn_sends_the_thinking_once_with_its_signature(gateway: Gateway) -> None: + marker: Final = uuid.uuid4().hex + follow_up: Final = uuid.uuid4().hex + with wire_server(standard_peer) as wire, gateway.scenario() as scenario: + model: Final = _deployment(scenario, "anthropic", wire.url, gateway.upstream_url) + first: Final = _stream_chat(gateway, _chat_body(model, marker)) + assert first.status_code == 200, first.text + deltas: Final = deltas_of(chunks_of(first.text)) + second: Final = _stream_chat( + gateway, _chat_body(model, follow_up, messages=_replay_messages(marker, follow_up, deltas)) + ) + assert second.status_code == 200, second.text + assert content_text(deltas_of(chunks_of(second.text))) == answer(follow_up), second.text + received: Final = wire.drain() + assert len(received) == 2, [request.body for request in received] + assert _assistant_turn(received[1]) == ( + thinking_block(THINKING, SIGNATURE), + {"type": "text", "text": answer(marker)}, + ), received[1].body + + +def test_two_signed_blocks_each_keep_their_own_text_through_a_replay(gateway: Gateway) -> None: + marker: Final = uuid.uuid4().hex + follow_up: Final = uuid.uuid4().hex + + def respond(request: Request) -> Reply: + found: Final = marker_of(request) + if not streams(request): + return Reply(body=message_body(found)) + events: Final = message_events( + found, + ( + thinking_events(0, ("one ", "two"), (SIGNATURE,)), + thinking_events(1, ("three ", "four"), (_SECOND_SIGNATURE,)), + text_events(2, answer(found)), + ), + ) + return stream_reply(request, events) + + with wire_server(respond) as wire, gateway.scenario() as scenario: + model: Final = _deployment(scenario, "anthropic", wire.url, gateway.upstream_url) + first: Final = _stream_chat(gateway, _chat_body(model, marker)) + assert first.status_code == 200, first.text + deltas: Final = deltas_of(chunks_of(first.text)) + assert signed_blocks(deltas) == (signature_only(SIGNATURE), signature_only(_SECOND_SIGNATURE)), deltas + assert accumulate(deltas) == ( + thinking_block("one two", SIGNATURE), + thinking_block("three four", _SECOND_SIGNATURE), + ), deltas + assert reasoning_text(deltas) == "one twothree four", deltas + second: Final = _stream_chat( + gateway, _chat_body(model, follow_up, messages=_replay_messages(marker, follow_up, deltas)) + ) + assert second.status_code == 200, second.text + received: Final = wire.drain() + assert len(received) == 2, [request.body for request in received] + assert _assistant_turn(received[1]) == ( + thinking_block("one two", SIGNATURE), + thinking_block("three four", _SECOND_SIGNATURE), + {"type": "text", "text": answer(marker)}, + ), received[1].body + + +def test_redacted_block_before_a_signed_block_replays_each_once(gateway: Gateway) -> None: + marker: Final = uuid.uuid4().hex + follow_up: Final = uuid.uuid4().hex + + def respond(request: Request) -> Reply: + found: Final = marker_of(request) + if not streams(request): + return Reply(body=message_body(found)) + events: Final = message_events( + found, + ( + redacted_events(0, _REDACTED), + thinking_events(1, THINKING_PARTS, (SIGNATURE,)), + text_events(2, answer(found)), + ), + ) + return stream_reply(request, events) + + with wire_server(respond) as wire, gateway.scenario() as scenario: + model: Final = _deployment(scenario, "anthropic", wire.url, gateway.upstream_url) + first: Final = _stream_chat(gateway, _chat_body(model, marker)) + assert first.status_code == 200, first.text + deltas: Final = deltas_of(chunks_of(first.text)) + assert accumulate(deltas) == ( + {"type": "redacted_thinking", "data": _REDACTED}, + thinking_block(THINKING, SIGNATURE), + ), deltas + second: Final = _stream_chat( + gateway, _chat_body(model, follow_up, messages=_replay_messages(marker, follow_up, deltas)) + ) + assert second.status_code == 200, second.text + received: Final = wire.drain() + assert len(received) == 2, [request.body for request in received] + assert _assistant_turn(received[1]) == ( + {"type": "redacted_thinking", "data": _REDACTED}, + thinking_block(THINKING, SIGNATURE), + {"type": "text", "text": answer(marker)}, + ), received[1].body + + +def test_signature_only_block_without_thinking_deltas_is_relayed_as_is(gateway: Gateway) -> None: + marker: Final = uuid.uuid4().hex + + def respond(request: Request) -> Reply: + return stream_reply(request, standard_events(marker_of(request), parts=())) + + with wire_server(respond) as wire, gateway.scenario() as scenario: + model: Final = _deployment(scenario, "anthropic", wire.url, gateway.upstream_url) + response: Final = _stream_chat(gateway, _chat_body(model, marker)) + assert response.status_code == 200, response.text + deltas: Final = deltas_of(chunks_of(response.text)) + assert signed_blocks(deltas) == (signature_only(SIGNATURE),), deltas + assert accumulate(deltas) == (signature_only(SIGNATURE),), deltas + assert reasoning_text(deltas) == "", deltas + assert content_text(deltas) == answer(marker), deltas + assert len(wire.drain()) == 1 + + +def test_two_identical_requests_with_no_cache_each_land_their_own_spend_row(gateway: Gateway) -> None: + marker: Final = uuid.uuid4().hex + with wire_server(standard_peer) as wire, gateway.scenario() as scenario: + model: Final = _deployment(scenario, "anthropic", wire.url, gateway.upstream_url) + responses: Final = tuple(_stream_chat(gateway, _chat_body(model, marker)) for _ in range(2)) + ids: Final = tuple(str(chunks_of(response.text)[0]["id"]) for response in responses) + for response in responses: + assert response.status_code == 200, response.text + _assert_signed_once(deltas_of(chunks_of(response.text)), marker) + assert len(set(ids)) == 2, ids + assert len(wire.drain()) == 2 + for request_id in ids: + assert _spend_row(request_id)["model_group"] == model + + +@pytest.mark.parametrize("signature", [123, [], ""], ids=["integer", "list", "empty"]) +def test_unusable_signature_values_yield_no_signed_block_and_keep_the_stream_intact( + gateway: Gateway, signature: JsonValue +) -> None: + marker: Final = uuid.uuid4().hex + + def respond(request: Request) -> Reply: + return stream_reply(request, standard_events(marker_of(request), signatures=(signature,))) + + with wire_server(respond) as wire, gateway.scenario() as scenario: + model: Final = _deployment(scenario, "anthropic", wire.url, gateway.upstream_url) + response: Final = _stream_chat(gateway, _chat_body(model, marker)) + assert response.status_code == 200, response.text + assert response.text.rstrip().endswith("data: [DONE]"), response.text + deltas: Final = deltas_of(chunks_of(response.text)) + assert signed_blocks(deltas) == (), deltas + assert reasoning_text(deltas) == THINKING, deltas + assert content_text(deltas) == answer(marker), deltas + assert len(wire.drain()) == 1 + assert gateway.client.get("/health/liveliness").status_code == 200 + + +def test_five_kilobyte_signature_is_relayed_verbatim_without_thinking_text(gateway: Gateway) -> None: + marker: Final = uuid.uuid4().hex + + def respond(request: Request) -> Reply: + return stream_reply(request, standard_events(marker_of(request), signatures=(_LONG_SIGNATURE,))) + + with wire_server(respond) as wire, gateway.scenario() as scenario: + model: Final = _deployment(scenario, "anthropic", wire.url, gateway.upstream_url) + response: Final = _stream_chat(gateway, _chat_body(model, marker)) + assert response.status_code == 200, response.text + _assert_signed_once(deltas_of(chunks_of(response.text)), marker, signature=_LONG_SIGNATURE) + assert len(wire.drain()) == 1 + + +def test_duplicate_signature_deltas_never_repeat_the_thinking_text(gateway: Gateway) -> None: + marker: Final = uuid.uuid4().hex + + def respond(request: Request) -> Reply: + return stream_reply(request, standard_events(marker_of(request), signatures=(SIGNATURE, SIGNATURE))) + + with wire_server(respond) as wire, gateway.scenario() as scenario: + model: Final = _deployment(scenario, "anthropic", wire.url, gateway.upstream_url) + response: Final = _stream_chat(gateway, _chat_body(model, marker)) + assert response.status_code == 200, response.text + deltas: Final = deltas_of(chunks_of(response.text)) + assert signed_blocks(deltas) == (signature_only(SIGNATURE), signature_only(SIGNATURE)), deltas + assert "".join(str(block["thinking"]) for block in accumulate(deltas)) == THINKING, deltas + assert reasoning_text(deltas) == THINKING, deltas + assert len(wire.drain()) == 1 + + +def test_non_string_thinking_delta_is_ignored_and_the_signed_block_still_lands_once(gateway: Gateway) -> None: + marker: Final = uuid.uuid4().hex + + def respond(request: Request) -> Reply: + return stream_reply(request, standard_events(marker_of(request), parts=("alpha ", 7, "beta"))) + + with wire_server(respond) as wire, gateway.scenario() as scenario: + model: Final = _deployment(scenario, "anthropic", wire.url, gateway.upstream_url) + response: Final = _stream_chat(gateway, _chat_body(model, marker)) + assert response.status_code == 200, response.text + assert response.text.rstrip().endswith("data: [DONE]"), response.text + _assert_signed_once(deltas_of(chunks_of(response.text)), marker) + assert len(wire.drain()) == 1 + + +def test_upstream_authentication_error_reaches_the_caller_and_leaves_the_proxy_healthy(gateway: Gateway) -> None: + marker: Final = uuid.uuid4().hex + + def respond(request: Request) -> Reply: + body: Final = {"type": "error", "error": {"type": "authentication_error", "message": "scripted invalid key"}} + return Reply(status=401, body=json.dumps(body).encode()) + + with wire_server(respond) as wire, gateway.scenario() as scenario: + model: Final = _deployment(scenario, "anthropic", wire.url, gateway.upstream_url) + response: Final = _stream_chat(gateway, _chat_body(model, marker)) + assert response.status_code == 401, response.text + assert "scripted invalid key" in response.text, response.text + assert len(wire.drain()) >= 1 + assert gateway.client.get("/health/liveliness").status_code == 200 + control: Final = uuid.uuid4().hex + with wire_server(standard_peer) as healthy, gateway.scenario() as again: + working: Final = _deployment(again, "anthropic", healthy.url, gateway.upstream_url) + recovered: Final = _stream_chat(gateway, _chat_body(working, control)) + assert recovered.status_code == 200, recovered.text + _assert_signed_once(deltas_of(chunks_of(recovered.text)), control) + + +def test_unauthenticated_stream_is_refused_before_the_upstream_is_called(gateway: Gateway) -> None: + marker: Final = uuid.uuid4().hex + with wire_server(standard_peer) as wire, gateway.scenario() as scenario: + model: Final = _deployment(scenario, "anthropic", wire.url, gateway.upstream_url) + response: Final = _stream_chat(gateway, _chat_body(model, marker), key=f"sk-not-a-key-{marker}") + assert response.status_code == 401, response.text + assert wire.drain() == () + + +@dataclass(frozen=True, slots=True) +class _Call: + endpoint: Endpoint + stream: bool + marker: str + + +@dataclass(frozen=True, slots=True) +class _Served: + call: _Call + status: int + text: str + + +def _path(endpoint: Endpoint) -> str: + match endpoint: + case "chat": + return "/v1/chat/completions" + case "messages": + return "/v1/messages" + case "responses": + return "/v1/responses" + + +def _body(model: str, call: _Call) -> dict[str, JsonValue]: + match call.endpoint: + case "chat": + return _chat_body(model, call.marker) | {"stream": call.stream} + case "messages": + return { + "model": model, + "max_tokens": 64, + "stream": call.stream, + "messages": [{"role": "user", "content": prompt(call.marker)}], + } + case "responses": + return { + "model": model, + "input": prompt(call.marker), + "stream": call.stream, + "max_output_tokens": 64, + **NO_CACHE, + } + + +async def _send(client: httpx.AsyncClient, key: str, model: str, call: _Call) -> _Served: + try: + async with client.stream( + "POST", _path(call.endpoint), json=_body(model, call), headers={"Authorization": f"Bearer {key}"} + ) as response: + raw: Final = await response.aread() + return _Served(call=call, status=response.status_code, text=raw.decode()) + except httpx.TransportError as error: + return _Served(call=call, status=0, text=repr(error)) + + +async def _burst(base_url: str, key: str, model: str, calls: Sequence[_Call]) -> tuple[_Served, ...]: + async with httpx.AsyncClient(base_url=base_url, timeout=60, trust_env=False) as client: + return tuple(await asyncio.gather(*(_send(client, key, model, call) for call in calls))) + + +def _calls(count: int, endpoints: Sequence[Endpoint]) -> tuple[_Call, ...]: + return tuple( + _Call(endpoint=endpoints[index % len(endpoints)], stream=index % 2 == 0, marker=uuid.uuid4().hex) + for index in range(count) + ) + + +def _completed_id(item: _Served) -> str | None: + match item.call.endpoint: + case "chat": + first: Final = chunks_of(item.text)[0] if item.call.stream else JSON_OBJECT.validate_json(item.text) + return str(first["id"]) + case "messages": + return identity(item.call.marker) + case "responses": + return None + + +def _success_rows(model: str) -> list[dict[str, JsonValue]]: + return read_rows( + 'SELECT request_id FROM "LiteLLM_SpendLogs" WHERE model_group=%s AND status=%s', (model, "success") + ) + + +async def test_mid_thinking_upstream_aborts_in_a_mixed_burst_leave_every_completed_call_logged_once( + gateway: Gateway, +) -> None: + calls: Final = _calls(24, ("chat", "messages", "responses")) + aborted: Final = frozenset(call.marker for index, call in enumerate(calls) if index % 4 == 0) + + def respond(request: Request) -> Reply: + marker: Final = marker_of(request) + if not streams(request): + return Reply(body=message_body(marker)) + return stream_reply(request, standard_events(marker), abort_after=3 if marker in aborted else None) + + with wire_server(respond) as wire, gateway.scenario() as scenario: + model: Final = _deployment(scenario, "anthropic", wire.url, gateway.upstream_url) + served: Final = await _burst(str(gateway.client.base_url), gateway.key, model, calls) + assert gateway.client.get("/health/liveliness").status_code == 200 + completed: Final = tuple(item for item in served if item.call.marker not in aborted) + for item in served: + if item.call.marker in aborted: + assert answer(item.call.marker) not in item.text, item.text + else: + assert item.status == 200, item.text + assert answer(item.call.marker) in item.text, item.text + assert len(completed) == 18, [item.call for item in completed] + for item in completed: + if item.call.endpoint == "chat" and item.call.stream: + _assert_signed_once(deltas_of(chunks_of(item.text)), item.call.marker) + assert len(wire.drain()) == 24 + rows: Final = await asyncio.to_thread( + eventually, lambda: _success_rows(model), lambda found: len(found) == len(completed), 70 + ) + logged: Final = tuple(str(row["request_id"]) for row in rows) + for item in completed: + request_id: Final = _completed_id(item) + assert request_id is None or logged.count(request_id) == 1, (request_id, logged) + + +def _chaos_config(wire: Wire, directory: Path) -> Path: + config: Final = yaml.safe_load(Path("tests/integration/proxy_config.yaml").read_text()) + config["model_list"] = [ + { + "model_name": _CONFIG_MODEL, + "litellm_params": { + "model": f"anthropic/{MODEL}", + "api_base": wire.url, + "api_key": "scripted-anthropic-key", + }, + } + ] + path: Final = directory / "anthropic-signature-chaos.yaml" + path.write_text(yaml.safe_dump(config)) + return path + + +def _open_upstream_connections(pid: int, upstream: str) -> int: + port: Final = urlsplit(upstream).port + return sum( + 1 + for connection in psutil.Process(pid).net_connections(kind="tcp") + if connection.status == psutil.CONN_ESTABLISHED and connection.raddr and connection.raddr.port == port + ) + + +@pytest.mark.timeout(180) +async def test_worker_sigkill_mid_burst_leaves_the_sibling_streaming_signed_thinking_once( + gateway: Gateway, tmp_path: Path +) -> None: + calls: Final = _calls(20, ("chat",)) + release: Final = threading.Event() + held_markers: Final[SimpleQueue[str]] = SimpleQueue() + + def held(request: Request) -> Reply: + held_markers.put(marker_of(request)) + assert release.wait(timeout=60), "The burst was never released" + return standard_peer(request) + + with wire_server(held) as wire: + path: Final = _chaos_config(wire, tmp_path) + with owned_proxy_process(gateway, tmp_path, {}, config=path, workers=2) as owned: + candidate: Final = owned.gateway + workers: Final = eventually( + lambda: tuple(int(pid) for pid in _STARTED_WORKER.findall(owned.log.read_text())), + lambda pids: len(pids) == 2, + seconds=30, + ) + burst: Final = asyncio.create_task( + _burst(str(candidate.client.base_url), candidate.key, _CONFIG_MODEL, calls) + ) + await asyncio.to_thread(eventually, held_markers.qsize, lambda size: size == 20, 60) + held_by: Final = MappingProxyType({pid: _open_upstream_connections(pid, wire.url) for pid in workers}) + assert sum(held_by.values()) == 20, held_by + victim_pid, survivor_pid = sorted(workers, key=held_by.__getitem__) + victim: Final = psutil.Process(victim_pid) + victim.suspend() + victim.send_signal(signal.SIGKILL) + release.set() + served: Final = await burst + assert held_by[survivor_pid] >= 10, held_by + completed: Final = tuple(item for item in served if item.status == 200) + assert len(completed) == held_by[survivor_pid], (held_by, [item.status for item in served]) + for item in completed: + if item.call.stream: + _assert_signed_once(deltas_of(chunks_of(item.text)), item.call.marker) + else: + assert answer(item.call.marker) in item.text, item.text + follow_up: Final = _Call(endpoint="chat", stream=True, marker=uuid.uuid4().hex) + (answered,) = await _burst(str(candidate.client.base_url), candidate.key, _CONFIG_MODEL, (follow_up,)) + assert answered.status == 200, answered.text + _assert_signed_once(deltas_of(chunks_of(answered.text)), follow_up.marker) + assert len(wire.drain()) == 21 diff --git a/tests/integration/providers/test_bedrock_converse_stream_event_frames_wire.py b/tests/integration/providers/test_bedrock_converse_stream_event_frames_wire.py new file mode 100644 index 00000000000..bf4f611107d --- /dev/null +++ b/tests/integration/providers/test_bedrock_converse_stream_event_frames_wire.py @@ -0,0 +1,935 @@ +import asyncio +import base64 +import json +import os +import re +import signal +import threading +import uuid +from collections.abc import Callable, Iterable, Mapping +from dataclasses import dataclass +from pathlib import Path +from queue import SimpleQueue +from types import MappingProxyType +from typing import Final, Literal +from urllib.parse import unquote, urlsplit + +import anthropic +import httpx +import openai +import psutil +import pytest +import yaml +from integration._support.client import Gateway, Scenario, eventually +from integration._support.database import read_rows +from integration._support.process import owned_proxy_process +from integration._support.upstream import _aws_event_frame, aws_event_stream_frame +from integration._support.wire import Reply, Request, Wire, wire_server +from pydantic import JsonValue, TypeAdapter + +from litellm.proxy.common_utils.encrypt_decrypt_utils import decrypt_if_encrypted_with + +_MODEL_ID: Final = "global.moonshotai.kimi-k3" +_CONVERSE_MODEL: Final = f"bedrock/converse/{_MODEL_ID}" +_STREAM_TARGET: Final = f"/model/{_MODEL_ID}/converse-stream" +_CONVERSE_TARGET: Final = f"/model/{_MODEL_ID}/converse" +_INVOKE_MODEL_ID: Final = "anthropic.claude-3-haiku-20240307-v1:0" +_INVOKE_MODEL: Final = f"bedrock/invoke/{_INVOKE_MODEL_ID}" +_INVOKE_STREAM_TARGET: Final = f"/model/{_INVOKE_MODEL_ID}/invoke-with-response-stream" +_EVENT_STREAM: Final = "application/vnd.amazon.eventstream" +_ANSWER: Final = "bedrock event frame control" +_REJECTION: Final = "structured output schema uses unsupported regex negative look-ahead" +_THROTTLED: Final = "Too many requests, please wait before trying again" +_UNKNOWN_TYPE: Final = "somethingBedrockAddedLater" +_UNKNOWN_ONLY: Final = "none of its 1 events carried a known event type" +_SIGNING_KEY: Final = os.environ.get("LITELLM_SALT_KEY", "sk-integration-salt") +_JSON_HEADERS: Final = MappingProxyType({":content-type": "application/json", ":message-type": "event"}) +_USAGE: Final[dict[str, JsonValue]] = {"usage": {"inputTokens": 11, "outputTokens": 4, "totalTokens": 15}} +_RESPONSE: Final = json.dumps( + { + "output": {"message": {"role": "assistant", "content": [{"text": _ANSWER}]}}, + "stopReason": "end_turn", + **_USAGE, + "metrics": {"latencyMs": 1}, + } +).encode() +_JSON: Final = TypeAdapter(dict[str, JsonValue]) +_AWS: Final[dict[str, JsonValue]] = { + "aws_access_key_id": "AKIASCRIPTEDPROVIDER", + "aws_secret_access_key": "scripted-secret", + "aws_region_name": "us-east-1", +} +_EXTRA: Final[dict[str, JsonValue]] = {"num_retries": 0, "cache": {"no-cache": True}} +_PROMPT: Final = "What does the gateway do with this stream?" +_USER_TURN: Final[dict[str, JsonValue]] = {"role": "user", "content": _PROMPT} +_CONVERSE_USER_TURN: Final[dict[str, JsonValue]] = {"role": "user", "content": [{"text": _PROMPT}]} +_STARTED_WORKER: Final = re.compile(r"Started server process \[(\d+)\]") +_CALL_INDEX: Final = re.compile(r"call-[0-9a-f]{32}-(\d+)") +_PLAIN: Final = "bedrock-event-frames-plain" + +Endpoint = Literal["chat", "messages", "responses"] + + +def _error_body(message: str) -> str: + return json.dumps({"message": message}, separators=(",", ":")) + + +def _frame(event_type: str, payload: Mapping[str, JsonValue]) -> bytes: + return _aws_event_frame(event_type, payload, "sc", "u") + + +def _typed_frame(headers: Mapping[str, str | int], payload: bytes) -> bytes: + return aws_event_stream_frame({**headers, **_JSON_HEADERS}, payload) + + +def _exception_message_frame(exception_type: str, message: str) -> bytes: + return aws_event_stream_frame( + {":exception-type": exception_type, ":content-type": "application/json", ":message-type": "exception"}, + _error_body(message).encode(), + ) + + +def _text_frames(text: str) -> tuple[bytes, ...]: + return ( + _frame("messageStart", {"role": "assistant"}), + _frame("contentBlockDelta", {"delta": {"text": text}, "contentBlockIndex": 0}), + _frame("contentBlockStop", {"contentBlockIndex": 0}), + ) + + +_NORMAL: Final = b"".join( + (*_text_frames(_ANSWER), _frame("messageStop", {"stopReason": "end_turn"}), _frame("metadata", _USAGE)) +) +_VALIDATION_FRAME: Final = _frame("validationException", {"message": _REJECTION}) +_THROTTLING_FRAME: Final = _frame("throttlingException", {"message": _THROTTLED}) +_UNKNOWN_FRAME: Final = _frame(_UNKNOWN_TYPE, {"future": True}) +_UNKNOWN_BESIDE_KNOWN: Final = b"".join( + ( + *_text_frames(_ANSWER), + _UNKNOWN_FRAME, + _frame("messageStop", {"stopReason": "end_turn"}), + _frame("metadata", _USAGE), + ) +) +_THROTTLED_AFTER_TEXT: Final = b"".join((*_text_frames(_ANSWER), _THROTTLING_FRAME)) +_EMPTY_DELTA_BESIDE_KNOWN: Final = b"".join( + ( + _frame("messageStart", {"role": "assistant"}), + _frame("contentBlockDelta", {}), + _frame("contentBlockDelta", {"delta": {"text": _ANSWER}, "contentBlockIndex": 0}), + _frame("messageStop", {"stopReason": "end_turn"}), + _frame("metadata", _USAGE), + ) +) +_EXCEPTION_MID_STREAM: Final = b"".join((*_text_frames(_ANSWER), _VALIDATION_FRAME)) +_EXCEPTION_MESSAGE_MID_STREAM: Final = b"".join( + (*_text_frames(_ANSWER), _exception_message_frame("throttlingException", _THROTTLED)) +) + + +def _invoke_chunk(event: Mapping[str, JsonValue]) -> bytes: + return _frame("chunk", {"bytes": base64.b64encode(json.dumps(event).encode()).decode()}) + + +_INVOKE_STREAM: Final = b"".join( + ( + _invoke_chunk( + { + "type": "message_start", + "message": { + "id": "msg_invoke", + "type": "message", + "role": "assistant", + "content": [], + "model": _INVOKE_MODEL_ID, + "stop_reason": None, + "usage": {"input_tokens": 11, "output_tokens": 1}, + }, + } + ), + _invoke_chunk({"type": "content_block_start", "index": 0, "content_block": {"type": "text", "text": ""}}), + _invoke_chunk({"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": _ANSWER}}), + _invoke_chunk({"type": "content_block_stop", "index": 0}), + _invoke_chunk({"type": "message_delta", "delta": {"stop_reason": "end_turn"}, "usage": {"output_tokens": 4}}), + _invoke_chunk({"type": "message_stop"}), + ) +) + + +@dataclass(frozen=True, slots=True) +class _Streamed: + status: int + call_id: str + lines: tuple[str, ...] + + @property + def text(self) -> str: + return "\n".join(self.lines) + + +@dataclass(frozen=True, slots=True) +class _Call: + endpoint: Endpoint + user: str + index: int + + +@dataclass(frozen=True, slots=True) +class _Served: + call: _Call + status: int + call_id: str + text: str + + +def _stream_peer(frames: bytes) -> Callable[[Request], Reply]: + def respond(request: Request) -> Reply: + if unquote(request.target) in (_STREAM_TARGET, _INVOKE_STREAM_TARGET): + return Reply(body=frames, content_type=_EVENT_STREAM) + return Reply(body=_RESPONSE) + + return respond + + +def _converse_deployment(scenario: Scenario, wire: Wire, **extra: JsonValue) -> str: + return scenario.model(model=_CONVERSE_MODEL, api_base=wire.url, **_AWS, **extra) + + +def _auth(gateway: Gateway) -> dict[str, str]: + return {"Authorization": f"Bearer {gateway.key}"} + + +def _proxy_url(gateway: Gateway) -> str: + return str(gateway.client.base_url).rstrip("/") + + +def _path(endpoint: Endpoint) -> str: + match endpoint: + case "chat": + return "/v1/chat/completions" + case "messages": + return "/v1/messages" + case "responses": + return "/v1/responses" + + +def _body(endpoint: Endpoint, model: str, *, user: str | None = None) -> dict[str, JsonValue]: + marker: Final[dict[str, JsonValue]] = {} if user is None else {"user": user} + match endpoint: + case "chat": + return {"model": model, "messages": [_USER_TURN], "max_tokens": 16, "stream": True, **_EXTRA, **marker} + case "messages": + return {"model": model, "messages": [_USER_TURN], "max_tokens": 16, "stream": True, **_EXTRA} + case "responses": + return {"model": model, "input": _PROMPT, "stream": True, **_EXTRA, **marker} + + +def _stream( + gateway: Gateway, endpoint: Endpoint, body: Mapping[str, JsonValue], *, key: str | None = None +) -> _Streamed: + headers: Final = _auth(gateway) if key is None else {"Authorization": f"Bearer {key}"} + with gateway.client.stream("POST", _path(endpoint), json=body, headers=headers) as response: + lines: Final = tuple(line for line in response.iter_lines() if line) + return _Streamed(response.status_code, response.headers.get("x-litellm-call-id", ""), lines) + + +def _sse_payloads(lines: Iterable[str]) -> tuple[dict[str, JsonValue], ...]: + return tuple(json.loads(line[6:]) for line in lines if line.startswith("data: ") and line != "data: [DONE]") + + +def _sse_events(lines: Iterable[str]) -> tuple[str, ...]: + return tuple(line[7:] for line in lines if line.startswith("event: ")) + + +def _first_choice(chunk: Mapping[str, JsonValue]) -> dict[str, JsonValue] | None: + choices: Final = chunk.get("choices") + return _JSON.validate_python(choices[0]) if isinstance(choices, list) and choices else None + + +def _chat_text(chunks: Iterable[dict[str, JsonValue]]) -> str: + choices: Final = tuple(choice for choice in map(_first_choice, chunks) if choice is not None) + return "".join(str(_JSON.validate_python(choice["delta"]).get("content") or "") for choice in choices) + + +def _finish_reasons(chunks: Iterable[dict[str, JsonValue]]) -> tuple[JsonValue, ...]: + return tuple(choice.get("finish_reason") for choice in map(_first_choice, chunks) if choice is not None) + + +def _chat_id(chunks: Iterable[dict[str, JsonValue]]) -> str: + (identity,) = {str(chunk["id"]) for chunk in chunks if "id" in chunk} + return identity + + +def _message_id(payloads: Iterable[dict[str, JsonValue]]) -> str: + (started,) = tuple(payload for payload in payloads if payload.get("type") == "message_start") + return str(_JSON.validate_python(started["message"])["id"]) + + +def _messages_text(payloads: Iterable[dict[str, JsonValue]]) -> str: + deltas: Final = tuple(payload for payload in payloads if payload.get("type") == "content_block_delta") + return "".join(str(_JSON.validate_python(delta["delta"]).get("text") or "") for delta in deltas) + + +def _responses_text(events: Iterable[dict[str, JsonValue]]) -> str: + return "".join( + str(event.get("delta") or "") for event in events if event.get("type") == "response.output_text.delta" + ) + + +def _inner_response_id(identity: str) -> str: + managed: Final = decrypt_if_encrypted_with(identity.removeprefix("resp_"), _SIGNING_KEY) + assert managed is not None, identity + issued: Final = managed.split(";", 1)[0].rsplit("response_id:", 1)[1] + decoded: Final = base64.b64decode(issued.removeprefix("resp_")).decode() + return decoded.rsplit("response_id:", 1)[1] + + +def _completed_response_id(events: Iterable[dict[str, JsonValue]]) -> str: + (completed,) = tuple(event for event in events if event.get("type") == "response.completed") + return _inner_response_id(str(_JSON.validate_python(completed["response"])["id"])) + + +def _only_received(wire: Wire) -> tuple[str, dict[str, JsonValue]]: + (request,) = wire.drain() + return unquote(request.target), _JSON.validate_python(json.loads(request.body)) + + +def _assert_stream_request(wire: Wire, target: str = _STREAM_TARGET) -> dict[str, JsonValue]: + received_target, received = _only_received(wire) + assert received_target == target, received_target + return received + + +def _spend_row(request_id: str) -> dict[str, JsonValue]: + assert request_id, "No id to look the spend row up by" + (row,) = eventually( + lambda: read_rows( + 'SELECT request_id, status, call_type, end_user FROM "LiteLLM_SpendLogs" WHERE request_id=%s', + (request_id,), + ), + lambda found: len(found) >= 1, + seconds=70, + ) + return row + + +def _failure_row(call_id: str) -> dict[str, JsonValue]: + row: Final = _spend_row(call_id) + assert row["status"] == "failure", row + return row + + +def _success_row(request_id: str) -> dict[str, JsonValue]: + row: Final = _spend_row(request_id) + assert row["status"] == "success", row + return row + + +def _rows_for(request_ids: frozenset[str], *, expected: int) -> tuple[dict[str, JsonValue], ...]: + rows: Final = eventually( + lambda: read_rows( + 'SELECT request_id, status FROM "LiteLLM_SpendLogs" WHERE request_id = ANY(string_to_array(%s, %s))', + (",".join(sorted(request_ids)), ","), + ), + lambda found: len(found) >= expected, + seconds=90, + ) + return tuple(rows) + + +def _assert_rejected_chat(streamed: _Streamed, status: int, message: str) -> None: + assert streamed.status == status, (streamed.status, streamed.text) + error: Final = _JSON.validate_python(json.loads(streamed.text)["error"]) + assert message in str(error["message"]), streamed.text + assert str(error["code"]) == str(status), streamed.text + + +def _unescaped(text: str) -> str: + return text.replace('\\"', '"') + + +def _assert_rejected_stream_body(streamed: _Streamed, message: str) -> None: + assert streamed.status == 200, (streamed.status, streamed.text) + assert message in _unescaped(streamed.text), streamed.text + assert _ANSWER not in streamed.text, streamed.text + + +def test_r01_chat_stream_with_a_validation_exception_frame_first_is_a_400_and_a_failure_row(gateway: Gateway) -> None: + with wire_server(_stream_peer(_VALIDATION_FRAME)) as wire, gateway.scenario() as scenario: + model: Final = _converse_deployment(scenario, wire) + streamed: Final = _stream(gateway, "chat", _body("chat", model)) + _assert_rejected_chat(streamed, 400, f"validationException {_error_body(_REJECTION)}") + received: Final = _assert_stream_request(wire) + assert received["messages"] == [_CONVERSE_USER_TURN], received + _failure_row(streamed.call_id) + + +async def _consume_openai_chat_stream(client: openai.AsyncOpenAI, model: str) -> None: + stream = await client.chat.completions.create( + model=model, messages=[_USER_TURN], max_tokens=16, stream=True, extra_body=_EXTRA + ) + _ = [chunk async for chunk in stream] + + +async def test_r02_openai_async_sdk_stream_with_a_validation_exception_frame_first_raises_bad_request( + gateway: Gateway, +) -> None: + with wire_server(_stream_peer(_VALIDATION_FRAME)) as wire, gateway.scenario() as scenario: + model: Final = _converse_deployment(scenario, wire) + async with openai.AsyncOpenAI( + base_url=f"{_proxy_url(gateway)}/v1", api_key=gateway.key, max_retries=0 + ) as client: + with pytest.raises(openai.BadRequestError, match=re.escape(_REJECTION)) as raised: + await _consume_openai_chat_stream(client, model) + _assert_stream_request(wire) + _failure_row(raised.value.response.headers.get("x-litellm-call-id", "")) + + +def test_r03_chat_stream_throttled_after_text_delivers_the_text_then_the_error_and_no_stop_chunk( + gateway: Gateway, +) -> None: + with wire_server(_stream_peer(_THROTTLED_AFTER_TEXT)) as wire, gateway.scenario() as scenario: + model: Final = _converse_deployment(scenario, wire) + streamed: Final = _stream(gateway, "chat", _body("chat", model)) + assert streamed.status == 200, streamed.text + chunks: Final = _sse_payloads(streamed.lines) + assert _chat_text(chunks) == _ANSWER, streamed.text + assert "throttlingException" in streamed.text and _THROTTLED in streamed.text, streamed.text + assert "stop" not in _finish_reasons(chunks), streamed.text + _assert_stream_request(wire) + _failure_row(streamed.call_id) + + +def test_r04_messages_stream_with_a_validation_exception_frame_first_emits_an_error_event_and_no_message_stop( + gateway: Gateway, +) -> None: + with wire_server(_stream_peer(_VALIDATION_FRAME)) as wire, gateway.scenario() as scenario: + model: Final = _converse_deployment(scenario, wire) + streamed: Final = _stream(gateway, "messages", _body("messages", model)) + _assert_rejected_stream_body(streamed, f"validationException {_error_body(_REJECTION)}") + events: Final = _sse_events(streamed.lines) + assert "error" in events and "message_stop" not in events, streamed.text + _assert_stream_request(wire) + + +async def test_r05_anthropic_async_sdk_stream_with_a_validation_exception_frame_first_raises( + gateway: Gateway, +) -> None: + with wire_server(_stream_peer(_VALIDATION_FRAME)) as wire, gateway.scenario() as scenario: + model: Final = _converse_deployment(scenario, wire) + async with anthropic.AsyncAnthropic(base_url=_proxy_url(gateway), api_key=gateway.key, max_retries=0) as client: + with pytest.raises(anthropic.APIError, match=re.escape(_REJECTION)): + async with client.messages.stream( + model=model, max_tokens=16, messages=[_USER_TURN], extra_body=_EXTRA + ) as stream: + _ = [event async for event in stream] + _assert_stream_request(wire) + + +def test_r06_responses_stream_with_a_validation_exception_frame_first_fails_the_response(gateway: Gateway) -> None: + with wire_server(_stream_peer(_VALIDATION_FRAME)) as wire, gateway.scenario() as scenario: + model: Final = _converse_deployment(scenario, wire) + streamed: Final = _stream(gateway, "responses", _body("responses", model)) + _assert_rejected_stream_body(streamed, f"validationException {_error_body(_REJECTION)}") + types: Final = tuple(str(event["type"]) for event in _sse_payloads(streamed.lines)) + assert "response.failed" in types and "response.completed" not in types, streamed.text + _assert_stream_request(wire) + _failure_row(streamed.call_id) + + +def test_r07_chat_stream_whose_only_frame_has_an_unknown_event_type_is_a_502_naming_the_type( + gateway: Gateway, +) -> None: + with wire_server(_stream_peer(_UNKNOWN_FRAME)) as wire, gateway.scenario() as scenario: + model: Final = _converse_deployment(scenario, wire) + streamed: Final = _stream(gateway, "chat", _body("chat", model)) + _assert_rejected_chat(streamed, 502, f"{_UNKNOWN_ONLY} (event types=['{_UNKNOWN_TYPE}']") + _assert_stream_request(wire) + _failure_row(streamed.call_id) + + +def _assert_text_stream(gateway: Gateway, endpoint: Endpoint, frames: bytes) -> None: + marker: Final = f"call-{uuid.uuid4().hex}" + with wire_server(_stream_peer(frames)) as wire, gateway.scenario() as scenario: + model: Final = _converse_deployment(scenario, wire) + streamed: Final = _stream(gateway, endpoint, _body(endpoint, model, user=marker)) + assert streamed.status == 200, streamed.text + payloads: Final = _sse_payloads(streamed.lines) + match endpoint: + case "chat": + assert _chat_text(payloads) == _ANSWER, streamed.text + assert _finish_reasons(payloads)[-1] == "stop", streamed.text + _success_row(_chat_id(payloads)) + case "messages": + assert _messages_text(payloads) == _ANSWER, streamed.text + assert _sse_events(streamed.lines)[-1] == "message_stop", streamed.text + _success_row(_message_id(payloads)) + case "responses": + assert _responses_text(payloads) == _ANSWER, streamed.text + assert str(payloads[-1]["type"]) == "response.completed", streamed.text + assert _success_row(_completed_response_id(payloads))["end_user"] == marker + _assert_stream_request(wire) + + +@pytest.mark.parametrize( + "endpoint", ("chat", "messages", "responses"), ids=("r08-chat", "r08-messages", "r08-responses") +) +def test_r08_an_unknown_frame_between_known_frames_leaves_the_text_and_the_success_row_intact( + gateway: Gateway, endpoint: Endpoint +) -> None: + _assert_text_stream(gateway, endpoint, _UNKNOWN_BESIDE_KNOWN) + + +@pytest.mark.parametrize( + "endpoint", ("chat", "messages", "responses"), ids=("r09-chat", "r09-messages", "r09-responses") +) +def test_r09_a_normal_converse_stream_delivers_the_text_and_a_success_row(gateway: Gateway, endpoint: Endpoint) -> None: + _assert_text_stream(gateway, endpoint, _NORMAL) + + +def test_r10_a_non_streaming_converse_call_is_untouched(gateway: Gateway) -> None: + with wire_server(_stream_peer(_NORMAL)) as wire, gateway.scenario() as scenario: + model: Final = _converse_deployment(scenario, wire) + response: Final = gateway.request( + "POST", "/v1/chat/completions", {"model": model, "messages": [_USER_TURN], "max_tokens": 16, **_EXTRA} + ) + assert response.status_code == 200, response.text + assert response.json()["choices"][0]["message"]["content"] == _ANSWER, response.text + _assert_stream_request(wire, _CONVERSE_TARGET) + _success_row(str(response.json()["id"])) + + +def test_r11_an_invoke_framed_anthropic_stream_is_untouched(gateway: Gateway) -> None: + with wire_server(_stream_peer(_INVOKE_STREAM)) as wire, gateway.scenario() as scenario: + model: Final = scenario.model( + model=_INVOKE_MODEL, api_key=None, aws_bedrock_runtime_endpoint=wire.url, api_base=wire.url, **_AWS + ) + streamed: Final = _stream(gateway, "chat", _body("chat", model)) + assert streamed.status == 200, streamed.text + chunks: Final = _sse_payloads(streamed.lines) + assert _chat_text(chunks) == _ANSWER, streamed.text + assert _finish_reasons(chunks)[-1] == "stop", streamed.text + _assert_stream_request(wire, _INVOKE_STREAM_TARGET) + _success_row(_chat_id(chunks)) + + +@pytest.mark.parametrize( + ("exception_type", "expected"), + (("throttlingException", 429), ("somethingNewException", 400)), + ids=("r12", "r13"), +) +def test_r12_r13_an_exception_message_frame_keeps_its_modeled_status( + gateway: Gateway, exception_type: str, expected: int +) -> None: + with ( + wire_server(_stream_peer(_exception_message_frame(exception_type, _THROTTLED))) as wire, + gateway.scenario() as scenario, + ): + model: Final = _converse_deployment(scenario, wire) + streamed: Final = _stream(gateway, "chat", _body("chat", model)) + _assert_rejected_chat(streamed, expected, _THROTTLED) + _assert_stream_request(wire) + _failure_row(streamed.call_id) + + +@pytest.mark.parametrize( + "frames", (_EXCEPTION_MID_STREAM, _EXCEPTION_MESSAGE_MID_STREAM), ids=("r14-event-frame", "r15-exception-message") +) +def test_r14_r15_passthrough_converse_stream_relays_an_exception_frame_byte_for_byte( + gateway: Gateway, frames: bytes +) -> None: + with wire_server(_stream_peer(frames)) as wire, gateway.scenario() as scenario: + deployment: Final = scenario.model( + model=f"bedrock/{_MODEL_ID}", api_base=wire.url, aws_bedrock_runtime_endpoint=wire.url, **_AWS + ) + response: Final = gateway.request( + "POST", f"/bedrock/model/{deployment}/converse-stream", {"messages": [_CONVERSE_USER_TURN]} + ) + assert response.status_code == 200, response.text + assert response.headers.get("content-type") == _EVENT_STREAM, dict(response.headers) + assert response.content == frames, response.content + _assert_stream_request(wire) + + +_HEADERLESS: Final = _typed_frame({}, b'{"future": true}') +_INT_TYPED: Final = _typed_frame({":event-type": 7}, b'{"future": true}') +_EMPTY_TYPED: Final = _typed_frame({":event-type": ""}, b'{"future": true}') +_LONG_TYPE: Final = "x" * 5120 +_LONG_TYPED: Final = _typed_frame({":event-type": _LONG_TYPE}, b'{"future": true}') + + +@pytest.mark.parametrize( + ("frames", "named"), + ( + (_HEADERLESS, f"{_UNKNOWN_ONLY} (event types=['']"), + (_INT_TYPED, f"{_UNKNOWN_ONLY} (event types=['']"), + (_EMPTY_TYPED, f"{_UNKNOWN_ONLY} (event types=['']"), + (_LONG_TYPED, f"{_UNKNOWN_ONLY} (event types=['{_LONG_TYPE}']"), + ( + _UNKNOWN_FRAME + _UNKNOWN_FRAME, + f"none of its 2 events carried a known event type (event types=['{_UNKNOWN_TYPE}']", + ), + ), + ids=( + "s01-no-event-type", + "s02-int-event-type", + "s03-empty-event-type", + "s04-5kb-event-type", + "s05-same-type-twice", + ), +) +def test_s01_to_s05_odd_event_type_headers_alone_are_a_502_that_names_what_arrived( + gateway: Gateway, frames: bytes, named: str +) -> None: + with wire_server(_stream_peer(frames)) as wire, gateway.scenario() as scenario: + model: Final = _converse_deployment(scenario, wire) + streamed: Final = _stream(gateway, "chat", _body("chat", model)) + _assert_rejected_chat(streamed, 502, named) + _assert_stream_request(wire) + _failure_row(streamed.call_id) + + +def test_s06_a_validation_exception_frame_with_a_non_utf8_body_is_a_400_with_the_bytes_replaced( + gateway: Gateway, +) -> None: + frame: Final = _typed_frame({":event-type": "validationException"}, b'{"message": "bad \xff\xfe bytes"}') + with wire_server(_stream_peer(frame)) as wire, gateway.scenario() as scenario: + model: Final = _converse_deployment(scenario, wire) + streamed: Final = _stream(gateway, "chat", _body("chat", model)) + _assert_rejected_chat(streamed, 400, 'validationException {"message": "bad �� bytes"}') + _assert_stream_request(wire) + _failure_row(streamed.call_id) + + +def test_s07_a_validation_exception_frame_with_an_empty_body_is_still_a_400(gateway: Gateway) -> None: + with wire_server(_stream_peer(_typed_frame({":event-type": "validationException"}, b""))) as wire: + with gateway.scenario() as scenario: + model: Final = _converse_deployment(scenario, wire) + streamed: Final = _stream(gateway, "chat", _body("chat", model)) + _assert_rejected_chat(streamed, 400, "validationException") + _assert_stream_request(wire) + _failure_row(streamed.call_id) + + +def test_s08_a_known_frame_with_an_empty_body_beside_normal_frames_keeps_the_text(gateway: Gateway) -> None: + with wire_server(_stream_peer(_EMPTY_DELTA_BESIDE_KNOWN)) as wire, gateway.scenario() as scenario: + model: Final = _converse_deployment(scenario, wire) + streamed: Final = _stream(gateway, "chat", _body("chat", model)) + assert streamed.status == 200, streamed.text + chunks: Final = _sse_payloads(streamed.lines) + assert _chat_text(chunks) == _ANSWER, streamed.text + assert _finish_reasons(chunks)[-1] == "stop", streamed.text + _assert_stream_request(wire) + _success_row(_chat_id(chunks)) + + +def test_s09_an_unauthenticated_stream_never_reaches_the_peer(gateway: Gateway) -> None: + with wire_server(_stream_peer(_VALIDATION_FRAME)) as wire, gateway.scenario() as scenario: + model: Final = _converse_deployment(scenario, wire) + streamed: Final = _stream(gateway, "chat", _body("chat", model), key="sk-integration-bogus") + assert streamed.status == 401, streamed.text + assert wire.drain() == (), streamed.text + + +def test_s10_a_rejected_stream_leaves_a_healthy_deployment_serving(gateway: Gateway) -> None: + with ( + wire_server(_stream_peer(_VALIDATION_FRAME)) as rejecting, + wire_server(_stream_peer(_NORMAL)) as healthy, + gateway.scenario() as scenario, + ): + rejected_model: Final = _converse_deployment(scenario, rejecting) + healthy_model: Final = _converse_deployment(scenario, healthy) + _assert_rejected_chat(_stream(gateway, "chat", _body("chat", rejected_model)), 400, "validationException") + streamed: Final = _stream(gateway, "chat", _body("chat", healthy_model)) + assert streamed.status == 200, streamed.text + assert _chat_text(_sse_payloads(streamed.lines)) == _ANSWER, streamed.text + assert len(rejecting.drain()) == 1 and len(healthy.drain()) == 1 + + +def test_e01_a_rejected_stream_is_never_served_from_the_response_cache(gateway: Gateway) -> None: + with wire_server(_stream_peer(_VALIDATION_FRAME)) as wire, gateway.scenario() as scenario: + model: Final = _converse_deployment(scenario, wire) + cached: Final = {key: value for key, value in _body("chat", model).items() if key != "cache"} + first: Final = _stream(gateway, "chat", cached) + second: Final = _stream(gateway, "chat", cached) + _assert_rejected_chat(first, 400, "validationException") + _assert_rejected_chat(second, 400, "validationException") + assert len(wire.drain()) == 2, (first.text, second.text) + + +def _sibling_deployment(scenario: Scenario, name: str, wire: Wire, **extra: JsonValue) -> None: + created: Final = scenario.gateway.post( + "/model/new", + { + "model_name": name, + "litellm_params": {"model": _CONVERSE_MODEL, "api_base": wire.url, **_AWS, **extra}, + "model_info": {}, + }, + ) + identity: Final = _JSON.validate_python(created["model_info"])["id"] + assert isinstance(identity, str), created + scenario.cleanups.callback(scenario.delete_model, identity) + + +def test_e02_a_throttling_frame_first_is_a_429_after_one_attempt_even_with_retries_and_a_sibling_deployment( + gateway: Gateway, +) -> None: + with wire_server(_stream_peer(_THROTTLING_FRAME)) as wire, gateway.scenario() as scenario: + model: Final = _converse_deployment(scenario, wire, num_retries=2) + _sibling_deployment(scenario, model, wire, num_retries=2) + streamed: Final = _stream(gateway, "chat", {**_body("chat", model), "num_retries": 2}) + _assert_rejected_chat(streamed, 429, f"throttlingException {_error_body(_THROTTLED)}") + attempts: Final = len(wire.drain()) + assert attempts == 1, attempts + _failure_row(streamed.call_id) + + +def test_e03_three_rejected_streams_land_one_failure_row_each(gateway: Gateway) -> None: + with wire_server(_stream_peer(_VALIDATION_FRAME)) as wire, gateway.scenario() as scenario: + model: Final = _converse_deployment(scenario, wire) + streamed: Final = tuple(_stream(gateway, "chat", _body("chat", model)) for _ in range(3)) + for item in streamed: + _assert_rejected_chat(item, 400, "validationException") + call_ids: Final = frozenset(item.call_id for item in streamed) + assert len(call_ids) == 3, streamed + rows: Final = _rows_for(call_ids, expected=3) + assert {str(row["request_id"]) for row in rows} == call_ids, rows + assert all(row["status"] == "failure" for row in rows), rows + assert len(wire.drain()) == 3 + + +def _calls(marker: str, endpoint: Endpoint, indexes: range) -> tuple[_Call, ...]: + return tuple(_Call(endpoint, f"{marker}-{index}", index) for index in indexes) + + +def _burst_body(model: str, call: _Call) -> dict[str, JsonValue]: + prompt: Final = f"{_PROMPT} {call.user}" + match call.endpoint: + case "chat": + return {**_body("chat", model, user=call.user), "messages": [{"role": "user", "content": prompt}]} + case "messages": + return {**_body("messages", model), "messages": [{"role": "user", "content": prompt}]} + case "responses": + return {**_body("responses", model, user=call.user), "input": prompt} + + +def _call_index(request: Request) -> int: + found: Final = _CALL_INDEX.search(request.body.decode()) + assert found is not None, request.body + return int(found.group(1)) + + +async def _send(client: httpx.AsyncClient, key: str, model: str, call: _Call) -> _Served: + async with client.stream( + "POST", _path(call.endpoint), json=_burst_body(model, call), headers={"Authorization": f"Bearer {key}"} + ) as response: + raw: Final = await response.aread() + return _Served(call, response.status_code, response.headers.get("x-litellm-call-id", ""), raw.decode()) + + +async def _burst( + base_url: str, key: str, model: str, calls: tuple[_Call, ...], *, tolerate_transport_errors: bool = False +) -> tuple[_Served, ...]: + async with httpx.AsyncClient(base_url=base_url, timeout=60, trust_env=False) as client: + results: Final = await asyncio.gather( + *(_send(client, key, model, call) for call in calls), return_exceptions=tolerate_transport_errors + ) + for result in results: + assert not isinstance(result, BaseException) or isinstance(result, httpx.TransportError), repr(result) + return tuple(result for result in results if isinstance(result, _Served)) + + +def _carries_text(served: _Served) -> bool: + return _ANSWER in served.text + + +def _is_rejection(served: _Served, message: str) -> bool: + return ( + message in _unescaped(served.text) and not _carries_text(served) and '"finish_reason":"stop"' not in served.text + ) + + +def _served_success_id(served: _Served) -> str: + payloads: Final = _sse_payloads(served.text.splitlines()) + match served.call.endpoint: + case "chat": + return _chat_id(payloads) + case "messages": + return _message_id(payloads) + case "responses": + return _completed_response_id(payloads) + + +async def test_c01_a_mixed_burst_of_normal_rejected_and_unknown_streams_sorts_every_call_and_row( + gateway: Gateway, +) -> None: + marker: Final = f"call-{uuid.uuid4().hex}" + calls: Final = ( + *_calls(marker, "chat", range(0, 10)), + *_calls(marker, "messages", range(10, 20)), + *_calls(marker, "responses", range(20, 30)), + ) + + def respond(request: Request) -> Reply: + match _call_index(request) % 3: + case 1: + return Reply(body=_VALIDATION_FRAME, content_type=_EVENT_STREAM) + case 2: + return Reply(body=_UNKNOWN_FRAME, content_type=_EVENT_STREAM) + case _: + return Reply(body=_NORMAL, content_type=_EVENT_STREAM) + + with wire_server(respond) as wire, gateway.scenario() as scenario: + model: Final = _converse_deployment(scenario, wire) + served: Final = await _burst(_proxy_url(gateway), gateway.key, model, calls) + assert len(served) == 30 + normal: Final = tuple(item for item in served if item.call.index % 3 == 0) + rejected: Final = tuple(item for item in served if item.call.index % 3 == 1) + unknown: Final = tuple(item for item in served if item.call.index % 3 == 2) + for item in normal: + assert item.status == 200 and _carries_text(item), (item.call, item.status, item.text) + for item in rejected: + assert _is_rejection(item, _REJECTION), (item.call, item.status, item.text) + for item in unknown: + assert _is_rejection(item, _UNKNOWN_ONLY), (item.call, item.status, item.text) + failed_ids: Final = frozenset( + item.call_id for item in (*rejected, *unknown) if item.call.endpoint != "messages" + ) + assert len(failed_ids) == 13, failed_ids + failure_rows: Final = _rows_for(failed_ids, expected=13) + assert {str(row["request_id"]) for row in failure_rows} == failed_ids, failure_rows + assert all(row["status"] == "failure" for row in failure_rows), failure_rows + success_ids: Final = frozenset(_served_success_id(item) for item in normal) + assert len(success_ids) == 10, success_ids + success_rows: Final = _rows_for(success_ids, expected=10) + assert {str(row["request_id"]) for row in success_rows} == success_ids, success_rows + assert all(row["status"] == "success" for row in success_rows), success_rows + assert len(wire.drain()) == 30 + + +def _owned_config(wire: Wire, directory: Path) -> Path: + base: Final = _JSON.validate_python(yaml.safe_load(Path("tests/integration/proxy_config.yaml").read_text())) + config: Final[dict[str, JsonValue]] = { + **base, + "model_list": [ + { + "model_name": _PLAIN, + "litellm_params": { + "model": _CONVERSE_MODEL, + "api_base": wire.url, + "api_key": "integration-provider-key", + **_AWS, + }, + } + ], + "router_settings": {**_JSON.validate_python(base["router_settings"]), "num_retries": 0}, + } + path: Final = directory / "bedrock-event-frames.yaml" + path.write_text(yaml.safe_dump(config)) + return path + + +def _open_peer_connections(pid: int, peer_url: str) -> int: + port: Final = urlsplit(peer_url).port + return sum( + 1 + for connection in psutil.Process(pid).net_connections(kind="tcp") + if connection.status == psutil.CONN_ESTABLISHED and connection.raddr and connection.raddr.port == port + ) + + +def _held_rejection(release: threading.Event, held_indexes: SimpleQueue[int]) -> Callable[[Request], Reply]: + first: Final = _frame("messageStart", {"role": "assistant"}) + + def held(request: Request) -> Reply: + held_indexes.put(_call_index(request)) + return Reply(content_type=_EVENT_STREAM, chunks=(first, _VALIDATION_FRAME), gate_after_first=release) + + return held + + +@pytest.mark.timeout(600) +async def test_c02_worker_sigkill_mid_burst_leaves_the_sibling_rejecting_the_held_streams( + gateway: Gateway, tmp_path: Path +) -> None: + marker: Final = f"call-{uuid.uuid4().hex}" + again: Final = f"call-{uuid.uuid4().hex}" + calls: Final = _calls(marker, "chat", range(20)) + release: Final = threading.Event() + held_indexes: Final[SimpleQueue[int]] = SimpleQueue() + with wire_server(_held_rejection(release, held_indexes)) as wire: + config: Final = _owned_config(wire, tmp_path) + with owned_proxy_process(gateway, tmp_path, {}, config=config, workers=2) as owned: + candidate: Final = owned.gateway + workers: Final = eventually( + lambda: tuple(int(pid) for pid in _STARTED_WORKER.findall(owned.log.read_text())), + lambda pids: len(pids) == 2, + seconds=30, + ) + burst: Final = asyncio.create_task( + _burst(_proxy_url(candidate), candidate.key, _PLAIN, calls, tolerate_transport_errors=True) + ) + await asyncio.to_thread(eventually, held_indexes.qsize, lambda size: size == 20, 60) + held_by: Final = MappingProxyType({pid: _open_peer_connections(pid, wire.url) for pid in workers}) + assert sum(held_by.values()) == 20, held_by + victim_pid, survivor_pid = sorted(workers, key=held_by.__getitem__) + psutil.Process(victim_pid).send_signal(signal.SIGKILL) + release.set() + served: Final = await burst + assert held_by[survivor_pid] >= 10, held_by + assert len(served) == held_by[survivor_pid], (held_by, len(served)) + for item in served: + assert item.status in (200, 400) and _is_rejection(item, _REJECTION), ( + item.call, + item.status, + item.text, + ) + eventually( + lambda: len(_STARTED_WORKER.findall(owned.log.read_text())), lambda count: count == 3, seconds=60 + ) + follow_up: Final = await _burst( + _proxy_url(candidate), candidate.key, _PLAIN, _calls(again, "chat", range(6)) + ) + assert len(follow_up) == 6 + for item in follow_up: + assert item.status in (200, 400) and _is_rejection(item, _REJECTION), ( + item.call, + item.status, + item.text, + ) + assert len(wire.drain()) == 26 + served_ids: Final = frozenset(item.call_id for item in (*served, *follow_up)) + assert len(served_ids) == len(served) + 6, served_ids + rows: Final = _rows_for(served_ids, expected=len(served_ids)) + assert {str(row["request_id"]) for row in rows} == served_ids, rows + assert all(row["status"] == "failure" for row in rows), rows + + +@pytest.mark.timeout(600) +async def test_c03_proxy_terminated_mid_burst_lands_every_served_rejection_at_most_once( + gateway: Gateway, tmp_path: Path +) -> None: + marker: Final = f"call-{uuid.uuid4().hex}" + calls: Final = _calls(marker, "chat", range(12)) + release: Final = threading.Event() + held_indexes: Final[SimpleQueue[int]] = SimpleQueue() + with wire_server(_held_rejection(release, held_indexes)) as wire: + config: Final = _owned_config(wire, tmp_path) + with owned_proxy_process(gateway, tmp_path, {}, config=config, workers=1) as owned: + candidate: Final = owned.gateway + burst: Final = asyncio.create_task( + _burst(_proxy_url(candidate), candidate.key, _PLAIN, calls, tolerate_transport_errors=True) + ) + await asyncio.to_thread(eventually, held_indexes.qsize, lambda size: size == 12, 60) + owned.process.terminate() + release.set() + served: Final = await burst + eventually(owned.process.poll, lambda code: code is not None, seconds=60) + assert len(served) <= 12 + for item in served: + assert _is_rejection(item, _REJECTION), (item.call, item.status, item.text) + served_ids: Final = frozenset(item.call_id for item in served if item.call_id) + landed: Final = tuple(str(row["request_id"]) for row in _rows_for(served_ids, expected=0)) + assert len(landed) == len(set(landed)), landed + assert set(landed) <= served_ids, (landed, served_ids) + assert len(wire.drain()) == 12 diff --git a/tests/integration/providers/test_responses_minted_reasoning_replay_chaos.py b/tests/integration/providers/test_responses_minted_reasoning_replay_chaos.py new file mode 100644 index 00000000000..d62b67e08d6 --- /dev/null +++ b/tests/integration/providers/test_responses_minted_reasoning_replay_chaos.py @@ -0,0 +1,523 @@ +import asyncio +import json +import re +import signal +import socket +import threading +import time +import uuid +from collections.abc import Iterator, Mapping +from dataclasses import dataclass +from pathlib import Path +from queue import SimpleQueue +from types import MappingProxyType +from typing import Final, Literal +from urllib.parse import urlsplit + +import httpx +import psutil +import pytest +import websockets +import yaml +from integration._support import claude_code as cc +from integration._support import responses_vendor as rv +from integration._support.client import Gateway, Scenario, eventually, gateway_from_environment +from integration._support.database import read_rows +from integration._support.process import OwnedProxy, owned_proxy_process +from integration._support.tls import server_context, write_self_signed_cert +from integration._support.wire import Reply, Request, Wire, wire_server +from pydantic import JsonValue + +_GPT: Final = "gpt-5.6" +_CODEX: Final = "gpt-5.3-codex" +_OPENAI_KEY: Final = "synthetic-openai-key" +_CONFIG_MODEL: Final = "responses-minted-reasoning-chaos" +_FOUNDRY_BASE: Final = "http://minted-reasoning-audit.services.ai.azure.com" +_STARTED_WORKER: Final = re.compile(r"Started server process \[(\d+)\]") +_CACHE_BUST: Final[Mapping[str, JsonValue]] = MappingProxyType({"cache": {"no-cache": True}}) + +Endpoint = Literal["responses", "chat", "messages"] + + +@dataclass(frozen=True, slots=True) +class _Call: + endpoint: Endpoint + stream: bool + marker: str + + +@dataclass(frozen=True, slots=True) +class _Served: + call: _Call + status: int + text: str + call_id: str + + +@dataclass(frozen=True, slots=True) +class _Models: + responses: str + chat: str + messages: str + + def of(self, endpoint: Endpoint) -> str: + match endpoint: + case "responses": + return self.responses + case "chat": + return self.chat + case "messages": + return self.messages + + +def _register(scenario: Scenario, api_base: str) -> _Models: + return _Models( + responses=scenario.model(model=f"openai/{_GPT}", api_base=api_base, api_key=_OPENAI_KEY), + chat=scenario.model(model=f"openai/{_CODEX}", api_base=api_base, api_key=_OPENAI_KEY), + messages=scenario.model(model=f"anthropic/{cc.OPUS}", api_base=api_base, api_key=cc.ANTHROPIC_API_KEY), + ) + + +def _path(endpoint: Endpoint) -> str: + match endpoint: + case "responses": + return "/v1/responses" + case "chat": + return "/v1/chat/completions" + case "messages": + return "/v1/messages" + + +def _body(models: _Models, call: _Call) -> dict[str, JsonValue]: + common: Final[dict[str, JsonValue]] = { + "model": models.of(call.endpoint), + "stream": call.stream, + "num_retries": 0, + **_CACHE_BUST, + } + match call.endpoint: + case "responses": + return {**common, "input": rv.agents_sdk_history(call.marker, rv.minted_item(call.marker))} + case "chat": + return { + **common, + "messages": [ + {"role": "user", "content": "Pick a city."}, + { + "role": "assistant", + "content": "Prague", + "reasoning_items": [ + {"type": "reasoning", "encrypted_content": f"gAAAAA-stored-{call.marker}", "summary": []} + ], + }, + {"role": "user", "content": f"Name a landmark marker-{call.marker}"}, + ], + } + case "messages": + return { + **common, + "max_tokens": 64, + "messages": [ + {"role": "user", "content": "Pick a city."}, + { + "role": "assistant", + "content": [ + {"type": "thinking", "thinking": rv.THOUGHT, "signature": rv.signature(call.marker)}, + {"type": "text", "text": "Prague"}, + ], + }, + {"role": "user", "content": f"Name a landmark marker-{call.marker}"}, + ], + } + + +def _calls(count: int, endpoints: tuple[Endpoint, ...]) -> tuple[_Call, ...]: + return tuple( + _Call(endpoint=endpoints[index % len(endpoints)], stream=index % 2 == 1, marker=uuid.uuid4().hex) + for index in range(count) + ) + + +async def _send(client: httpx.AsyncClient, key: str, models: _Models, call: _Call) -> _Served: + async with client.stream( + "POST", + _path(call.endpoint), + json=_body(models, call), + headers={"Authorization": f"Bearer {key}", "anthropic-version": "2023-06-01"}, + ) as response: + raw: Final = await response.aread() + return _Served(call, response.status_code, raw.decode(), response.headers.get("x-litellm-call-id", "")) + + +async def _burst( + base_url: str, key: str, models: _Models, calls: tuple[_Call, ...], *, tolerate_transport_errors: bool = False +) -> tuple[_Served, ...]: + async with httpx.AsyncClient(base_url=base_url, timeout=60, trust_env=False) as client: + results: Final = await asyncio.gather( + *(_send(client, key, models, call) for call in calls), return_exceptions=tolerate_transport_errors + ) + for result in results: + assert not isinstance(result, BaseException) or isinstance(result, httpx.TransportError), repr(result) + return tuple(result for result in results if isinstance(result, _Served)) + + +def _frames(text: str) -> list[dict[str, JsonValue]]: + return [rv.JSON_OBJECT.validate_json(line[6:]) for line in text.splitlines() if line.startswith("data: {")] + + +def _response_id(served: _Served) -> str: + if not served.call.stream: + return str(rv.JSON_OBJECT.validate_json(served.text)["id"]) + frames: Final = _frames(served.text) + match served.call.endpoint: + case "responses": + (completed,) = [frame for frame in frames if frame.get("type") == "response.completed"] + return str(rv.JSON_OBJECT.validate_python(completed["response"])["id"]) + case "chat": + return str(frames[0]["id"]) + case "messages": + (start,) = [frame for frame in frames if frame.get("type") == "message_start"] + return str(rv.JSON_OBJECT.validate_python(start["message"])["id"]) + + +def _assert_answered_with_its_own_marker(served: _Served) -> None: + assert served.status == 200, served.text + assert set(rv.MARKER.findall(served.text)) == {served.call.marker}, served.text + + +def _assert_forwarded_without_a_minted_item(request: Request, marker: str) -> None: + body: Final = rv.JSON_OBJECT.validate_json(request.body) + path: Final = urlsplit(request.target).path + assert "no-cache" not in request.body.decode(), request.body + if path.endswith("/messages"): + (assistant,) = [turn for turn in rv.ITEMS.validate_python(body["messages"]) if turn["role"] == "assistant"] + assert assistant["content"] == [ + {"type": "thinking", "thinking": rv.THOUGHT, "signature": rv.signature(marker)}, + {"type": "text", "text": "Prague"}, + ], assistant + return + assert path.endswith("/responses"), request.target + items: Final = rv.reasoning_items(body) + if body["model"] == _CODEX: + assert items == [{"type": "reasoning", "encrypted_content": f"gAAAAA-stored-{marker}", "summary": []}], items + return + assert items == [], body["input"] + + +def _spend_rows(models: _Models, expected: int) -> list[dict[str, JsonValue]]: + return eventually( + lambda: read_rows( + 'SELECT request_id, status FROM "LiteLLM_SpendLogs" WHERE model_group IN (%s, %s, %s)', + (models.responses, models.chat, models.messages), + ), + lambda found: len(found) >= expected, + seconds=70, + ) + + +def _assert_each_lands_once( + rows: list[dict[str, JsonValue]], failed: tuple[_Served, ...], served: tuple[_Served, ...] +) -> None: + by_status: Final = {str(row["request_id"]): str(row["status"]) for row in rows} + assert len(by_status) == len(rows) == len(failed) + len(served), rows + for item in failed: + assert by_status.get(item.call_id) == "failure", (item.call_id, rows) + for item in served: + (match,) = [request_id for request_id in by_status if rv.same_response(request_id, _response_id(item))] + assert by_status[match] == "success", rows + + +def _free_port() -> int: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as probe: + probe.bind(("127.0.0.1", 0)) + return int(probe.getsockname()[1]) + + +def _health_counts(gateway: Gateway, model: str) -> tuple[int, int]: + response: Final = gateway.request("GET", f"/health?model={model}", None) + assert response.status_code in (200, 503), response.text + health: Final = rv.JSON_OBJECT.validate_json(response.text) + return int(str(health["healthy_count"])), int(str(health["unhealthy_count"])) + + +def _marked(received: tuple[Request, ...]) -> dict[str, Request]: + marked: Final = {marker: request for request in received if (marker := rv.newest_marker(request.body.decode()))} + assert len(marked) == sum(1 for request in received if rv.newest_marker(request.body.decode())), received + return marked + + +@pytest.mark.timeout(180) +async def test_vendor_outage_fails_each_replay_cleanly_and_the_recovered_vendor_gets_them_without_minted_items( + gateway: Gateway, +) -> None: + port: Final = _free_port() + while_down: Final = _calls(15, ("responses", "chat", "messages")) + after: Final = _calls(15, ("responses", "chat", "messages")) + with gateway.scenario() as scenario: + models: Final = _register(scenario, f"http://127.0.0.1:{port}") + failed: Final = await _burst(str(gateway.client.base_url), gateway.key, models, while_down) + assert len(failed) == 15 + for item in failed: + assert item.status == 500 and "Cannot connect to host" in item.text, (item.status, item.text) + assert "answer marker" not in item.text, item.text + assert item.call_id, item + assert _health_counts(gateway, models.responses) == (0, 1) + with wire_server(rv.ResponsesVendor().respond, port=port) as wire: + assert _health_counts(gateway, models.responses) == (1, 0) + wire.drain() + served: Final = await _burst(str(gateway.client.base_url), gateway.key, models, after) + assert len(served) == 15 + for item in served: + _assert_answered_with_its_own_marker(item) + forwarded: Final = _marked(wire.drain()) + assert set(forwarded) == {call.marker for call in after}, sorted(forwarded) + for marker, request in forwarded.items(): + _assert_forwarded_without_a_minted_item(request, marker) + _assert_each_lands_once(_spend_rows(models, 30), failed, served) + + +async def test_slow_vendor_streams_are_each_forwarded_once_without_the_minted_item(gateway: Gateway) -> None: + calls: Final = tuple(_Call("responses", True, uuid.uuid4().hex) for _ in range(10)) + with wire_server(rv.ResponsesVendor(pause_between_chunks=0.3).respond) as wire, gateway.scenario() as scenario: + models: Final = _register(scenario, wire.url) + served: Final = await _burst(str(gateway.client.base_url), gateway.key, models, calls) + assert len(served) == 10 + for item in served: + _assert_answered_with_its_own_marker(item) + assert "response.completed" in item.text, item.text + received: Final = wire.drain() + assert len(received) == 10, [request.target for request in received] + forwarded: Final = _marked(received) + assert set(forwarded) == {call.marker for call in calls} + for marker, request in forwarded.items(): + _assert_forwarded_without_a_minted_item(request, marker) + _assert_each_lands_once(_spend_rows(models, 10), (), served) + + +def _chaos_config(wire: Wire, tmp_path: Path) -> Path: + config: Final = yaml.safe_load(Path("tests/integration/proxy_config.yaml").read_text()) + config["model_list"] = [ + { + "model_name": _CONFIG_MODEL, + "litellm_params": {"model": f"openai/{_GPT}", "api_base": wire.url, "api_key": _OPENAI_KEY}, + } + ] + path: Final = tmp_path / "responses-minted-reasoning-chaos.yaml" + path.write_text(yaml.safe_dump(config)) + return path + + +def _open_upstream_connections(pid: int, upstream: str) -> int: + port: Final = urlsplit(upstream).port + return sum( + 1 + for connection in psutil.Process(pid).net_connections(kind="tcp") + if connection.status == psutil.CONN_ESTABLISHED and connection.raddr and connection.raddr.port == port + ) + + +@pytest.mark.timeout(240) +async def test_worker_sigkill_mid_burst_leaves_the_sibling_dropping_the_minted_item( + gateway: Gateway, tmp_path: Path +) -> None: + calls: Final = tuple(_Call("responses", False, uuid.uuid4().hex) for _ in range(20)) + release: Final = threading.Event() + held_markers: Final[SimpleQueue[str]] = SimpleQueue() + vendor: Final = rv.ResponsesVendor() + + def held(request: Request) -> Reply: + if request.method == "GET": + return vendor.respond(request) + marker: Final = rv.newest_marker(request.body.decode()) + assert marker is not None, request.body + held_markers.put(marker) + assert release.wait(timeout=60), "The burst was never released" + return vendor.respond(request) + + with wire_server(held) as wire: + path: Final = _chaos_config(wire, tmp_path) + with owned_proxy_process(gateway, tmp_path, {}, config=path, workers=2) as owned: + candidate: Final = owned.gateway + models: Final = _Models(_CONFIG_MODEL, _CONFIG_MODEL, _CONFIG_MODEL) + workers: Final = eventually( + lambda: tuple(int(pid) for pid in _STARTED_WORKER.findall(owned.log.read_text())), + lambda pids: len(pids) == 2, + seconds=30, + ) + burst: Final = asyncio.create_task( + _burst(str(candidate.client.base_url), candidate.key, models, calls, tolerate_transport_errors=True) + ) + await asyncio.to_thread(eventually, held_markers.qsize, lambda size: size == 20, 60) + held_by: Final = MappingProxyType({pid: _open_upstream_connections(pid, wire.url) for pid in workers}) + assert sum(held_by.values()) == 20, held_by + victim_pid, survivor_pid = sorted(workers, key=held_by.__getitem__) + victim: Final = psutil.Process(victim_pid) + victim.suspend() + victim.send_signal(signal.SIGKILL) + release.set() + served: Final = await burst + assert held_by[survivor_pid] >= 10, held_by + assert len(served) == held_by[survivor_pid], (held_by, len(served)) + for item in served: + _assert_answered_with_its_own_marker(item) + follow_up: Final = _Call("responses", False, uuid.uuid4().hex) + (answered,) = await _burst(str(candidate.client.base_url), candidate.key, models, (follow_up,)) + _assert_answered_with_its_own_marker(answered) + forwarded: Final = _marked(tuple(request for request in wire.drain() if request.method == "POST")) + assert set(forwarded) == {call.marker for call in (*calls, follow_up)}, sorted(forwarded) + for marker, request in forwarded.items(): + _assert_forwarded_without_a_minted_item(request, marker) + + +@dataclass(frozen=True, slots=True) +class _Rig: + wire: Wire + proxy: OwnedProxy + cert: Path + key: Path + + +@pytest.fixture(scope="module") +def rig(tmp_path_factory: pytest.TempPathFactory) -> Iterator[_Rig]: + directory: Final = tmp_path_factory.mktemp("minted-reasoning-rig") + cert, key = write_self_signed_cert(directory) + copilot: Final = directory / "copilot" + chatgpt: Final = directory / "chatgpt" + copilot.mkdir() + chatgpt.mkdir() + with gateway_from_environment() as gateway, wire_server(rv.ResponsesVendor().respond) as wire: + (copilot / "api-key.json").write_text( + json.dumps( + {"token": "synthetic-copilot-token", "expires_at": time.time() + 3600, "endpoints": {"api": wire.url}} + ) + ) + (chatgpt / "auth.json").write_text( + json.dumps( + { + "access_token": "synthetic-chatgpt-token", + "account_id": "acct-synthetic", + "expires_at": time.time() + 3600, + } + ) + ) + overrides: Final = { + "GITHUB_COPILOT_TOKEN_DIR": str(copilot), + "CHATGPT_TOKEN_DIR": str(chatgpt), + "CHATGPT_API_BASE": wire.url, + "SSL_CERT_FILE": str(cert), + "HTTP_PROXY": wire.url, + "NO_PROXY": "127.0.0.1,localhost", + } + with owned_proxy_process(gateway, directory, overrides, workers=2) as owned: + yield _Rig(wire, owned, cert, key) + + +def _replay(gateway: Gateway, model: str, history: list[dict[str, JsonValue]], stream: bool) -> httpx.Response: + return gateway.request("POST", "/v1/responses", {"model": model, "input": history, "stream": stream, **_CACHE_BUST}) + + +@dataclass(frozen=True, slots=True) +class _LoginDeployment: + label: str + model: str + api_key: str | None + + +_LOGIN_DEPLOYMENTS: Final = ( + _LoginDeployment("github_copilot", f"github_copilot/{_CODEX}", None), + _LoginDeployment("chatgpt", f"chatgpt/{_CODEX}", None), + _LoginDeployment("azure_ai-foundry-host", "azure_ai/deepseek-v3", "synthetic-azure-key"), +) + + +@pytest.mark.timeout(240) +@pytest.mark.parametrize("stream", [False, True], ids=["sync", "stream"]) +@pytest.mark.parametrize("deployment", _LOGIN_DEPLOYMENTS, ids=[deployment.label for deployment in _LOGIN_DEPLOYMENTS]) +def test_login_backed_and_foundry_deployments_forward_the_minted_item_unchanged( + rig: _Rig, deployment: _LoginDeployment, stream: bool +) -> None: + marker: Final = uuid.uuid4().hex + minted: Final = rv.minted_item(marker, summary=[]) + history: Final = rv.agents_sdk_history(marker, minted) + api_base: Final = _FOUNDRY_BASE if deployment.label.startswith("azure_ai") else rig.wire.url + rig.wire.drain() + with rig.proxy.gateway.scenario() as scenario: + parameters: Final[dict[str, JsonValue]] = {"model": deployment.model, "api_base": api_base} + model: Final = scenario.model( + **parameters, **({} if deployment.api_key is None else {"api_key": deployment.api_key}) + ) + response: Final = _replay(rig.proxy.gateway, model, history, stream) + received: Final = rig.wire.drain() + assert len(received) == 1, [(request.method, request.target) for request in received] + target: Final = urlsplit(received[0].target) + assert target.path.endswith("/responses"), received[0].target + if deployment.label.startswith("azure_ai"): + assert target.scheme == "http" and target.netloc == urlsplit(_FOUNDRY_BASE).netloc, received[0].target + items: Final = rv.reasoning_items(rv.JSON_OBJECT.validate_json(received[0].body)) + assert items == [minted], items + assert response.status_code == 404, response.text + assert f"Item with id '{minted['id']}' not found" in response.text, response.text + + +@pytest.mark.timeout(240) +async def test_websocket_session_forwards_the_minted_item_as_before(rig: _Rig) -> None: + marker: Final = uuid.uuid4().hex + minted: Final = rv.minted_item(marker) + history: Final = rv.agents_sdk_history(marker, minted) + frames: Final[SimpleQueue[tuple[str, str]]] = SimpleQueue() + + async def vendor(connection: websockets.ServerConnection) -> None: + first: Final = await connection.recv() + frames.put((str(connection.request.path), str(first))) + tag: Final = uuid.uuid4().hex + response: Final[dict[str, JsonValue]] = { + "id": f"resp_{tag}", + "object": "response", + "created_at": 1, + "status": "completed", + "model": _GPT, + "output": [ + { + "id": f"msg_{tag}", + "type": "message", + "role": "assistant", + "status": "completed", + "content": [{"type": "output_text", "text": rv.answer(marker), "annotations": []}], + } + ], + "usage": rv.USAGE, + } + created: Final = { + "type": "response.created", + "sequence_number": 0, + "response": {**response, "status": "in_progress", "output": []}, + } + await connection.send(json.dumps(created)) + await connection.send(json.dumps({"type": "response.completed", "sequence_number": 1, "response": response})) + await connection.wait_closed() + + gateway: Final = rig.proxy.gateway + async with websockets.serve(vendor, "127.0.0.1", 0, ssl=server_context(rig.cert, rig.key)) as server: + port: Final = server.sockets[0].getsockname()[1] + with gateway.scenario() as scenario: + model: Final = scenario.model( + model=f"openai/{_GPT}", api_base=f"https://127.0.0.1:{port}", api_key=_OPENAI_KEY + ) + session_url: Final = ( + f"{str(gateway.client.base_url).rstrip('/').replace('http://', 'ws://')}/v1/responses?model={model}" + ) + async with websockets.connect( + session_url, additional_headers={"Authorization": f"Bearer {gateway.key}"} + ) as session: + await session.send(json.dumps({"type": "response.create", "model": model, "input": history})) + received: Final[list[dict[str, JsonValue]]] = [] + while not received or received[-1].get("type") != "response.completed": + received.append(rv.JSON_OBJECT.validate_json(str(await session.recv()))) + assert [event["type"] for event in received] == ["response.created", "response.completed"], received + completed: Final = rv.JSON_OBJECT.validate_python(received[-1]["response"]) + (message,) = rv.ITEMS.validate_python(completed["output"]) + assert rv.ITEMS.validate_python(message["content"])[0]["text"] == rv.answer(marker), message + assert frames.qsize() == 1 + path, first = frames.get_nowait() + assert path.startswith("/responses?") and f"model={_GPT}" in path, path + assert rv.JSON_OBJECT.validate_json(first)["input"] == history, first diff --git a/tests/integration/providers/test_responses_minted_reasoning_replay_wire.py b/tests/integration/providers/test_responses_minted_reasoning_replay_wire.py new file mode 100644 index 00000000000..036f96916a6 --- /dev/null +++ b/tests/integration/providers/test_responses_minted_reasoning_replay_wire.py @@ -0,0 +1,783 @@ +import json +import threading +import time +import uuid +from collections import deque +from collections.abc import Mapping, Sequence +from dataclasses import dataclass +from types import EllipsisType, MappingProxyType +from typing import Final +from urllib.parse import urlsplit + +import anthropic +import httpx +import openai +import pytest +from integration._support import claude_code as cc +from integration._support import responses_vendor as rv +from integration._support.client import Gateway, Scenario, eventually +from integration._support.database import read_rows +from integration._support.wire import Request, Wire, wire_server +from pydantic import JsonValue, TypeAdapter + +_GPT: Final = "gpt-5.6" +_CODEX: Final = "gpt-5.3-codex" +_CLAUDE: Final = cc.OPUS +_OPENAI_KEY: Final = "synthetic-openai-key" +_AZURE_KEY: Final = "synthetic-azure-key" +_CACHE_BUST: Final[Mapping[str, JsonValue]] = MappingProxyType({"cache": {"no-cache": True}}) +_JSON_OBJECT: Final = TypeAdapter(dict[str, JsonValue]) +_ITEMS: Final = TypeAdapter(list[dict[str, JsonValue]]) + + +@dataclass(frozen=True, slots=True) +class _Deployment: + label: str + model: str + api_key: str + target: str + extra: Mapping[str, JsonValue] = MappingProxyType({}) + strips_message_status: bool = False + types_untyped_items_as_messages: bool = False + model_info: Mapping[str, JsonValue] | None = None + + def register(self, scenario: Scenario, wire: Wire) -> str: + return scenario.model( + model=self.model, api_base=wire.url, api_key=self.api_key, model_info=self.model_info, **dict(self.extra) + ) + + def on_wire(self, items: Sequence[JsonValue]) -> list[JsonValue]: + return [self._as_sent(item) for item in items] + + def _as_sent(self, item: JsonValue) -> JsonValue: + if not isinstance(item, dict): + return item + if self.strips_message_status and item.get("type") == "message": + return {key: value for key, value in item.items() if key != "status"} + if self.types_untyped_items_as_messages and "type" not in item: + return {**item, "type": "message"} + return item + + +_OPENAI: Final = _Deployment("openai", f"openai/{_GPT}", _OPENAI_KEY, "/responses") +_AZURE: Final = _Deployment( + "azure", + f"azure/{_GPT}", + _AZURE_KEY, + "/openai/v1/responses?api-version=preview", + MappingProxyType({"api_version": "preview"}), + strips_message_status=True, +) +_AZURE_AI_OPENAI_HOST: Final = _Deployment( + "azure_ai-rewritten-to-azure", + f"azure_ai/{_GPT}", + _AZURE_KEY, + "/openai/v1/responses?api-version=preview", + strips_message_status=True, +) +_DROPPING: Final = (_OPENAI, _AZURE, _AZURE_AI_OPENAI_HOST) +_KEEPING: Final = ( + _Deployment("litellm_proxy", f"litellm_proxy/{_GPT}", "synthetic-proxy-key", "/responses"), + _Deployment("databricks", "databricks/gpt-5.6", "synthetic-databricks-key", "/responses"), + _Deployment("openrouter", f"openrouter/openai/{_GPT}", "synthetic-openrouter-key", "/responses"), + _Deployment("xai", "xai/grok-4.7", "synthetic-xai-key", "/responses"), + _Deployment("hosted_vllm", "hosted_vllm/qwen3", "synthetic-vllm-key", "/responses"), + _Deployment("fireworks_ai", "fireworks_ai/accounts/fireworks/models/kimi", "synthetic-fireworks-key", "/responses"), + _Deployment("volcengine", "volcengine/doubao", "synthetic-volcengine-key", "/responses"), + _Deployment("manus", "manus/manus-1", "synthetic-manus-key", "/responses"), + _Deployment("edenai", "edenai/openai/gpt-5.6", "synthetic-edenai-key", "/responses"), + _Deployment( + "perplexity", + "perplexity/sonar-pro", + "synthetic-perplexity-key", + "/v1/responses", + types_untyped_items_as_messages=True, + ), + _Deployment("bedrock_mantle", "bedrock_mantle/openai.gpt-oss-120b", "synthetic-mantle-key", "/v1/responses"), + _Deployment( + "bedrock", + "bedrock/openai.gpt-oss-120b-1:0", + "synthetic-bedrock-key", + "/openai/v1/responses", + MappingProxyType({"aws_region_name": "us-east-1"}), + model_info=MappingProxyType({"supported_endpoints": ["/v1/responses"]}), + ), + *( + _Deployment(slug, f"{slug}/{model}", f"synthetic-{slug}-key", "/responses") + for slug, model in ( + ("sail", "sail-1"), + ("neosantara", "nusantara-base"), + ("tensormesh", "qwen3"), + ("parasail", "parasail-gpt-oss-120b"), + ("empiriolabs", "empirio-1"), + ("meta", "llama-4-maverick"), + ("cortecs", "gpt-oss-120b"), + ("pinstripes", "gpt-5.6"), + ("prism", "gpt-oss-120b"), + ) + ), +) + + +def _base_url(gateway: Gateway) -> str: + return str(gateway.client.base_url).rstrip("/") + + +def _sdk(gateway: Gateway) -> openai.OpenAI: + return openai.OpenAI( + base_url=f"{_base_url(gateway)}/v1", + api_key=gateway.key, + max_retries=0, + http_client=httpx.Client(trust_env=False, timeout=60), + ) + + +def _async_sdk(gateway: Gateway) -> openai.AsyncOpenAI: + return openai.AsyncOpenAI( + base_url=f"{_base_url(gateway)}/v1", + api_key=gateway.key, + max_retries=0, + http_client=httpx.AsyncClient(trust_env=False, timeout=60), + ) + + +def _claude_sdk(gateway: Gateway) -> anthropic.Anthropic: + return anthropic.Anthropic( + base_url=_base_url(gateway), + api_key=gateway.key, + max_retries=0, + http_client=httpx.Client(trust_env=False, timeout=60), + ) + + +def _create( + client: openai.OpenAI, model: str, history: Sequence[Mapping[str, JsonValue]], stream: bool +) -> dict[str, JsonValue]: + if not stream: + return client.responses.create(model=model, input=list(history), extra_body=dict(_CACHE_BUST)).model_dump() + events: Final = list( + client.responses.create(model=model, input=list(history), stream=True, extra_body=dict(_CACHE_BUST)) + ) + completed: Final = [event for event in events if event.type == "response.completed"] + assert len(completed) == 1, [event.type for event in events] + return completed[0].response.model_dump() + + +async def _create_async( + client: openai.AsyncOpenAI, model: str, history: Sequence[Mapping[str, JsonValue]], stream: bool +) -> dict[str, JsonValue]: + if not stream: + return ( + await client.responses.create(model=model, input=list(history), extra_body=dict(_CACHE_BUST)) + ).model_dump() + events: Final = [ + event + async for event in await client.responses.create( + model=model, input=list(history), stream=True, extra_body=dict(_CACHE_BUST) + ) + ] + completed: Final = [event for event in events if event.type == "response.completed"] + assert len(completed) == 1, [event.type for event in events] + return completed[0].response.model_dump() + + +def _raw( + gateway: Gateway, path: str, body: Mapping[str, JsonValue], *, key: str | None | EllipsisType = ... +) -> httpx.Response: + with httpx.Client(base_url=_base_url(gateway), trust_env=False, timeout=60) as client: + bearer: Final = gateway.key if key is ... else key + headers: Final = {} if bearer is None else {"Authorization": f"Bearer {bearer}"} + with client.stream("POST", path, json={**body, **_CACHE_BUST}, headers=headers) as response: + response.read() + return response + + +def _completed_payload(response: httpx.Response) -> dict[str, JsonValue]: + if not response.headers.get("content-type", "").startswith("text/event-stream"): + return _JSON_OBJECT.validate_json(response.content) + frames: Final = [json.loads(line[6:]) for line in response.text.splitlines() if line.startswith("data: {")] + completed: Final = [frame for frame in frames if frame.get("type") == "response.completed"] + assert len(completed) == 1, [frame.get("type") for frame in frames] + return _JSON_OBJECT.validate_python(completed[0]["response"]) + + +def _answer_text(payload: Mapping[str, JsonValue]) -> str: + messages: Final = [item for item in _ITEMS.validate_python(payload["output"]) if item.get("type") == "message"] + assert len(messages) == 1, payload + return str(_ITEMS.validate_python(messages[0]["content"])[0]["text"]) + + +def _only_request(wire: Wire) -> tuple[Request, dict[str, JsonValue]]: + received: Final = wire.drain() + assert len(received) == 1, [(request.method, request.target) for request in received] + return received[0], _JSON_OBJECT.validate_json(received[0].body) + + +def _assert_spend_rows(model: str, response_ids: Sequence[str]) -> None: + rows: Final = eventually( + lambda: read_rows('SELECT request_id, status FROM "LiteLLM_SpendLogs" WHERE model_group=%s', (model,)), + lambda found: len(found) >= len(response_ids), + seconds=70, + ) + logged: Final = {str(row["request_id"]): str(row["status"]) for row in rows} + assert len(logged) == len(rows) == len(response_ids), rows + for response_id in response_ids: + (match,) = [logged_id for logged_id in logged if rv.same_response(logged_id, response_id)] + assert logged[match] == "success", rows + + +def _assert_vendor_body( + body: Mapping[str, JsonValue], backend: str, forwarded: Sequence[JsonValue], stream: bool +) -> None: + assert body["model"] == backend, body + assert body["input"] == list(forwarded), body["input"] + assert body.get("stream", False) is stream, body + assert "cache" not in body and "no-cache" not in json.dumps(body), body + + +def _backend_of(deployment: _Deployment) -> str: + return deployment.model.split("/", 1)[1] + + +@pytest.mark.parametrize("stream", [False, True], ids=["sync", "stream"]) +@pytest.mark.parametrize("deployment", _DROPPING, ids=[deployment.label for deployment in _DROPPING]) +def test_agents_sdk_history_replays_to_openai_shaped_vendors_without_the_minted_item( + gateway: Gateway, deployment: _Deployment, stream: bool +) -> None: + marker: Final = uuid.uuid4().hex + minted: Final = rv.minted_item(marker) + history: Final = rv.agents_sdk_history(marker, minted) + with wire_server(rv.ResponsesVendor().respond) as wire, gateway.scenario() as scenario: + model: Final = deployment.register(scenario, wire) + payload: Final = _create(_sdk(gateway), model, history, stream) + assert _answer_text(payload) == f"answer marker-{marker}", payload + request, body = _only_request(wire) + assert request.target == deployment.target, request.target + _assert_vendor_body(body, _backend_of(deployment), deployment.on_wire(rv.without(history, (minted,))), stream) + _assert_spend_rows(model, (str(payload["id"]),)) + + +@pytest.mark.parametrize("stream", [False, True], ids=["sync", "stream"]) +async def test_async_openai_sdk_replays_without_the_minted_item(gateway: Gateway, stream: bool) -> None: + marker: Final = uuid.uuid4().hex + minted: Final = rv.minted_item(marker) + history: Final = rv.agents_sdk_history(marker, minted) + with wire_server(rv.ResponsesVendor().respond) as wire, gateway.scenario() as scenario: + model: Final = _OPENAI.register(scenario, wire) + payload: Final = await _create_async(_async_sdk(gateway), model, history, stream) + assert _answer_text(payload) == f"answer marker-{marker}", payload + request, body = _only_request(wire) + assert request.target == "/responses", request.target + _assert_vendor_body(body, _GPT, rv.without(history, (minted,)), stream) + + +@pytest.mark.parametrize("path", ["/v1/responses", "/responses", "/openai/v1/responses"]) +def test_every_responses_route_alias_drops_the_minted_item(gateway: Gateway, path: str) -> None: + marker: Final = uuid.uuid4().hex + minted: Final = rv.minted_item(marker) + history: Final = rv.agents_sdk_history(marker, minted) + with wire_server(rv.ResponsesVendor().respond) as wire, gateway.scenario() as scenario: + model: Final = _OPENAI.register(scenario, wire) + response: Final = _raw(gateway, path, {"model": model, "input": history}) + assert response.status_code == 200, response.text + assert _answer_text(_completed_payload(response)) == f"answer marker-{marker}" + _, body = _only_request(wire) + _assert_vendor_body(body, _GPT, rv.without(history, (minted,)), False) + + +def test_identical_replays_each_land_one_spend_row(gateway: Gateway) -> None: + marker: Final = uuid.uuid4().hex + history: Final = rv.agents_sdk_history(marker, rv.minted_item(marker)) + with wire_server(rv.ResponsesVendor().respond) as wire, gateway.scenario() as scenario: + model: Final = _OPENAI.register(scenario, wire) + first: Final = _completed_payload(_raw(gateway, "/v1/responses", {"model": model, "input": history})) + second: Final = _completed_payload(_raw(gateway, "/v1/responses", {"model": model, "input": history})) + assert first["id"] != second["id"] + assert len(wire.drain()) == 2 + _assert_spend_rows(model, (str(first["id"]), str(second["id"]))) + + +def _decoded_thinking(item: Mapping[str, JsonValue]) -> list[dict[str, JsonValue]]: + encrypted: Final = item["encrypted_content"] + assert isinstance(encrypted, str), item + return _ITEMS.validate_json(encrypted) + + +@pytest.mark.parametrize("stream", [False, True], ids=["sync", "stream"]) +def test_claude_turn_replays_to_openai_without_its_item_and_to_claude_with_its_thinking( + gateway: Gateway, stream: bool +) -> None: + marker: Final = uuid.uuid4().hex + with wire_server(rv.ResponsesVendor().respond) as wire, gateway.scenario() as scenario: + claude: Final = scenario.model(model=f"anthropic/{_CLAUDE}", api_base=wire.url, api_key=cc.ANTHROPIC_API_KEY) + gpt: Final = _OPENAI.register(scenario, wire) + question: Final[dict[str, JsonValue]] = {"role": "user", "content": f"Pick a city marker-{marker}"} + produced: Final = _completed_payload( + _raw(gateway, "/v1/responses", {"model": claude, "input": [question], "stream": stream}) + ) + reasoning, message = _ITEMS.validate_python(produced["output"]) + assert reasoning["type"] == "reasoning" and rv.MINTED_ID.match(str(reasoning["id"])), reasoning + assert "summary" not in reasoning, reasoning + (block,) = _decoded_thinking(reasoning) + assert (block["type"], block["signature"]) == ("thinking", rv.signature(marker)), block + assert message["type"] == "message", message + producing_request, producing_body = _only_request(wire) + assert producing_request.target == "/v1/messages" + + follow_up: Final = uuid.uuid4().hex + history: Final[list[dict[str, JsonValue]]] = [ + question, + reasoning, + message, + {"role": "user", "content": f"Name a landmark marker-{follow_up}"}, + ] + to_openai: Final = _raw(gateway, "/v1/responses", {"model": gpt, "input": history, "stream": stream}) + assert to_openai.status_code == 200, to_openai.text + assert _answer_text(_completed_payload(to_openai)) == f"answer marker-{follow_up}" + openai_request, openai_body = _only_request(wire) + assert openai_request.target == "/responses" + _assert_vendor_body(openai_body, _GPT, [question, message, history[3]], stream) + + to_claude: Final = _raw(gateway, "/v1/responses", {"model": claude, "input": history, "stream": stream}) + assert to_claude.status_code == 200, to_claude.text + claude_request, claude_body = _only_request(wire) + assert claude_request.target == "/v1/messages" + messages: Final = _ITEMS.validate_python(claude_body["messages"]) + assistant: Final = [turn for turn in messages if turn["role"] == "assistant"] + assert len(assistant) == 1, messages + assert assistant[0]["content"] == [ + {"type": "thinking", "thinking": block["thinking"], "signature": rv.signature(marker)}, + {"type": "text", "text": _answer_text(produced)}, + ], assistant[0] + + +@pytest.mark.parametrize("stream", [False, True], ids=["sync", "stream"]) +@pytest.mark.parametrize("deployment", _KEEPING, ids=[deployment.label for deployment in _KEEPING]) +def test_other_responses_providers_forward_the_minted_item_unchanged( + gateway: Gateway, deployment: _Deployment, stream: bool +) -> None: + marker: Final = uuid.uuid4().hex + minted: Final = rv.minted_item(marker, summary=[]) + history: Final = rv.agents_sdk_history(marker, minted) + with wire_server(rv.ResponsesVendor().respond) as wire, gateway.scenario() as scenario: + model: Final = deployment.register(scenario, wire) + response: Final = _raw(gateway, "/v1/responses", {"model": model, "input": history, "stream": stream}) + request, body = _only_request(wire) + assert urlsplit(request.target).path.endswith("/responses"), request.target + assert body["input"] == deployment.on_wire(history), body["input"] + assert response.status_code == 404, response.text + assert f"Item with id '{minted['id']}' not found" in response.text, response.text + + +@pytest.mark.parametrize( + ("prefix", "forwarded_blocks"), + [ + ("litellm_proxy", ("thinking", "text", "tool_use")), + ("openai", ("text", "tool_use")), + ], +) +def test_chained_hop_through_this_proxy_to_claude( + gateway: Gateway, prefix: str, forwarded_blocks: tuple[str, ...] +) -> None: + marker: Final = uuid.uuid4().hex + minted: Final = rv.minted_item(marker) + history: Final = rv.agents_sdk_history(marker, minted) + with wire_server(rv.ResponsesVendor().respond) as wire, gateway.scenario() as scenario: + claude: Final = scenario.model(model=f"anthropic/{_CLAUDE}", api_base=wire.url, api_key=cc.ANTHROPIC_API_KEY) + outer: Final = scenario.model(model=f"{prefix}/{claude}", api_base=_base_url(gateway), api_key=gateway.key) + response: Final = _raw(gateway, "/v1/responses", {"model": outer, "input": history}) + assert response.status_code == 200, response.text + assert _answer_text(_completed_payload(response)) == f"answer marker-{marker}" + request, body = _only_request(wire) + assert request.target == "/v1/messages" + assistant: Final = [turn for turn in _ITEMS.validate_python(body["messages"]) if turn["role"] == "assistant"] + assert len(assistant) == 1, body["messages"] + blocks: Final = _ITEMS.validate_python(assistant[0]["content"]) + assert tuple(str(block["type"]) for block in blocks) == forwarded_blocks, blocks + if "thinking" in forwarded_blocks: + assert blocks[0] == {"type": "thinking", "thinking": rv.THOUGHT, "signature": rv.signature(marker)}, blocks[ + 0 + ] + + +@dataclass(frozen=True, slots=True) +class _Hostile: + label: str + item: dict[str, JsonValue] + status: int + forwarded: bool + detail: str = "" + on_wire: Mapping[str, JsonValue] | None = None + + +def _hostile_cases() -> tuple[_Hostile, ...]: + marker: Final = "0" * 32 + signed: Final = {"type": "thinking", "thinking": rv.THOUGHT, "signature": rv.signature(marker)} + unsigned: Final = {"type": "thinking", "thinking": rv.THOUGHT} + summary: Final[list[JsonValue]] = [{"type": "summary_text", "text": "thought about it"}] + big_blob: Final = "x" * 5000 + big_blocks: Final = json.dumps([signed] * 60) + assert len(big_blocks) > 5000 + return ( + _Hostile( + "uppercase-uuid4-id", + {"type": "reasoning", "id": f"rs_{str(uuid.uuid4()).upper()}", "summary": []}, + 404, + True, + "Item with id", + ), + _Hostile( + "minted-id-with-summary", {"type": "reasoning", "id": f"rs_{uuid.uuid4()}", "summary": summary}, 200, False + ), + _Hostile( + "idless-opaque-blob", {"type": "reasoning", "encrypted_content": "gAAAAA-opaque", "summary": []}, 200, True + ), + _Hostile( + "idless-unverifiable-blocks", + {"type": "reasoning", "encrypted_content": json.dumps([unsigned]), "summary": []}, + 200, + True, + ), + _Hostile( + "idless-mixed-blocks", + { + "type": "reasoning", + "encrypted_content": json.dumps([unsigned, {"type": "text", "text": "x"}, signed]), + "summary": [], + }, + 200, + False, + ), + _Hostile("int-id", {"type": "reasoning", "id": 7, "summary": []}, 400, True, "input"), + _Hostile("list-id", {"type": "reasoning", "id": ["rs_x"], "summary": []}, 400, True, "input"), + _Hostile("empty-id", {"type": "reasoning", "id": "", "summary": summary}, 400, True, "empty string"), + _Hostile("int-encrypted-content", {"type": "reasoning", "encrypted_content": 7, "summary": []}, 200, True), + _Hostile( + "list-encrypted-content", {"type": "reasoning", "encrypted_content": [signed], "summary": []}, 200, True + ), + _Hostile("empty-encrypted-content", {"type": "reasoning", "encrypted_content": "", "summary": []}, 200, True), + _Hostile("five-kb-blob", {"type": "reasoning", "encrypted_content": big_blob, "summary": []}, 200, True), + _Hostile( + "five-kb-signed-blocks", {"type": "reasoning", "encrypted_content": big_blocks, "summary": []}, 200, False + ), + _Hostile( + "null-id-null-encrypted", + {"type": "reasoning", "id": None, "encrypted_content": None, "summary": []}, + 200, + True, + on_wire={"type": "reasoning", "id": None, "summary": []}, + ), + _Hostile( + "message-with-minted-looking-id", + { + "type": "message", + "id": f"rs_{uuid.uuid4()}", + "role": "assistant", + "content": [{"type": "output_text", "text": "x", "annotations": []}], + }, + 200, + True, + ), + ) + + +_HOSTILE: Final = _hostile_cases() + + +@pytest.mark.parametrize("case", _HOSTILE, ids=[case.label for case in _HOSTILE]) +def test_hostile_reasoning_items_reach_the_vendor_or_are_dropped_as_classified( + gateway: Gateway, case: _Hostile +) -> None: + marker: Final = uuid.uuid4().hex + history: Final = rv.agents_sdk_history(marker, case.item) + with wire_server(rv.ResponsesVendor().respond) as wire, gateway.scenario() as scenario: + model: Final = _OPENAI.register(scenario, wire) + response: Final = _raw(gateway, "/v1/responses", {"model": model, "input": history}) + assert response.status_code == case.status, response.text + assert case.detail in response.text, response.text + received: Final = wire.drain() + if response.status_code >= 400 and not received: + return + assert len(received) == 1, [(request.method, request.target) for request in received] + body: Final = _JSON_OBJECT.validate_json(received[0].body) + expected: Final = ( + [case.on_wire if item is case.item and case.on_wire is not None else item for item in history] + if case.forwarded + else rv.without(history, (case.item,)) + ) + assert body["input"] == expected, body["input"] + assert response.status_code == case.status + if case.status == 200: + assert _answer_text(_completed_payload(response)) == f"answer marker-{marker}" + unrelated: Final = _raw(gateway, "/v1/responses", {"model": model, "input": f"ping marker-{marker}"}) + assert unrelated.status_code == 200, unrelated.text + + +def test_vendor_owned_reasoning_item_from_a_producing_turn_is_kept(gateway: Gateway) -> None: + marker: Final = uuid.uuid4().hex + with wire_server(rv.ResponsesVendor().respond) as wire, gateway.scenario() as scenario: + model: Final = _OPENAI.register(scenario, wire) + question: Final[dict[str, JsonValue]] = {"role": "user", "content": f"Pick a city marker-{marker}"} + produced: Final = _completed_payload(_raw(gateway, "/v1/responses", {"model": model, "input": [question]})) + reasoning, message = _ITEMS.validate_python(produced["output"]) + assert str(reasoning["id"]).startswith("rs_") and not rv.MINTED_ID.match(str(reasoning["id"])), reasoning + wire.drain() + follow_up: Final = uuid.uuid4().hex + history: Final[list[dict[str, JsonValue]]] = [ + question, + reasoning, + message, + {"role": "user", "content": f"Name a landmark marker-{follow_up}"}, + ] + response: Final = _raw(gateway, "/v1/responses", {"model": model, "input": history}) + assert response.status_code == 200, response.text + _, body = _only_request(wire) + assert body["input"] == history, body["input"] + + +def test_two_minted_items_are_both_dropped_and_a_minted_only_history_goes_out_empty(gateway: Gateway) -> None: + marker: Final = uuid.uuid4().hex + first: Final = rv.minted_item(marker) + second: Final = rv.minted_item(marker) + history: Final = rv.agents_sdk_history(marker, first, second) + with wire_server(rv.ResponsesVendor().respond) as wire, gateway.scenario() as scenario: + model: Final = _OPENAI.register(scenario, wire) + response: Final = _raw(gateway, "/v1/responses", {"model": model, "input": history}) + assert response.status_code == 200, response.text + _, body = _only_request(wire) + assert body["input"] == rv.without(history, (first, second)), body["input"] + + lonely: Final = _raw(gateway, "/v1/responses", {"model": model, "input": [rv.minted_item(marker)]}) + assert lonely.status_code == 400, lonely.text + assert "previous_response_id" in lonely.text and "must be provided" in lonely.text, lonely.text + _, lonely_body = _only_request(wire) + assert lonely_body["input"] == [], lonely_body + + +def test_a_megabyte_of_minted_thinking_is_dropped_while_the_proxy_stays_responsive(gateway: Gateway) -> None: + marker: Final = uuid.uuid4().hex + block: Final = {"type": "thinking", "thinking": "t" * 4000, "signature": rv.signature(marker)} + encrypted: Final = json.dumps([block] * 256) + assert len(encrypted) > 1_000_000 + minted: Final[dict[str, JsonValue]] = { + "type": "reasoning", + "id": f"rs_{uuid.uuid4()}", + "encrypted_content": encrypted, + } + history: Final = rv.agents_sdk_history(marker, minted) + latencies: Final[deque[float]] = deque() + done: Final = threading.Event() + + def probe() -> None: + with httpx.Client(base_url=_base_url(gateway), trust_env=False, timeout=30) as client: + while not done.is_set(): + started: Final = time.monotonic() + assert client.get("/health/liveliness").status_code == 200 + latencies.append(time.monotonic() - started) + + with wire_server(rv.ResponsesVendor().respond) as wire, gateway.scenario() as scenario: + model: Final = _OPENAI.register(scenario, wire) + prober: Final = threading.Thread(target=probe) + prober.start() + started: Final = time.monotonic() + response: Final = _raw(gateway, "/v1/responses", {"model": model, "input": history}) + elapsed: Final = time.monotonic() - started + done.set() + prober.join(timeout=35) + assert response.status_code == 200, response.text[:500] + assert elapsed < 20, elapsed + assert latencies and max(latencies) < 5, (max(latencies), len(latencies)) + _, body = _only_request(wire) + assert body["input"] == rv.without(history, (minted,)) + + +def test_unauthenticated_replay_never_reaches_the_vendor_and_other_keys_keep_working(gateway: Gateway) -> None: + marker: Final = uuid.uuid4().hex + history: Final = rv.agents_sdk_history(marker, rv.minted_item(marker)) + with wire_server(rv.ResponsesVendor().respond) as wire, gateway.scenario() as scenario: + model: Final = _OPENAI.register(scenario, wire) + other: Final = scenario.key(models=[model]) + anonymous: Final = _raw(gateway, "/v1/responses", {"model": model, "input": history}, key=None) + assert anonymous.status_code == 401, anonymous.text + forged: Final = _raw(gateway, "/v1/responses", {"model": model, "input": history}, key="sk-not-a-key") + assert forged.status_code == 401, forged.text + assert wire.drain() == () + failing: Final = _raw( + gateway, + "/v1/responses", + { + "model": model, + "input": rv.agents_sdk_history(marker, {"type": "reasoning", "id": "rs_" + "f" * 32, "summary": []}), + }, + ) + assert failing.status_code == 404, failing.text + assert "rs_" + "f" * 32 in failing.text, failing.text + healthy: Final = _raw(gateway, "/v1/responses", {"model": model, "input": history}, key=other) + assert healthy.status_code == 200, healthy.text + assert [request.target for request in wire.drain()] == ["/responses", "/responses"] + + +def _chat_history(marker: str, reasoning_items: Sequence[Mapping[str, JsonValue]]) -> list[dict[str, JsonValue]]: + return [ + {"role": "user", "content": "Pick a city."}, + {"role": "assistant", "content": "Prague", "reasoning_items": [dict(item) for item in reasoning_items]}, + {"role": "user", "content": f"Name a landmark marker-{marker}"}, + ] + + +def _chat_create(client: openai.OpenAI, model: str, messages: Sequence[Mapping[str, JsonValue]], stream: bool) -> str: + if not stream: + completion: Final = client.chat.completions.create( + model=model, messages=list(messages), extra_body=dict(_CACHE_BUST) + ) + return str(completion.choices[0].message.content) + chunks: Final = list( + client.chat.completions.create(model=model, messages=list(messages), stream=True, extra_body=dict(_CACHE_BUST)) + ) + return "".join(str(chunk.choices[0].delta.content or "") for chunk in chunks if chunk.choices) + + +@pytest.mark.parametrize("stream", [False, True], ids=["sync", "stream"]) +def test_chat_bridge_replays_a_stored_reasoning_item_without_inventing_an_id(gateway: Gateway, stream: bool) -> None: + marker: Final = uuid.uuid4().hex + stored: Final[dict[str, JsonValue]] = { + "type": "reasoning", + "encrypted_content": f"gAAAAA-stored-{marker}", + "summary": [], + } + with wire_server(rv.ResponsesVendor().respond) as wire, gateway.scenario() as scenario: + model: Final = scenario.model(model=f"openai/{_CODEX}", api_base=wire.url, api_key=_OPENAI_KEY) + answer: Final = _chat_create(_sdk(gateway), model, _chat_history(marker, (stored,)), stream) + assert answer == f"answer marker-{marker}" + request, body = _only_request(wire) + assert request.target == "/responses" + assert body["model"] == _CODEX + assert rv.reasoning_items(body) == [stored], body["input"] + + +async def test_chat_bridge_async_client_replays_a_stored_reasoning_item_without_inventing_an_id( + gateway: Gateway, +) -> None: + marker: Final = uuid.uuid4().hex + stored: Final[dict[str, JsonValue]] = { + "type": "reasoning", + "encrypted_content": f"gAAAAA-stored-{marker}", + "summary": [], + } + with wire_server(rv.ResponsesVendor().respond) as wire, gateway.scenario() as scenario: + model: Final = scenario.model(model=f"openai/{_CODEX}", api_base=wire.url, api_key=_OPENAI_KEY) + completion: Final = await _async_sdk(gateway).chat.completions.create( + model=model, messages=_chat_history(marker, (stored,)), extra_body=dict(_CACHE_BUST) + ) + assert completion.choices[0].message.content == f"answer marker-{marker}" + _, body = _only_request(wire) + assert rv.reasoning_items(body) == [stored], body["input"] + + +def test_chat_bridge_keeps_a_vendor_minted_id_and_sends_an_empty_item_bare(gateway: Gateway) -> None: + marker: Final = uuid.uuid4().hex + with wire_server(rv.ResponsesVendor().respond) as wire, gateway.scenario() as scenario: + model: Final = scenario.model(model=f"openai/{_CODEX}", api_base=wire.url, api_key=_OPENAI_KEY) + produced: Final = _sdk(gateway).chat.completions.create( + model=model, + messages=[{"role": "user", "content": f"Pick a city marker-{marker}"}], + extra_body=dict(_CACHE_BUST), + ) + message: Final = produced.choices[0].message.model_dump() + (stored,) = _ITEMS.validate_python(message["reasoning_items"]) + assert str(stored["id"]).startswith("rs_") and str(stored["encrypted_content"]).startswith("gAAAAA-vendor-"), ( + stored + ) + wire.drain() + follow_up: Final = uuid.uuid4().hex + answer: Final = _chat_create(_sdk(gateway), model, _chat_history(follow_up, (stored,)), False) + assert answer == f"answer marker-{follow_up}" + _, body = _only_request(wire) + assert rv.reasoning_items(body) == [ + {"type": "reasoning", "id": stored["id"], "summary": [], "encrypted_content": stored["encrypted_content"]} + ], body["input"] + + bare: Final = uuid.uuid4().hex + assert ( + _chat_create(_sdk(gateway), model, _chat_history(bare, ({"type": "reasoning", "summary": []},)), False) + == f"answer marker-{bare}" + ) + _, bare_body = _only_request(wire) + assert rv.reasoning_items(bare_body) == [{"type": "reasoning", "summary": []}], bare_body["input"] + + +def test_chat_mode_model_takes_the_same_assistant_message_on_the_chat_wire(gateway: Gateway) -> None: + marker: Final = uuid.uuid4().hex + stored: Final[dict[str, JsonValue]] = { + "type": "reasoning", + "encrypted_content": f"gAAAAA-stored-{marker}", + "summary": [], + } + with wire_server(rv.ResponsesVendor().respond) as wire, gateway.scenario() as scenario: + model: Final = scenario.model(model=f"openai/{_GPT}", api_base=wire.url, api_key=_OPENAI_KEY) + assert _chat_create(_sdk(gateway), model, _chat_history(marker, (stored,)), False) == f"answer marker-{marker}" + request, body = _only_request(wire) + assert request.target == "/chat/completions" + messages: Final = _ITEMS.validate_python(body["messages"]) + assert [turn["role"] for turn in messages] == ["user", "assistant", "user"], messages + assert messages[1]["content"] == "Prague", messages[1] + + +def _thinking_turns(marker: str) -> list[dict[str, JsonValue]]: + return [ + {"role": "user", "content": "Pick a city."}, + { + "role": "assistant", + "content": [ + {"type": "thinking", "thinking": rv.THOUGHT, "signature": rv.signature(marker)}, + {"type": "text", "text": "Prague"}, + ], + }, + {"role": "user", "content": f"Name a landmark marker-{marker}"}, + ] + + +def _messages_create( + client: anthropic.Anthropic, model: str, messages: Sequence[Mapping[str, JsonValue]], stream: bool +) -> str: + if not stream: + reply: Final = client.messages.create( + model=model, max_tokens=64, messages=list(messages), extra_body=dict(_CACHE_BUST) + ) + return "".join(block.text for block in reply.content if block.type == "text") + with client.messages.stream( + model=model, max_tokens=64, messages=list(messages), extra_body=dict(_CACHE_BUST) + ) as stream_reply: + final: Final = stream_reply.get_final_message() + return "".join(block.text for block in final.content if block.type == "text") + + +@pytest.mark.parametrize("stream", [False, True], ids=["sync", "stream"]) +def test_messages_endpoint_replays_claude_thinking_to_claude_unchanged(gateway: Gateway, stream: bool) -> None: + marker: Final = uuid.uuid4().hex + turns: Final = _thinking_turns(marker) + with wire_server(rv.ResponsesVendor().respond) as wire, gateway.scenario() as scenario: + model: Final = scenario.model(model=f"anthropic/{_CLAUDE}", api_base=wire.url, api_key=cc.ANTHROPIC_API_KEY) + assert _messages_create(_claude_sdk(gateway), model, turns, stream) == f"answer marker-{marker}" + request, body = _only_request(wire) + assert request.target == "/v1/messages" + assert body["messages"] == turns, body["messages"] + assert body.get("stream", False) is stream, body + + +@pytest.mark.parametrize("backend", [_CODEX, _GPT]) +@pytest.mark.parametrize("stream", [False, True], ids=["sync", "stream"]) +def test_messages_endpoint_on_an_openai_model_sends_an_idless_reasoning_item( + gateway: Gateway, backend: str, stream: bool +) -> None: + marker: Final = uuid.uuid4().hex + with wire_server(rv.ResponsesVendor().respond) as wire, gateway.scenario() as scenario: + model: Final = scenario.model(model=f"openai/{backend}", api_base=wire.url, api_key=_OPENAI_KEY) + assert ( + _messages_create(_claude_sdk(gateway), model, _thinking_turns(marker), stream) == f"answer marker-{marker}" + ) + request, body = _only_request(wire) + assert request.target == "/responses" + assert body.get("stream", False) is stream, body + (item,) = rv.reasoning_items(body) + assert "id" not in item and "summary" in item, item diff --git a/tests/integration/sdk/conftest.py b/tests/integration/sdk/conftest.py new file mode 100644 index 00000000000..066cca6250a --- /dev/null +++ b/tests/integration/sdk/conftest.py @@ -0,0 +1,20 @@ +from typing import Final + +import litellm +import pytest + +CALLBACK_LISTS: Final = ( + "callbacks", + "success_callback", + "failure_callback", + "input_callback", + "_async_success_callback", + "_async_failure_callback", + "_async_input_callback", +) + + +@pytest.fixture(autouse=True) +def isolate_litellm_callback_lists(monkeypatch: pytest.MonkeyPatch) -> None: + for name in CALLBACK_LISTS: + monkeypatch.setattr(litellm, name, list(getattr(litellm, name))) diff --git a/tests/integration/sdk/test_bedrock_converse_stream_sync_decoder_wire.py b/tests/integration/sdk/test_bedrock_converse_stream_sync_decoder_wire.py new file mode 100644 index 00000000000..90acdd774d0 --- /dev/null +++ b/tests/integration/sdk/test_bedrock_converse_stream_sync_decoder_wire.py @@ -0,0 +1,100 @@ +import re +from collections.abc import Callable, Mapping +from dataclasses import dataclass +from typing import Final +from urllib.parse import unquote + +import pytest +from integration._support.upstream import _aws_event_frame +from integration._support.wire import Reply, Request, Wire, wire_server +from pydantic import JsonValue + +import litellm +from litellm.exceptions import BadGatewayError, BadRequestError, MidStreamFallbackError +from litellm.litellm_core_utils.streaming_handler import CustomStreamWrapper + +_MODEL_ID: Final = "global.moonshotai.kimi-k3" +_CONVERSE_MODEL: Final = f"bedrock/converse/{_MODEL_ID}" +_STREAM_TARGET: Final = f"/model/{_MODEL_ID}/converse-stream" +_EVENT_STREAM: Final = "application/vnd.amazon.eventstream" +_ANSWER: Final = "bedrock sync decoder control" +_REJECTION: Final = "structured output schema uses unsupported regex negative look-ahead" +_UNKNOWN_TYPE: Final = "somethingBedrockAddedLater" +_USAGE: Final[dict[str, JsonValue]] = {"usage": {"inputTokens": 11, "outputTokens": 4, "totalTokens": 15}} + + +def _frame(event_type: str, payload: Mapping[str, JsonValue]) -> bytes: + return _aws_event_frame(event_type, payload, "sc", "u") + + +_NORMAL: Final = b"".join( + ( + _frame("messageStart", {"role": "assistant"}), + _frame("contentBlockDelta", {"delta": {"text": _ANSWER}, "contentBlockIndex": 0}), + _frame("contentBlockStop", {"contentBlockIndex": 0}), + _frame("messageStop", {"stopReason": "end_turn"}), + _frame("metadata", _USAGE), + ) +) +_VALIDATION_FRAME: Final = _frame("validationException", {"message": _REJECTION}) +_UNKNOWN_FRAME: Final = _frame(_UNKNOWN_TYPE, {"future": True}) + + +@dataclass(frozen=True, slots=True) +class _Consumed: + text: str + finish_reasons: tuple[str | None, ...] + + +def _peer(frames: bytes) -> Callable[[Request], Reply]: + def respond(request: Request) -> Reply: + assert unquote(request.target) == _STREAM_TARGET, request.target + return Reply(body=frames, content_type=_EVENT_STREAM) + + return respond + + +def _consume_sync_stream(wire: Wire) -> _Consumed: + response: Final = litellm.completion( + model=_CONVERSE_MODEL, + messages=[{"role": "user", "content": "What does the sync decoder do with this stream?"}], + max_tokens=16, + stream=True, + api_base=wire.url, + aws_access_key_id="AKIASCRIPTEDPROVIDER", + aws_secret_access_key="scripted-secret", + aws_region_name="us-east-1", + num_retries=0, + ) + assert isinstance(response, CustomStreamWrapper), type(response) + chunks: Final = tuple(response) + return _Consumed( + text="".join(str(chunk.choices[0].delta.content or "") for chunk in chunks), + finish_reasons=tuple(chunk.choices[0].finish_reason for chunk in chunks), + ) + + +def test_k01_sync_stream_with_a_validation_exception_frame_first_raises_a_400() -> None: + with wire_server(_peer(_VALIDATION_FRAME)) as wire: + with pytest.raises(BadRequestError, match=re.escape(_REJECTION)) as raised: + _consume_sync_stream(wire) + assert raised.value.status_code == 400, raised.value + assert len(wire.drain()) == 1 + + +def test_k02_sync_stream_whose_only_frame_has_an_unknown_event_type_raises_a_502_naming_it() -> None: + with wire_server(_peer(_UNKNOWN_FRAME)) as wire: + with pytest.raises(MidStreamFallbackError, match=re.escape(_UNKNOWN_TYPE)) as raised: + _consume_sync_stream(wire) + assert raised.value.status_code == 502, raised.value + assert isinstance(raised.value.original_exception, BadGatewayError), raised.value.original_exception + assert "none of its 1 events carried a known event type" in str(raised.value), raised.value + assert len(wire.drain()) == 1 + + +def test_k03_sync_stream_with_normal_frames_delivers_the_text_and_a_stop() -> None: + with wire_server(_peer(_NORMAL)) as wire: + consumed: Final = _consume_sync_stream(wire) + assert consumed.text == _ANSWER, consumed + assert consumed.finish_reasons[-1] == "stop", consumed + assert len(wire.drain()) == 1 diff --git a/tests/integration/spend/test_daily_activity_key_owner_faults.py b/tests/integration/spend/test_daily_activity_key_owner_faults.py index 998cd2396ae..2ab56622ec4 100644 --- a/tests/integration/spend/test_daily_activity_key_owner_faults.py +++ b/tests/integration/spend/test_daily_activity_key_owner_faults.py @@ -161,7 +161,9 @@ def test_every_key_of_a_team_is_reported_with_its_own_user(gateway: Gateway) -> ) with daily_rows(rows): response: Final = gateway.request( - "GET", AGGREGATED_TEAM_ACTIVITY, params={"start_date": DAY, "end_date": DAY, "team_ids": team} + "GET", + AGGREGATED_TEAM_ACTIVITY, + params={"start_date": DAY, "end_date": DAY, "team_ids": team, "api_key_limit": KEYS_OF_ONE_TEAM}, ) assert response.status_code == 200, response.text body: Final = object_value(response.json()) diff --git a/tests/integration/spend/test_passthrough_request_tags.py b/tests/integration/spend/test_passthrough_request_tags.py new file mode 100644 index 00000000000..e6f5e15e161 --- /dev/null +++ b/tests/integration/spend/test_passthrough_request_tags.py @@ -0,0 +1,421 @@ +import json +import uuid +from collections.abc import Callable, Mapping +from hashlib import sha256 +from pathlib import Path +from typing import Final + +import pytest +from integration._support.client import Gateway, JsonValue, Scenario, eventually, object_value +from integration._support.database import read_rows +from integration._support.process import owned_proxy +from integration._support.wire import Reply, Request, wire_server +from pydantic import TypeAdapter + + +def _chat_reply(marker: str) -> dict[str, JsonValue]: + return { + "id": f"chatcmpl-{marker}", + "object": "chat.completion", + "created": 1, + "model": "gpt-4o-mini", + "choices": [{"index": 0, "message": {"role": "assistant", "content": marker}, "finish_reason": "stop"}], + "usage": {"prompt_tokens": 5, "completion_tokens": 3, "total_tokens": 8}, + } + + +def _anthropic_reply(marker: str) -> dict[str, JsonValue]: + return { + "id": f"msg_{marker}", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-5", + "content": [{"type": "text", "text": marker}], + "stop_reason": "end_turn", + "stop_sequence": None, + "usage": {"input_tokens": 5, "output_tokens": 3}, + } + + +def _chat_stream_frames(marker: str) -> tuple[bytes, ...]: + chunk: Final = {"id": f"chatcmpl-{marker}", "object": "chat.completion.chunk", "created": 1, "model": "gpt-4o-mini"} + return ( + f"data: {json.dumps({**chunk, 'choices': [{'index': 0, 'delta': {'role': 'assistant', 'content': marker}}]})}\n\n".encode(), + f"data: {json.dumps({**chunk, 'choices': [{'index': 0, 'delta': {}, 'finish_reason': 'stop'}], 'usage': {'prompt_tokens': 5, 'completion_tokens': 3, 'total_tokens': 8}})}\n\n".encode(), + b"data: [DONE]\n\n", + ) + + +def _spend_row(digest: str, call_type: str) -> dict[str, JsonValue]: + rows: Final = eventually( + lambda: read_rows( + 'SELECT request_tags, metadata, team_id FROM "LiteLLM_SpendLogs" WHERE api_key=%s AND call_type=%s', + (digest, call_type), + ), + lambda values: len(values) == 1, + seconds=70, + ) + return rows[0] + + +def _spend_row_tagged(tag: str) -> dict[str, JsonValue]: + rows: Final = eventually( + lambda: read_rows( + 'SELECT request_tags, metadata, team_id, api_key FROM "LiteLLM_SpendLogs" WHERE request_tags::text LIKE %s', + (f'%"{tag}"%',), + ), + lambda values: len(values) == 1, + seconds=70, + ) + return rows[0] + + +def _policy_tags(row: Mapping[str, JsonValue]) -> list[JsonValue]: + raw: Final = row["request_tags"] + tags: Final = json.loads(raw) if isinstance(raw, str) else raw + assert isinstance(tags, list), row + return [tag for tag in tags if not (isinstance(tag, str) and tag.startswith("User-Agent: "))] + + +def _spend_logs_metadata(row: Mapping[str, JsonValue]) -> JsonValue: + metadata: Final = row["metadata"] + return object_value(json.loads(metadata) if isinstance(metadata, str) else metadata).get("spend_logs_metadata") + + +def _tagged_key(scenario: Scenario, marker: str, **fields: JsonValue) -> tuple[str, str]: + team: Final = scenario.team(metadata={"tags": [f"team-{marker}"], "spend_logs_metadata": {"team_field": marker}}) + project: Final = scenario.project(team, metadata={"tags": [f"project-{marker}"]}) + key: Final = scenario.key( + team_id=team, + project_id=project, + metadata={"tags": [f"key-{marker}"], "spend_logs_metadata": {"cost_center": marker}}, + **fields, + ) + return key, sha256(key.encode()).hexdigest() + + +def _digest(key: str) -> str: + return sha256(key.encode()).hexdigest() + + +def _configured_passthrough(gateway: Gateway, scenario: Scenario, marker: str, target: str, *, auth: bool) -> str: + path: Final = f"/integration-passthrough-{marker}" + created: Final = gateway.post("/config/pass_through_endpoint", {"path": path, "target": target, "auth": auth}) + endpoints: Final = TypeAdapter(list[JsonValue]).validate_python(created["endpoints"]) + endpoint_id: Final = object_value(endpoints[0])["id"] + scenario.cleanups.callback( + lambda: gateway.request("DELETE", "/config/pass_through_endpoint", params={"endpoint_id": str(endpoint_id)}) + ) + return path + + +def _responses_reply(marker: str, stream: bool) -> Reply: + response: Final[dict[str, JsonValue]] = { + "id": f"resp_{marker}", + "object": "response", + "created_at": 1, + "status": "completed", + "model": "gpt-4o-mini", + "output": [ + { + "id": f"msg_{marker}", + "type": "message", + "role": "assistant", + "status": "completed", + "content": [{"type": "output_text", "text": marker, "annotations": []}], + } + ], + "usage": {"input_tokens": 5, "output_tokens": 3, "total_tokens": 8}, + } + if not stream: + return Reply(body=json.dumps(response).encode()) + events: Final[tuple[dict[str, JsonValue], ...]] = ( + { + "type": "response.created", + "sequence_number": 0, + "response": {**response, "status": "in_progress", "output": []}, + }, + { + "type": "response.output_text.delta", + "sequence_number": 1, + "item_id": f"msg_{marker}", + "output_index": 0, + "content_index": 0, + "delta": marker, + }, + {"type": "response.completed", "sequence_number": 2, "response": response}, + ) + return Reply( + content_type="text/event-stream", + chunks=tuple(f"event: {event['type']}\ndata: {json.dumps(event)}\n\n".encode() for event in events), + ) + + +def _echo_upstream(marker: str) -> Callable[[Request], Reply]: + def respond(request: Request) -> Reply: + if request.method == "GET" and request.target == "/v1/models": + return Reply(body=json.dumps({"object": "list", "data": []}).encode()) + assert request.method == "POST", request + body: Final = object_value(json.loads(request.body)) + assert marker in json.dumps(body), request + if request.target == "/v1/responses": + return _responses_reply(marker, body.get("stream") is True) + assert body["messages"] == [{"role": "user", "content": marker}], request + if body.get("stream") is True: + return Reply(chunks=_chat_stream_frames(marker), content_type="text/event-stream") + return Reply(body=json.dumps(_chat_reply(marker)).encode()) + + return respond + + +def test_configured_passthrough_spend_row_matches_native_route_tags_and_spend_logs_metadata(gateway: Gateway) -> None: + marker: Final = uuid.uuid4().hex + with wire_server(_echo_upstream(marker)) as wire, gateway.scenario() as scenario: + model: Final = scenario.model(api_base=wire.url + "/v1") + path: Final = _configured_passthrough(gateway, scenario, marker, wire.url + "/echo", auth=True) + key, digest = _tagged_key(scenario, marker, models=[model], allowed_passthrough_routes=[path]) + headers: Final = {"x-litellm-tags": f"caller-{marker},key-{marker}", "User-Agent": "integration-tags/1"} + body: Final[dict[str, JsonValue]] = {"model": model, "messages": [{"role": "user", "content": marker}]} + + native: Final = gateway.request("POST", "/v1/chat/completions", body, key=key, headers=headers) + assert native.status_code == 200, native.text + passthrough: Final = gateway.request("POST", path, body, key=key, headers=headers) + assert passthrough.status_code == 200, passthrough.text + assert json.loads(passthrough.content) == _chat_reply(marker) + + native_row: Final = _spend_row(digest, "acompletion") + passthrough_row: Final = _spend_row(digest, "pass_through_endpoint") + expected: Final = [f"key-{marker}", f"team-{marker}", f"project-{marker}", f"caller-{marker}"] + assert _policy_tags(native_row) == expected, native_row + assert _policy_tags(passthrough_row) == expected, passthrough_row + assert _spend_logs_metadata(native_row) == {"cost_center": marker, "team_field": marker}, native_row + assert _spend_logs_metadata(passthrough_row) == {"cost_center": marker, "team_field": marker}, passthrough_row + + +@pytest.mark.parametrize("bucket", ["metadata", "litellm_metadata"]) +def test_configured_passthrough_body_tags_lead_and_body_spend_logs_metadata_wins_over_key_and_team( + gateway: Gateway, bucket: str +) -> None: + marker: Final = uuid.uuid4().hex + with wire_server(_echo_upstream(marker)) as wire, gateway.scenario() as scenario: + path: Final = _configured_passthrough(gateway, scenario, marker, wire.url + "/echo", auth=True) + key, digest = _tagged_key(scenario, marker, allowed_passthrough_routes=[path]) + body: Final[dict[str, JsonValue]] = { + "messages": [{"role": "user", "content": marker}], + bucket: { + "tags": [f"body-{marker}", f"team-{marker}"], + "spend_logs_metadata": {"cost_center": f"body-{marker}"}, + }, + } + response: Final = gateway.request("POST", path, body, key=key) + assert response.status_code == 200, response.text + row: Final = _spend_row(digest, "pass_through_endpoint") + assert _policy_tags(row) == [f"body-{marker}", f"team-{marker}", f"key-{marker}", f"project-{marker}"], row + assert _spend_logs_metadata(row) == {"cost_center": f"body-{marker}", "team_field": marker}, row + + +def test_configured_passthrough_streaming_upstream_row_carries_key_team_project_and_caller_tags( + gateway: Gateway, +) -> None: + marker: Final = uuid.uuid4().hex + with wire_server(_echo_upstream(marker)) as wire, gateway.scenario() as scenario: + path: Final = _configured_passthrough(gateway, scenario, marker, wire.url + "/echo", auth=True) + key, digest = _tagged_key(scenario, marker, allowed_passthrough_routes=[path]) + response: Final = gateway.request( + "POST", + path, + {"stream": True, "messages": [{"role": "user", "content": marker}]}, + key=key, + headers={"x-litellm-tags": f"caller-{marker}"}, + ) + assert response.status_code == 200, response.text + assert response.content == b"".join(_chat_stream_frames(marker)), response.text + row: Final = _spend_row(digest, "pass_through_endpoint") + assert _policy_tags(row) == [f"key-{marker}", f"team-{marker}", f"project-{marker}", f"caller-{marker}"], row + assert _spend_logs_metadata(row) == {"cost_center": marker, "team_field": marker}, row + + +def test_configured_passthrough_key_outside_any_team_carries_its_own_tags_and_spend_logs_metadata( + gateway: Gateway, +) -> None: + marker: Final = uuid.uuid4().hex + with wire_server(_echo_upstream(marker)) as wire, gateway.scenario() as scenario: + path: Final = _configured_passthrough(gateway, scenario, marker, wire.url + "/echo", auth=True) + key: Final = scenario.key( + allowed_passthrough_routes=[path], + metadata={"tags": [f"key-{marker}"], "spend_logs_metadata": {"cost_center": marker}}, + ) + response: Final = gateway.request( + "POST", + path, + {"messages": [{"role": "user", "content": marker}]}, + key=key, + headers={"x-litellm-tags": f"caller-{marker}"}, + ) + assert response.status_code == 200, response.text + row: Final = _spend_row(_digest(key), "pass_through_endpoint") + assert _policy_tags(row) == [f"key-{marker}", f"caller-{marker}"], row + assert _spend_logs_metadata(row) == {"cost_center": marker}, row + + +def test_configured_passthrough_untagged_key_row_keeps_only_caller_tag_and_no_spend_logs_metadata( + gateway: Gateway, +) -> None: + marker: Final = uuid.uuid4().hex + with wire_server(_echo_upstream(marker)) as wire, gateway.scenario() as scenario: + path: Final = _configured_passthrough(gateway, scenario, marker, wire.url + "/echo", auth=True) + team: Final = scenario.team() + key: Final = scenario.key(team_id=team, allowed_passthrough_routes=[path]) + response: Final = gateway.request( + "POST", + path, + {"messages": [{"role": "user", "content": marker}]}, + key=key, + headers={"x-litellm-tags": f"caller-{marker}"}, + ) + assert response.status_code == 200, response.text + row: Final = _spend_row(_digest(key), "pass_through_endpoint") + assert _policy_tags(row) == [f"caller-{marker}"], row + assert _spend_logs_metadata(row) is None, row + assert row["team_id"] == team, row + + +def test_open_passthrough_without_auth_row_carries_only_caller_tag(gateway: Gateway) -> None: + marker: Final = uuid.uuid4().hex + with wire_server(_echo_upstream(marker)) as wire, gateway.scenario() as scenario: + path: Final = _configured_passthrough(gateway, scenario, marker, wire.url + "/echo", auth=False) + response: Final = gateway.client.post( + path, + json={"messages": [{"role": "user", "content": marker}]}, + headers={"x-litellm-tags": f"caller-{marker}"}, + ) + assert response.status_code == 200, response.text + row: Final = _spend_row_tagged(f"caller-{marker}") + assert _policy_tags(row) == [f"caller-{marker}"], row + assert _spend_logs_metadata(row) is None, row + assert row["api_key"] == "", row + + +@pytest.mark.parametrize( + ("metadata", "leading_tags"), + [ + ({"tags": "string-not-list"}, []), + ({"tags": [1, None, "z"]}, [1, None, "z"]), + ({"spend_logs_metadata": "string-not-object"}, []), + ], +) +def test_configured_passthrough_hostile_body_metadata_shapes_still_carry_key_team_project_tags( + gateway: Gateway, metadata: JsonValue, leading_tags: list[JsonValue] +) -> None: + marker: Final = uuid.uuid4().hex + with wire_server(_echo_upstream(marker)) as wire, gateway.scenario() as scenario: + path: Final = _configured_passthrough(gateway, scenario, marker, wire.url + "/echo", auth=True) + key, digest = _tagged_key(scenario, marker, allowed_passthrough_routes=[path]) + response: Final = gateway.request( + "POST", path, {"messages": [{"role": "user", "content": marker}], "metadata": metadata}, key=key + ) + assert response.status_code == 200, response.text + row: Final = _spend_row(digest, "pass_through_endpoint") + assert _policy_tags(row) == [*leading_tags, f"key-{marker}", f"team-{marker}", f"project-{marker}"], row + assert _spend_logs_metadata(row) == {"cost_center": marker, "team_field": marker}, row + + +def test_configured_passthrough_body_cannot_forge_user_api_key_attribution_fields(gateway: Gateway) -> None: + marker: Final = uuid.uuid4().hex + with wire_server(_echo_upstream(marker)) as wire, gateway.scenario() as scenario: + path: Final = _configured_passthrough(gateway, scenario, marker, wire.url + "/echo", auth=True) + forged_team: Final = scenario.team() + key, digest = _tagged_key(scenario, marker, allowed_passthrough_routes=[path]) + forged: Final[dict[str, JsonValue]] = { + "user_api_key": "forged-" + marker, + "user_api_key_team_id": forged_team, + "user_api_key_user_id": "forged-" + marker, + "user_api_key_alias": "forged-" + marker, + } + body: Final[dict[str, JsonValue]] = {"messages": [{"role": "user", "content": marker}], "metadata": forged} + response: Final = gateway.request("POST", path, body, key=key) + assert response.status_code == 200, response.text + row: Final = _spend_row(digest, "pass_through_endpoint") + assert row["team_id"] != forged_team, row + assert _policy_tags(row) == [f"key-{marker}", f"team-{marker}", f"project-{marker}"], row + assert read_rows('SELECT api_key FROM "LiteLLM_SpendLogs" WHERE team_id=%s', (forged_team,)) == [], forged_team + + +@pytest.mark.parametrize("stream", [False, True]) +@pytest.mark.parametrize("route", ["/v1/chat/completions", "/v1/messages"]) +def test_native_routes_carry_key_team_project_and_caller_tags_and_key_over_team_spend_logs_metadata( + gateway: Gateway, route: str, stream: bool +) -> None: + marker: Final = uuid.uuid4().hex + with wire_server(_echo_upstream(marker)) as wire, gateway.scenario() as scenario: + model: Final = scenario.model(api_base=wire.url + "/v1") + key, digest = _tagged_key(scenario, marker, models=[model]) + body: Final[dict[str, JsonValue]] = { + "model": model, + "max_tokens": 16, + "stream": stream, + "messages": [{"role": "user", "content": marker}], + } + response: Final = gateway.request( + "POST", + route, + body, + key=key, + headers={"x-litellm-tags": f"caller-{marker}", "User-Agent": "integration-tags/1"}, + ) + assert response.status_code == 200, response.text + rows: Final = eventually( + lambda: read_rows('SELECT request_tags, metadata FROM "LiteLLM_SpendLogs" WHERE api_key=%s', (digest,)), + lambda values: len(values) == 1, + seconds=70, + ) + assert _policy_tags(rows[0]) == [f"key-{marker}", f"team-{marker}", f"project-{marker}", f"caller-{marker}"], ( + rows + ) + assert _spend_logs_metadata(rows[0]) == {"cost_center": marker, "team_field": marker}, rows + + +def test_anthropic_passthrough_spend_row_carries_key_team_project_tags_and_spend_logs_metadata( + gateway: Gateway, tmp_path: Path +) -> None: + marker: Final = uuid.uuid4().hex + + def respond(request: Request) -> Reply: + assert request.method == "POST" and request.target == "/v1/messages", request + assert request.headers["x-api-key"] == "synthetic-anthropic-key" + return Reply(body=json.dumps(_anthropic_reply(marker)).encode()) + + config: Final = tmp_path / "proxy_config.yaml" + config.write_text( + "model_list: []\n" + "general_settings:\n" + " master_key: os.environ/LITELLM_MASTER_KEY\n" + " database_url: os.environ/DATABASE_URL\n" + " store_model_in_db: true\n" + " disable_spend_logs: false\n" + " proxy_batch_write_at: 1\n" + "router_settings:\n" + " disable_cooldowns: true\n" + ) + with wire_server(respond) as wire: + overrides: Final = {"ANTHROPIC_API_BASE": wire.url, "ANTHROPIC_API_KEY": "synthetic-anthropic-key"} + with owned_proxy(gateway, tmp_path, overrides, config=config) as candidate, candidate.scenario() as scenario: + key, digest = _tagged_key(scenario, marker) + response: Final = candidate.request( + "POST", + "/anthropic/v1/messages", + { + "model": "claude-sonnet-4-5", + "max_tokens": 16, + "messages": [{"role": "user", "content": marker}], + }, + key=key, + headers={"x-litellm-tags": f"caller-{marker}", "User-Agent": "integration-tags/1"}, + ) + assert response.status_code == 200, response.text + assert json.loads(response.content) == _anthropic_reply(marker) + row: Final = _spend_row(digest, "pass_through_endpoint") + assert _policy_tags(row) == [f"key-{marker}", f"team-{marker}", f"project-{marker}", f"caller-{marker}"], ( + row + ) + assert _spend_logs_metadata(row) == {"cost_center": marker, "team_field": marker}, row diff --git a/tests/integration/spend/test_spend_log_read_scope.py b/tests/integration/spend/test_spend_log_read_scope.py new file mode 100644 index 00000000000..f9034371e35 --- /dev/null +++ b/tests/integration/spend/test_spend_log_read_scope.py @@ -0,0 +1,226 @@ +import os +import uuid +from collections.abc import AsyncIterator +from dataclasses import dataclass +from datetime import datetime, timedelta, timezone +from typing import Final +from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit + +import psycopg +import pytest +import pytest_asyncio +from integration._support.client import Gateway +from prisma import Prisma +from psycopg import sql +from psycopg.types.json import Jsonb +from pydantic import TypeAdapter + +from litellm.proxy.auth.authorization import AllRows, OwnedRows, ReadScope +from litellm.proxy.spend_tracking.spend_management_endpoints import _spend_log_payload_query, read_scope_sql + + +@dataclass(frozen=True, slots=True) +class SpendRow: + request_id: str + user: str | None + team_id: str | None + call_id: str | None = None + + +@dataclass(frozen=True, slots=True) +class RequestId: + request_id: str + + +REQUEST_IDS: Final = TypeAdapter(tuple[RequestId, ...]) +ROWS: Final = ( + SpendRow("own", "caller", None, "foreign"), + SpendRow("team-1", "other", "first"), + SpendRow("team-2", "third", "second"), + SpendRow("foreign", "other", "outside"), + SpendRow("ownerless", None, None), + SpendRow("team-ownerless", None, "first"), +) + + +def _seed_rows( + connection: psycopg.Connection, + schema: str, + rows: tuple[SpendRow, ...], + session_id: str, + started: datetime, +) -> None: + utc_timestamp: Final = started.astimezone(timezone.utc).replace(tzinfo=None) + with connection.cursor() as cursor: + cursor.executemany( + sql.SQL( + 'INSERT INTO {} (request_id, "user", team_id, litellm_call_id, session_id, ' + '"startTime", "endTime", messages, response, call_type) ' + "VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, 'acompletion')" + ).format(sql.Identifier(schema, "LiteLLM_SpendLogs")), + tuple( + ( + row.request_id, + row.user, + row.team_id, + row.call_id, + session_id, + utc_timestamp, + utc_timestamp, + Jsonb([{"role": "user", "content": row.request_id + " payload"}]), + Jsonb({"id": row.request_id}), + ) + for row in rows + ), + ) + + +@pytest_asyncio.fixture(loop_scope="function") +async def spend_database() -> AsyncIterator[Prisma]: + schema: Final = f"integration_spend_scope_{uuid.uuid4().hex}" + url: Final = os.environ["DATABASE_URL"] + parsed: Final = urlsplit(url) + scoped_url: Final = urlunsplit( + parsed._replace(query=urlencode({**dict(parse_qsl(parsed.query)), "schema": schema})) + ) + with psycopg.connect(url, autocommit=True) as setup: + setup.execute(sql.SQL("CREATE SCHEMA {}").format(sql.Identifier(schema))) + try: + setup.execute( + sql.SQL('CREATE TABLE {} (LIKE public."LiteLLM_SpendLogs" INCLUDING ALL)').format( + sql.Identifier(schema, "LiteLLM_SpendLogs") + ) + ) + _seed_rows(setup, schema, ROWS, "scope-session", datetime(2026, 1, 1, tzinfo=timezone.utc)) + database: Final = Prisma(datasource={"url": scoped_url}) + await database.connect() + try: + yield database + finally: + await database.disconnect() + finally: + setup.execute(sql.SQL("DROP SCHEMA {} CASCADE").format(sql.Identifier(schema))) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("preceding_filters", [False, True]) +@pytest.mark.parametrize( + ("scope", "user_filter", "expected"), + [ + (AllRows(), None, ("foreign", "own", "ownerless", "team-1", "team-2", "team-ownerless")), + (OwnedRows("caller"), None, ("own",)), + (OwnedRows(None), None, ()), + (OwnedRows(None, ("first", "second")), None, ("team-1", "team-2", "team-ownerless")), + (OwnedRows(None, ("first", "second")), "other", ("team-1",)), + (OwnedRows("caller", ("first", "second")), None, ("own", "team-1", "team-2", "team-ownerless")), + (OwnedRows("caller", ("first", "second")), "other", ("team-1",)), + (OwnedRows("caller", ("first' OR TRUE --",)), None, ("own",)), + (OwnedRows("caller' OR TRUE --", ("first",)), None, ("team-1", "team-ownerless")), + ], +) +async def test_ownership_sql_selects_allowed_rows_and_intersects_filters( + spend_database: Prisma, + scope: ReadScope, + user_filter: str | None, + expected: tuple[str, ...], + preceding_filters: bool, +) -> None: + window_params: Final = ("scope-session", "2026-01-01", "2026-01-02") if preceding_filters else () + window_sql: Final = ( + 'session_id = $1 AND "startTime" >= $2::timestamp AND "startTime" < $3::timestamp AND ' + if preceding_filters + else "" + ) + clause, scope_params = read_scope_sql(scope, len(window_params) + 1) + filter_sql: Final = f' AND "user" = ${len(window_params) + len(scope_params) + 1}' if user_filter else "" + params: Final = window_params + scope_params + ((user_filter,) if user_filter else ()) + result: Final = await spend_database.query_raw( + f'SELECT request_id FROM "LiteLLM_SpendLogs" WHERE {window_sql}{clause or "TRUE"}{filter_sql} ' + "ORDER BY request_id", + *params, + ) + assert tuple(row.request_id for row in REQUEST_IDS.validate_python(result)) == expected + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("scope", "expected"), + [(AllRows(), ("foreign",)), (OwnedRows("caller"), ("own",)), (OwnedRows(None), ())], +) +async def test_payload_sql_filters_foreign_collisions_and_prefers_exact_ids_for_admins( + spend_database: Prisma, scope: ReadScope, expected: tuple[str, ...] +) -> None: + query, params = _spend_log_payload_query("foreign", scope) + result: Final = await spend_database.query_raw(query, *params) + assert tuple(row.request_id for row in REQUEST_IDS.validate_python(result)) == expected + + +def _delete_session(session_id: str) -> None: + with psycopg.connect(os.environ["DATABASE_URL"]) as connection: + connection.execute('DELETE FROM "LiteLLM_SpendLogs" WHERE session_id = %s', (session_id,)) + + +@pytest.mark.parametrize( + ("member_role", "permissions", "team_access"), + [ + ("admin", [], True), + ("user", ["/spend/logs"], True), + ("user", ["/key/info"], False), + ("user", [], False), + ], +) +def test_spend_log_routes_preserve_user_and_permitted_team_access( + gateway: Gateway, member_role: str, permissions: list[str], team_access: bool +) -> None: + session_id: Final = f"scope-{uuid.uuid4().hex}" + started: Final = datetime.now(timezone.utc) - timedelta(hours=1) + with gateway.scenario() as scenario: + caller: Final = scenario.user(user_role="internal_user") + other: Final = scenario.user(user_role="internal_user") + team: Final = scenario.team( + members_with_roles=[{"user_id": caller, "role": member_role}], + team_member_permissions=list(permissions), + ) + outside_team: Final = scenario.team( + members_with_roles=[{"user_id": other, "role": "admin"}], + team_member_permissions=["/spend/logs"], + ) + key: Final = scenario.key(user_id=caller) + other_key: Final = scenario.key(user_id=other) + rows: Final = ( + SpendRow(session_id + "-own", caller, None, session_id + "-foreign"), + SpendRow(session_id + "-team", other, team), + SpendRow(session_id + "-foreign", other, outside_team), + SpendRow(session_id + "-ownerless", None, None), + SpendRow(session_id + "-outside", other, outside_team), + ) + scenario.cleanups.callback(_delete_session, session_id) + with psycopg.connect(os.environ["DATABASE_URL"]) as connection: + _seed_rows(connection, "public", rows, session_id, started) + expected: Final = (rows[0].request_id, rows[1].request_id) if team_access else (rows[0].request_id,) + session: Final = gateway.request("GET", "/spend/logs/session/ui", key=key, params={"session_id": session_id}) + assert session.status_code == 200, session.text + assert session.json()["total"] == len(expected), session.text + assert sorted(row["request_id"] for row in session.json()["data"]) == list(expected), session.text + filters: Final = { + "session_id": session_id, + "start_date": (started - timedelta(hours=1)).strftime("%Y-%m-%d %H:%M:%S"), + "end_date": (started + timedelta(hours=1)).strftime("%Y-%m-%d %H:%M:%S"), + } + listed: Final = gateway.request("GET", "/spend/logs/ui", key=key, params=filters) + assert listed.status_code == 200, listed.text + assert sorted(row["request_id"] for row in listed.json()["data"]) == list(expected), listed.text + narrowed: Final = gateway.request("GET", "/spend/logs/ui", key=key, params={**filters, "user_id": other}) + assert narrowed.status_code == 200, narrowed.text + assert [row["request_id"] for row in narrowed.json()["data"]] == ( + [rows[1].request_id] if team_access else [] + ), narrowed.text + refused: Final = gateway.request("GET", f"/spend/logs/ui/{rows[4].request_id}", key=key) + assert refused.status_code == 403, refused.text + for caller_key, expected_id in ((key, rows[0].request_id), (other_key, rows[2].request_id)): + payload: Final = gateway.request("GET", f"/spend/logs/ui/{rows[2].request_id}", key=caller_key) + assert payload.status_code == 200, payload.text + assert payload.json()["messages"] == [{"role": "user", "content": expected_id + " payload"}], payload.text + admin: Final = gateway.request("GET", f"/spend/logs/ui/{rows[2].request_id}") + assert admin.status_code == 200, admin.text + assert admin.json()["messages"] == [{"role": "user", "content": rows[2].request_id + " payload"}], admin.text diff --git a/tests/local_testing/test_secret_detect_hook.py b/tests/local_testing/test_secret_detect_hook.py index 0ee0f596177..c560637b785 100644 --- a/tests/local_testing/test_secret_detect_hook.py +++ b/tests/local_testing/test_secret_detect_hook.py @@ -272,6 +272,7 @@ async def test_chat_completion_request_with_redaction(): scope={ "type": "http", "method": "POST", + "path": "/chat/completions", "headers": [(b"content-type", b"application/json")], "query_string": query_params.encode(), } diff --git a/tests/mcp_tests/test_proxy_mcp_e2e.py b/tests/mcp_tests/test_proxy_mcp_e2e.py index a730f6c10ee..92f26e4ab7e 100644 --- a/tests/mcp_tests/test_proxy_mcp_e2e.py +++ b/tests/mcp_tests/test_proxy_mcp_e2e.py @@ -55,6 +55,7 @@ def _clear_proxy_database_env() -> typing.Iterator[None]: # the config file. We must set it here so the lifespan doesn't reset it to None. mp.setenv("LITELLM_MASTER_KEY", "sk-1234") mp.setenv("LITELLM_DANGEROUSLY_PERMIT_WEAK_OR_UNSET_MASTER_KEY", "true") + mp.setenv("LITELLM_ENABLE_MCP_STDIO", "true") try: yield finally: diff --git a/tests/pass_through_unit_tests/test_pass_through_unit_tests.py b/tests/pass_through_unit_tests/test_pass_through_unit_tests.py index 6c57e59f7e3..82cb652950c 100644 --- a/tests/pass_through_unit_tests/test_pass_through_unit_tests.py +++ b/tests/pass_through_unit_tests/test_pass_through_unit_tests.py @@ -63,7 +63,8 @@ def mock_request(): self.method = method self.request_body = request_body or {} # Add url attribute that the actual code expects - self.url = "http://localhost:8000/test" + self.url = httpx.URL("http://localhost:8000/test") + self.scope = {"type": "http", "method": method, "path": "/test"} # Add state attribute that FastAPI requests have self.state = type("State", (), {})() @@ -414,6 +415,8 @@ PROTOCOL_CONSTRAINED_PASS_THROUGH_ROUTES = { "/transcribe": {"POST"}, "/transcribe/{operation}": {"POST"}, "/tinyfish/{endpoint:path}": {"GET", "POST"}, + "/laya/v1/systemone": {"POST"}, + "/bespoke/v1/systemone": {"POST"}, } diff --git a/tests/proxy_behavior/lens/test_lifecycle.py b/tests/proxy_behavior/lens/test_lifecycle.py index 849b2186a62..77e1421675a 100644 --- a/tests/proxy_behavior/lens/test_lifecycle.py +++ b/tests/proxy_behavior/lens/test_lifecycle.py @@ -10,13 +10,27 @@ import pytest import pytest_asyncio from fastapi import HTTPException, Request from fastapi.security import HTTPAuthorizationCredentials +from pydantic import TypeAdapter from litellm import Router from litellm.proxy import proxy_server from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth from litellm.proxy.common_utils.user_api_key_cache import UserApiKeyCache from litellm.proxy.lens import endpoints -from litellm.proxy.lens.models import Check, Coverage, LensSettings, ModelRequest, Progress, Result, RunRequest +from litellm.proxy.lens.models import ( + Check, + Coverage, + Lens, + LensSettings, + ModelRequest, + Progress, + Result, + RunRequest, + Scope, + Worker, +) +from litellm.proxy.lens.repository import Database, LensRepository, Row +from litellm.proxy.lens.state import can_access from litellm.proxy.utils import PrismaClient, ProxyLogging @@ -46,7 +60,18 @@ async def lens_database() -> AsyncIterator[PrismaClient]: "input_cost_per_token": 0.000001, "output_cost_per_token": 0.000002, }, - } + }, + { + "model_name": "lens-team-route", + "model_info": {"team_id": "lens-test-team-a", "team_public_model_name": "private/*"}, + "litellm_params": { + "model": "openai/*", + "api_key": "test-only", + "input_cost_per_token": 0.000001, + "output_cost_per_token": 0.000002, + }, + }, + {"model_name": "unpriced/*", "litellm_params": {"model": "openai/*", "api_key": "test-only"}}, ] ) try: @@ -58,6 +83,138 @@ async def lens_database() -> AsyncIterator[PrismaClient]: await client.disconnect() +class _ObservedDatabase: + def __init__(self, db: Database) -> None: + self.db: Final = db + self.page_sizes: tuple[int, ...] = () + + async def query_raw(self, query: str, *args: object) -> object: + rows: Final = TypeAdapter(tuple[Row, ...]).validate_python(await self.db.query_raw(query, *args)) + self.page_sizes = (*self.page_sizes, len(rows)) + return rows + + async def execute_raw(self, query: str, *args: object) -> int: + return await self.db.execute_raw(query, *args) + + +@pytest.mark.parametrize("kind", ("all", "team", "key")) +@pytest.mark.asyncio +async def test_eligible_workers_filter_before_bounded_pages(lens_database: PrismaClient, kind: str) -> None: + prefix: Final = str(uuid4()) + now: Final = datetime.now(timezone.utc) + scopes: Final = { + "all": Scope(all_teams=True), + "team": Scope(team_id=prefix), + "key": Scope(api_key_hash=prefix), + } + workers: Final = ( + *(Worker(id=f"{prefix}-{i:03}", name=prefix, scope=scopes["all"], last_seen=now) for i in range(65)), + Worker(id=f"{prefix}-team", name=prefix, scope=scopes["team"], last_seen=now), + Worker(id=f"{prefix}-key", name=prefix, scope=scopes["key"], last_seen=now), + Worker(id=f"{prefix}-foreign", name=prefix, scope=Scope(team_id="other"), last_seen=now), + Worker(id=f"{prefix}-other-key", name=prefix, scope=Scope(api_key_hash="other"), last_seen=now), + Worker(id=f"{prefix}-revoked", name=prefix, scope=scopes["all"], last_seen=now, revoked=True), + ) + repo: Final = endpoints.repository() + try: + for worker in workers: + await repo.save_worker(worker, hashlib.sha256(worker.id.encode()).hexdigest()) + observed: Final = _ObservedDatabase(repo.db) + eligible: Final = [worker async for worker in LensRepository(observed).eligible_workers(scopes[kind])] + expected: Final = tuple(w for w in workers if not w.revoked and can_access(w.scope, scopes[kind])) + assert tuple(w.id for w in eligible) == tuple(sorted(w.id for w in expected)) + assert observed.page_sizes == (50, len(expected) - 50) + finally: + await lens_database.db.execute_raw("DELETE FROM \"LiteLLM_LensWorker\" WHERE data->>'name'=$1", prefix) + + +@pytest.mark.parametrize("enabled", (True, False)) +@pytest.mark.asyncio +async def test_unpriced_saved_model_allows_edits_but_not_new_runs(lens_database: PrismaClient, enabled: bool) -> None: + admin: Final = UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN) + now: Final = datetime.now(timezone.utc) + original: Final = Lens( + id=str(uuid4()), + scope=Scope(all_teams=True), + created_at=now, + next_run_at=now, + budget_month=now.strftime("%Y-%m"), + settings=LensSettings( + name="Saved investigation", model="unpriced/lens-saved-model", context="Answer questions", enabled=enabled + ), + ) + await endpoints.repository().create(original) + try: + settings: Final = original.settings.model_copy(update={"context": "Use cited sources", "enabled": False}) + edited: Final = await endpoints.update_lens(original.id, settings, admin) + assert edited.settings == settings + assert edited.revision == original.revision + 1 + assert (await endpoints.read_lens(original.id, admin)).settings == settings + for operation in ( + endpoints.run_lens(original.id, RunRequest(), admin), + endpoints.update_lens(original.id, settings.model_copy(update={"enabled": True}), admin), + endpoints.update_lens(original.id, settings.model_copy(update={"model": "unpriced/other-model"}), admin), + ): + with pytest.raises(HTTPException) as error: + await operation + assert error.value.status_code == 400 + assert "Pricing is not configured" in error.value.detail + with pytest.raises(HTTPException) as invalid_selection: + await endpoints.update_lens(original.id, settings.model_copy(update={"execution_ids": ("invalid",)}), admin) + assert invalid_selection.value.status_code == 422 + assert (await endpoints.read_lens(original.id, admin)).settings == settings + finally: + await lens_database.db.execute_raw('DELETE FROM "LiteLLM_Lens" WHERE id=$1', original.id) + + +@pytest.mark.asyncio +async def test_team_route_requires_a_worker_with_matching_model_access(lens_database: PrismaClient) -> None: + admin: Final = UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN, team_id="lens-test-team-a") + name: Final = f"Team route regression {uuid4()}" + settings: Final = LensSettings(name=name, model="private/analysis", context="Answer questions", enabled=False) + lens: Final = await endpoints.create_lens(settings, admin) + key_a: Final = hashlib.sha256(uuid4().bytes).hexdigest() + key_b: Final = hashlib.sha256(uuid4().bytes).hexdigest() + await lens_database.db.litellm_verificationtoken.create( + data={"token": key_a, "team_id": "lens-test-team-a", "models": ["private/*"]} + ) + await lens_database.db.litellm_verificationtoken.create( + data={"token": key_b, "team_id": "lens-test-team-b", "models": ["private/*"]} + ) + try: + wrong_team: Final = await endpoints.register_worker(endpoints.WorkerName(analysis_key_id=key_b), admin) + assert await endpoints.claim_candidate(lens, wrong_team.worker, datetime.now(timezone.utc)) is None + for operation in ( + endpoints.create_lens(settings, admin), + endpoints.run_lens(lens.id, RunRequest(), admin), + ): + with pytest.raises(HTTPException) as error: + await operation + assert error.value.status_code == 400 + assert "worker" in error.value.detail + edited: Final = await endpoints.update_lens( + lens.id, settings.model_copy(update={"context": "Use sources"}), admin + ) + assert edited.settings.context == "Use sources" + right_team: Final = await endpoints.register_worker(endpoints.WorkerName(analysis_key_id=key_a), admin) + await endpoints.validate_workers(settings, lens.scope) + claim: Final = await endpoints.claim_candidate(lens, right_team.worker, datetime.now(timezone.utc)) + assert claim is not None and claim.job.worker_id == right_team.worker.id + finally: + await lens_database.db.execute_raw( + """DELETE FROM "LiteLLM_LensRun" WHERE lens_id IN + (SELECT id FROM "LiteLLM_Lens" WHERE data->'settings'->>'name'=$1)""", + name, + ) + await lens_database.db.execute_raw("DELETE FROM \"LiteLLM_Lens\" WHERE data->'settings'->>'name'=$1", name) + await lens_database.db.execute_raw( + "DELETE FROM \"LiteLLM_LensWorker\" WHERE data->>'analysis_key_id' IN ($1, $2)", key_a, key_b + ) + await lens_database.db.execute_raw( + 'DELETE FROM "LiteLLM_VerificationToken" WHERE token IN ($1, $2)', key_a, key_b + ) + + @pytest.mark.asyncio async def test_scan_lifecycle_persists_results_and_revokes_worker(lens_database: PrismaClient) -> None: admin: Final = UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN) @@ -186,14 +343,19 @@ async def test_scan_lifecycle_persists_results_and_revokes_worker(lens_database: assert finished.jobs[0].coverage.screened == 2 assert finished.last_scan_at == claimed.job.end assert finished.next_run_at > finished.jobs[0].finished_at - assert await endpoints.result(lens.id, claimed.job.id, Result(coverage=Coverage()), worker, storage=None) == finished + assert ( + await endpoints.result(lens.id, claimed.job.id, Result(coverage=Coverage()), worker, storage=None) + == finished + ) with pytest.raises(HTTPException) as stale: await endpoints.heartbeat(lens.id, claimed.job.id, worker) assert stale.value.status_code == 409 - edited: Final = await endpoints.update_lens( - lens.id, settings.model_copy(update={"interval_minutes": 7}), admin - ) + edited: Final = await endpoints.update_lens(lens.id, settings.model_copy(update={"interval_minutes": 7}), admin) assert edited.revision == lens.revision + 1 + with pytest.raises(HTTPException) as unavailable_worker: + await endpoints.run_lens(lens.id, RunRequest(lookback_hours=3), admin) + assert unavailable_worker.value.status_code == 400 + await endpoints.set_worker_billing(worker.id, endpoints.WorkerBilling(analysis_key_id=key_id), admin) rerun: Final = await endpoints.run_lens(lens.id, RunRequest(lookback_hours=3), admin) assert rerun.jobs[0].settings.interval_minutes == 7 assert rerun.jobs[0].created_at - rerun.jobs[0].start == timedelta(hours=3) diff --git a/tests/proxy_behavior/spend/test_autorouter_session_rollup.py b/tests/proxy_behavior/spend/test_autorouter_session_rollup.py index 2c648f309f6..a5c6f5962a2 100644 --- a/tests/proxy_behavior/spend/test_autorouter_session_rollup.py +++ b/tests/proxy_behavior/spend/test_autorouter_session_rollup.py @@ -80,6 +80,25 @@ async def _turn( ) +async def _benchmark_rows( + db, start: datetime, end: datetime, key: str | None = None, user_id: str | None = None +) -> list[dict]: + return await db.query_raw( + AUTOROUTER_BENCHMARKS_SQL, + start.isoformat(), + end.isoformat(), + key, + user_id, + start.date().isoformat(), + (end - timedelta(days=1)).date().isoformat(), + ) + + +async def _days(db, key: str | None = None, user_id: str | None = None, router: str | None = None) -> list[dict]: + rows = await _benchmark_rows(db, T0 - timedelta(days=1), T0 + timedelta(days=2), key, user_id) + return [row for row in rows if row["turns"] and (router is None or row["router_name"] == router)] + + async def _row(db, key: str, session_id: str = "s1", router: str = "auto-1") -> dict: rows = await db.query_raw( 'SELECT * FROM "LiteLLM_AutoRouterSession" WHERE api_key = $1 AND session_id = $2 AND router_name = $3', @@ -225,18 +244,15 @@ async def test_subtotal_coverage_survives_legacy_and_rolling_writers(db, writers assert row["savings_estimated_turns"] == sum(writers) assert row["savings_estimated_actual_spend"] == pytest.approx(0.01 * sum(writers)) assert row["savings_estimated_saved_spend"] == pytest.approx(0.02 * sum(writers)) - groups: Final = await db.query_raw( - AUTOROUTER_BENCHMARKS_SQL, T0.isoformat(), (T0 + timedelta(days=1)).isoformat(), key, None - ) - assert len(groups) == 1 - assert groups[0]["classifier_cost"] == row["classifier_cost"] - assert groups[0]["classifier_cost_recorded_turns"] == sum(writers) - assert groups[0]["turns"] == len(writers) - assert groups[0]["spend"] == row["spend"] - assert groups[0]["saved_spend"] == row["saved_spend"] - assert groups[0]["savings_estimated_turns"] == sum(writers) - assert groups[0]["savings_estimated_actual_spend"] == row["savings_estimated_actual_spend"] - assert groups[0]["savings_estimated_saved_spend"] == row["savings_estimated_saved_spend"] + days: Final = await _days(db, key) + assert len(days) == int(any(writers)) + for day in days: + assert day["classifier_cost"] == row["classifier_cost"] + assert day["classifier_cost_recorded_turns"] == day["turns"] == sum(writers) + assert day["spend"] == pytest.approx(0.01 * sum(writers)) + assert day["saved_spend"] == pytest.approx(0.02 * sum(writers)) + assert day["savings_estimated_actual_spend"] == row["savings_estimated_actual_spend"] + assert day["savings_estimated_saved_spend"] == row["savings_estimated_saved_spend"] async def test_unknown_and_legacy_turns_preserve_actual_spend_without_entering_the_estimated_cohort(db: Prisma) -> None: @@ -250,13 +266,10 @@ async def test_unknown_and_legacy_turns_preserve_actual_spend_without_entering_t row: Final = await _row(db, key) assert row["saved_spend"] == pytest.approx(-0.03) assert row["savings_estimated_baseline_models"] == {"opus": 1} - groups: Final = await db.query_raw( - AUTOROUTER_BENCHMARKS_SQL, T0.isoformat(), (T0 + timedelta(days=1)).isoformat(), key, None - ) - assert len(groups) == 1 - for actual in (row, groups[0]): - assert actual["turns"] == 3 - assert actual["spend"] == pytest.approx(0.96) + (day,) = await _days(db, key) + assert (row["turns"], day["turns"]) == (3, 2) + assert (row["spend"], day["spend"]) == (pytest.approx(0.96), pytest.approx(0.95)) + for actual in (row, day): assert actual["savings_estimated_turns"] == 1 assert actual["savings_estimated_actual_spend"] == pytest.approx(0.25) assert actual["savings_estimated_saved_spend"] == pytest.approx(-0.05) @@ -281,25 +294,20 @@ async def test_the_benchmarks_aggregate_reads_only_overlapping_sessions(db): await _turn(db, key, "A", T0, session_id=in_window, router=router, saved=0.5, spend=0.25, classifier_cost=0.02) await _turn(db, key, "A", T0 - timedelta(days=40), session_id=out_of_window, router=router, classifier_cost=9.0) - rows = await db.query_raw( - AUTOROUTER_BENCHMARKS_SQL, - (T0 - timedelta(days=1)).isoformat(), - (T0 + timedelta(days=1)).isoformat(), - None, - None, - ) + rows = await _benchmark_rows(db, (T0 - timedelta(days=1)), (T0 + timedelta(days=1)), None, None) matching = [row for row in rows if row["router_name"] == router] assert len(matching) == 1 grouped = matching[0] assert grouped["router_type"] == "complexity" assert grouped["sessions"] == 1 - assert grouped["turns"] == 2 - assert grouped["spend"] == pytest.approx(0.5) - assert grouped["saved_spend"] == pytest.approx(1.0) - assert grouped["classifier_cost"] == pytest.approx(0.03) - assert grouped["classifier_cost_recorded_turns"] == 2 + assert grouped["session_turns"] == 2 assert grouped["unordered_turns"] == 1 assert grouped["session_seconds"] == pytest.approx(60.0) + (day,) = await _days(db, router=router) + assert (day["turns"], day["classifier_cost_recorded_turns"]) == (2, 2) + assert day["spend"] == pytest.approx(0.5) + assert day["saved_spend"] == pytest.approx(1.0) + assert day["classifier_cost"] == pytest.approx(0.03) async def test_the_benchmarks_aggregate_can_filter_to_one_key(db): @@ -309,32 +317,22 @@ async def test_the_benchmarks_aggregate_can_filter_to_one_key(db): await _turn(db, first_key, "A", T0, router=router, saved=0.5, classifier_cost=0.01) await _turn(db, second_key, "A", T0, router=router, saved=9.0, classifier_cost=0.09) - rows = await db.query_raw( - AUTOROUTER_BENCHMARKS_SQL, - (T0 - timedelta(days=1)).isoformat(), - (T0 + timedelta(days=1)).isoformat(), - first_key, - None, - ) + rows = await _benchmark_rows(db, (T0 - timedelta(days=1)), (T0 + timedelta(days=1)), first_key, None) matching = [row for row in rows if row["router_name"] == router] assert len(matching) == 1 assert matching[0]["sessions"] == 1 - assert matching[0]["saved_spend"] == pytest.approx(0.5) - assert matching[0]["classifier_cost"] == pytest.approx(0.01) - assert matching[0]["classifier_cost_recorded_turns"] == 1 + (day,) = await _days(db, first_key, router=router) + assert day["saved_spend"] == pytest.approx(0.5) + assert day["classifier_cost"] == pytest.approx(0.01) + assert day["classifier_cost_recorded_turns"] == 1 - unknown_key_rows = await db.query_raw( - AUTOROUTER_BENCHMARKS_SQL, - (T0 - timedelta(days=1)).isoformat(), - (T0 + timedelta(days=1)).isoformat(), - f"k-{uuid.uuid4()}", - None, - ) + unknown_key_rows = await _benchmark_rows(db, (T0 - timedelta(days=1)), (T0 + timedelta(days=1)), f"k-{uuid.uuid4()}", None) assert [row for row in unknown_key_rows if row["router_name"] == router] == [] class _BenchmarkRow(TypedDict): sessions: ReadOnly[int] + session_turns: ReadOnly[int] turns: ReadOnly[int] same_model_turns: ReadOnly[int] first_visit_turns: ReadOnly[int] @@ -350,14 +348,11 @@ class _BenchmarkRow(TypedDict): async def _scoped_benchmarks( db: Prisma, router: str, user_id: str | None = None, key: str | None = None ) -> tuple[_BenchmarkRow, ...]: - rows: Final = await db.query_raw( - AUTOROUTER_BENCHMARKS_SQL, - (T0 - timedelta(days=1)).isoformat(), - (T0 + timedelta(days=1)).isoformat(), - key, - user_id, + rows: Final = await _benchmark_rows(db, (T0 - timedelta(days=1)), (T0 + timedelta(days=1)), key, user_id) + days: Final = await _days(db, key, user_id, router) + return tuple( + cast(_BenchmarkRow, {**row, **next(iter(days), {})}) for row in rows if row["router_name"] == router ) - return tuple(cast(_BenchmarkRow, row) for row in rows if row["router_name"] == router) async def test_users_keep_written_identity_across_shared_keys_and_keyless_sessions(db: Prisma) -> None: @@ -384,28 +379,33 @@ async def test_users_keep_written_identity_across_shared_keys_and_keyless_sessio intersection: Final = await _scoped_benchmarks(db, router, user_id=alice, key=first_key) assert len(alice_rows) == len(bob_rows) == len(global_rows) == len(key_rows) == len(intersection) == 1 assert (alice_rows[0]["sessions"], alice_rows[0]["turns"], alice_rows[0]["same_model_turns"]) == (3, 4, 1) + assert (alice_rows[0]["session_turns"], bob_rows[0]["session_turns"]) == (4, 2) assert (bob_rows[0]["sessions"], bob_rows[0]["turns"], bob_rows[0]["first_visit_turns"]) == (2, 2, 2) assert alice_rows[0]["spend"] == pytest.approx(0.05) assert bob_rows[0]["spend"] == pytest.approx(0.07) assert alice_rows[0]["tier_turns"] == {"simple": 1} assert bob_rows[0]["tier_turns"] == {"complex": 1} assert (alice_rows[0]["cache_hits"], bob_rows[0]["cache_hits"]) == (1, 0) - assert (global_rows[0]["sessions"], global_rows[0]["turns"]) == (4, 7) + assert (global_rows[0]["sessions"], global_rows[0]["session_turns"], global_rows[0]["turns"]) == (4, 7, 6) assert (alice_rows[0]["savings_estimated_turns"], bob_rows[0]["savings_estimated_turns"]) == (4, 2) assert global_rows[0]["savings_estimated_turns"] == 6 for scoped in (alice_rows[0], bob_rows[0]): assert scoped["savings_estimated_actual_spend"] == pytest.approx(scoped["spend"]) assert scoped["savings_estimated_saved_spend"] == pytest.approx(scoped["saved_spend"]) - assert global_rows[0]["spend"] == pytest.approx(alice_rows[0]["spend"] + bob_rows[0]["spend"] + 0.01) - assert global_rows[0]["saved_spend"] == pytest.approx(alice_rows[0]["saved_spend"] + bob_rows[0]["saved_spend"] + 0.02) + assert global_rows[0]["spend"] == pytest.approx(alice_rows[0]["spend"] + bob_rows[0]["spend"]) + assert global_rows[0]["saved_spend"] == pytest.approx(alice_rows[0]["saved_spend"] + bob_rows[0]["saved_spend"]) assert global_rows[0]["tier_turns"] == {"simple": 1, "complex": 1} - assert (key_rows[0]["sessions"], key_rows[0]["turns"]) == (1, 3) - assert key_rows[0]["spend"] == pytest.approx(0.05) + assert (key_rows[0]["sessions"], key_rows[0]["session_turns"], key_rows[0]["turns"]) == (1, 3, 2) + assert key_rows[0]["spend"] == pytest.approx(0.04) assert (intersection[0]["sessions"], intersection[0]["turns"]) == (1, 1) assert intersection[0]["spend"] == pytest.approx(0.01) assert await _scoped_benchmarks(db, router, user_id=bob, key=second_key) == () assert await _scoped_benchmarks(db, router, user_id=f"u-{uuid.uuid4()}") == () - assert await _scoped_benchmarks(db, router, user_id="") == () + assert [ + row + for row in await _benchmark_rows(db, (T0 - timedelta(days=1)), (T0 + timedelta(days=1)), None, "") + if row["router_name"] == router + ] == [] async def test_a_failed_user_projection_rolls_back_the_keys_increment(db: Prisma) -> None: @@ -419,6 +419,7 @@ async def test_a_failed_user_projection_rolls_back_the_keys_increment(db: Prisma assert await _row(db, key) == before assert await db.query_raw('SELECT user_id FROM "LiteLLM_AutoRouterUserSession" WHERE user_id = $1', user_id) == [] + assert [day["turns"] for day in await _days(db, key)] == [1] first_user: Final = f"u-{uuid.uuid4()}" second_user: Final = f"u-{uuid.uuid4()}" @@ -463,6 +464,14 @@ async def test_a_failed_user_projection_rolls_back_the_keys_increment(db: Prisma assert (row["turns"], row["same_model_turns"], row["unordered_turns"], row["last_model"]) == (count, 1, 0, model) assert row["spend"] == pytest.approx(count * 0.01) assert row["saved_spend"] == pytest.approx(count * 0.02) + days: Final = await db.query_raw( + 'SELECT user_id, turns, saved_spend FROM "LiteLLM_AutoRouterDailySpend" WHERE api_key = $1', key + ) + assert {day["user_id"]: (day["turns"], day["saved_spend"]) for day in days} == { + "": (1, pytest.approx(0.02)), + first_user: (3, pytest.approx(0.06)), + second_user: (2, pytest.approx(0.04)), + } async def test_user_session_cleanup_keeps_another_users_recent_keyless_session(db: Prisma) -> None: @@ -490,13 +499,7 @@ async def test_a_reconfigured_alias_reports_each_router_type_as_its_own_group(db db, key, "A", T0 + timedelta(seconds=10), session_id=f"s-{uuid.uuid4()}", router=router, router_type="quality" ) - rows = await db.query_raw( - AUTOROUTER_BENCHMARKS_SQL, - (T0 - timedelta(days=1)).isoformat(), - (T0 + timedelta(days=1)).isoformat(), - None, - None, - ) + rows = await _benchmark_rows(db, (T0 - timedelta(days=1)), (T0 + timedelta(days=1)), None, None) matching = sorted( (row for row in rows if row["router_name"] == router), key=lambda row: row["router_type"], @@ -575,16 +578,10 @@ async def test_the_benchmarks_aggregate_sums_tier_turns_across_sessions(db): await _turn(db, key, "B", T0 + timedelta(seconds=20), session_id=f"s-{uuid.uuid4()}", router=router, tier="complex") await _turn(db, key, "C", T0 + timedelta(seconds=30), session_id=f"s-{uuid.uuid4()}", router=router, tier=None) - rows = await db.query_raw( - AUTOROUTER_BENCHMARKS_SQL, - (T0 - timedelta(days=1)).isoformat(), - (T0 + timedelta(days=1)).isoformat(), - None, - None, - ) + rows = await _benchmark_rows(db, (T0 - timedelta(days=1)), (T0 + timedelta(days=1)), None, None) grouped = next(row for row in rows if row["router_name"] == router) assert grouped["tier_turns"] == {"simple": 2, "complex": 1} - assert grouped["turns"] == 4 + assert grouped["session_turns"] == 4 async def test_tier_maps_stay_separate_per_router_type_on_a_reconfigured_alias(db): @@ -604,13 +601,7 @@ async def test_tier_maps_stay_separate_per_router_type_on_a_reconfigured_alias(d tier="2", ) - rows = await db.query_raw( - AUTOROUTER_BENCHMARKS_SQL, - (T0 - timedelta(days=1)).isoformat(), - (T0 + timedelta(days=1)).isoformat(), - None, - None, - ) + rows = await _benchmark_rows(db, (T0 - timedelta(days=1)), (T0 + timedelta(days=1)), None, None) by_type = {row["router_type"]: row["tier_turns"] for row in rows if row["router_name"] == router} assert by_type == {"complexity": {"medium": 1}, "quality": {"2": 1}} @@ -620,13 +611,7 @@ async def test_a_window_with_no_tiered_turns_aggregates_to_an_empty_map(db): router = f"r-{uuid.uuid4()}" await _turn(db, key, "A", T0, session_id=f"s-{uuid.uuid4()}", router=router, tier=None) - rows = await db.query_raw( - AUTOROUTER_BENCHMARKS_SQL, - (T0 - timedelta(days=1)).isoformat(), - (T0 + timedelta(days=1)).isoformat(), - None, - None, - ) + rows = await _benchmark_rows(db, (T0 - timedelta(days=1)), (T0 + timedelta(days=1)), None, None) grouped = next(row for row in rows if row["router_name"] == router) assert grouped["tier_turns"] == {} @@ -653,3 +638,49 @@ async def test_an_out_of_order_hit_still_counts_toward_the_overall_hit_rate(db): assert row["unordered_turns"] == 1 assert row["cache_hits"] == 1 assert row["same_model_hits"] + row["first_visit_hits"] + row["return_hits"] == 0 + + +async def test_a_cross_midnight_session_splits_its_money_by_request_day(db): + key = f"k-{uuid.uuid4()}" + router = f"auto-{uuid.uuid4()}" + midnight = datetime(2026, 9, 2) + await _turn(db, key, "A", midnight - timedelta(minutes=10), router=router, spend=1.0, saved=7.0, user_id="u1") + await _turn(db, key, "A", midnight + timedelta(minutes=10), router=router, spend=1.0, saved=3.0, user_id="u1") + await _turn(db, key, "B", midnight + timedelta(days=1), router=router, spend=1.0, saved=11.0, user_id="u1") + + assert (await _row(db, key, router=router))["saved_spend"] == 21.0 + days = await db.query_raw( + 'SELECT date, turns, saved_spend FROM "LiteLLM_AutoRouterDailySpend" WHERE api_key = $1 ORDER BY date', key + ) + assert [(d["date"], d["turns"], d["saved_spend"]) for d in days] == [ + ("2026-09-01", 1, 7.0), + ("2026-09-02", 1, 3.0), + ("2026-09-03", 1, 11.0), + ] + for user_id in (None, "u1"): + (selected,) = await _benchmark_rows(db, midnight, midnight + timedelta(days=1), key, user_id) + assert (selected["sessions"], selected["session_turns"]) == (1, 3) + assert (selected["turns"], selected["spend"], selected["saved_spend"]) == (1, 1.0, 3.0) + + +async def test_a_router_type_change_within_a_day_keeps_each_types_money_apart(db): + key = f"k-{uuid.uuid4()}" + router = f"auto-{uuid.uuid4()}" + await _turn(db, key, "A", T0, router=router, router_type="complexity", spend=1.0, saved=4.0) + await _turn(db, key, "A", T0 + timedelta(hours=1), router=router, router_type="quality", spend=2.0, saved=0.0) + + days = {day["router_type"]: (day["turns"], day["spend"], day["saved_spend"]) for day in await _days(db, key)} + assert days == {"complexity": (1, 1.0, 4.0), "quality": (1, 2.0, 0.0)} + + +async def test_a_router_type_change_mid_session_keeps_session_shape_with_the_sessions_type(db): + key = f"k-{uuid.uuid4()}" + router = f"auto-{uuid.uuid4()}" + await _turn(db, key, "A", T0, router=router, router_type="complexity", spend=1.0, saved=4.0) + await _turn(db, key, "B", T0 + timedelta(hours=1), router=router, router_type="quality", spend=2.0, saved=0.0) + + rows = {row["router_type"]: row for row in await _benchmark_rows(db, T0, T0 + timedelta(days=1), key)} + assert set(rows) == {"complexity", "quality"} + assert (rows["complexity"]["sessions"], rows["complexity"]["session_turns"], rows["complexity"]["turns"]) == (1, 2, 1) + assert (rows["quality"]["sessions"], rows["quality"]["session_turns"], rows["quality"]["turns"]) == (0, 0, 1) + assert rows["quality"]["spend"] == 2.0 diff --git a/tests/proxy_behavior/spend/test_baseline_accounting.py b/tests/proxy_behavior/spend/test_baseline_accounting.py index 3504751d132..8fb82d0c80e 100644 --- a/tests/proxy_behavior/spend/test_baseline_accounting.py +++ b/tests/proxy_behavior/spend/test_baseline_accounting.py @@ -151,6 +151,13 @@ async def test_late_replay_updates_all_projections_without_rebilling(db: Prisma, ): assert after_users["late-user"][field] == after[field] assert after_users["late-user"]["turns"] == 1 and after_users["late-user"]["spend"] == 0.17 + days: Final = await db.query_raw( + 'SELECT * FROM "LiteLLM_AutoRouterDailySpend" WHERE api_key=$1 ORDER BY user_id', late.api_key + ) + assert [(day["date"], day["user_id"]) for day in days] == [("1970-01-01", "early-user"), ("1970-01-01", "late-user")] + assert days[0]["saved_spend"] == days[0]["savings_estimated_turns"] == 0 + for field in ("saved_spend", "savings_estimated_turns", "savings_estimated_actual_spend", "savings_estimated_saved_spend"): + assert days[1][field] == after[field] for table in ("DailyUserSpend", "DailyTeamSpend", "DailyOrganizationSpend", "DailyEndUserSpend", "DailyAgentSpend", "DailyTagSpend"): rows: Final = await db.query_raw(f'SELECT spend,api_requests,autorouter_savings_spend FROM "LiteLLM_{table}" WHERE api_key=$1', late.api_key) assert rows[0]["spend"] == rows[0]["api_requests"] == 0 diff --git a/tests/test_litellm/tracing/fixtures/claude_agent_sdk_detailed_export.json b/tests/test_litellm/tracing/fixtures/claude_agent_sdk_detailed_export.json new file mode 100644 index 00000000000..982f30868b1 --- /dev/null +++ b/tests/test_litellm/tracing/fixtures/claude_agent_sdk_detailed_export.json @@ -0,0 +1,1819 @@ +{ + "resourceSpans": [ + { + "resource": { + "attributes": [ + { + "key": "service.name", + "value": { + "stringValue": "claude-agent-sdk-demo" + } + }, + { + "key": "os.type", + "value": { + "stringValue": "linux" + } + }, + { + "key": "os.version", + "value": { + "stringValue": "0.0.0" + } + }, + { + "key": "service.version", + "value": { + "stringValue": "2.1.286" + } + } + ], + "droppedAttributesCount": 0 + }, + "scopeSpans": [ + { + "scope": { + "name": "com.anthropic.claude_code.tracing", + "version": "1.0.0" + }, + "spans": [ + { + "traceId": "6444c31c3ebc86434c869bcb2c98327a", + "spanId": "76ec1951742116e7", + "name": "claude_code.llm_request", + "kind": 1, + "startTimeUnixNano": "1790903552975000000", + "endTimeUnixNano": "1790903554399789458", + "attributes": [ + { + "key": "user.id", + "value": { + "stringValue": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "key": "session.id", + "value": { + "stringValue": "efddb30e-4074-43fe-97d6-d10785429cd5" + } + }, + { + "key": "span.type", + "value": { + "stringValue": "llm_request" + } + }, + { + "key": "model", + "value": { + "stringValue": "anthropic/claude-sonnet-5" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "anthropic" + } + }, + { + "key": "gen_ai.request.model", + "value": { + "stringValue": "anthropic/claude-sonnet-5" + } + }, + { + "key": "llm_request.context", + "value": { + "stringValue": "standalone" + } + }, + { + "key": "speed", + "value": { + "stringValue": "normal" + } + }, + { + "key": "query_source", + "value": { + "stringValue": "generate_session_title" + } + }, + { + "key": "query_source_safe", + "value": { + "stringValue": "generate_session_title" + } + }, + { + "key": "system_prompt_hash", + "value": { + "stringValue": "sp_53788704fc52" + } + }, + { + "key": "system_prompt_preview", + "value": { + "stringValue": "x-anthropic-billing-header: cc_version=2.1.286.e44; cc_entrypoint=sdk-py;\n\nYou are a Claude agent, built on Anthropic's Claude Agent SDK.\n\nYou are naming a coding session so the user can pick it out of a long list of sessions. The title is a name for what the session is about, not a sentence describing the task: a short noun phrase of two to five words, in sentence case (capitalize only the first word, plus proper nouns, acronyms, and code identifiers exactly as written). When a draft runs past " + } + }, + { + "key": "system_prompt_length", + "value": { + "intValue": 3198 + } + }, + { + "key": "tools", + "value": { + "stringValue": "[]" + } + }, + { + "key": "tools_count", + "value": { + "intValue": 0 + } + }, + { + "key": "new_context_message_count", + "value": { + "intValue": 1 + } + }, + { + "key": "new_context", + "value": { + "stringValue": "[USER]\n\nUse Bash to run 'ls' in this directory, then use Read to read agent.py, and summarize in 2 sentences what it does.\n\n\nWrite the title in the predominant language of the session — a stray word or code token in another language doesn't change it, and neither does the English of these instructions." + } + }, + { + "key": "duration_ms", + "value": { + "intValue": 1425 + } + }, + { + "key": "input_tokens", + "value": { + "intValue": 1205 + } + }, + { + "key": "output_tokens", + "value": { + "intValue": 14 + } + }, + { + "key": "cache_read_tokens", + "value": { + "intValue": 0 + } + }, + { + "key": "cache_creation_tokens", + "value": { + "intValue": 0 + } + }, + { + "key": "success", + "value": { + "boolValue": true + } + }, + { + "key": "attempt", + "value": { + "intValue": 1 + } + }, + { + "key": "response.has_tool_call", + "value": { + "boolValue": false + } + }, + { + "key": "ttft_ms", + "value": { + "intValue": 978 + } + }, + { + "key": "first_content_ms", + "value": { + "intValue": 978 + } + }, + { + "key": "effort", + "value": { + "stringValue": "high" + } + }, + { + "key": "response.model_output", + "value": { + "stringValue": "{\"title\": \"agent.py summary\"}" + } + }, + { + "key": "stop_reason", + "value": { + "stringValue": "end_turn" + } + }, + { + "key": "gen_ai.response.finish_reasons", + "value": { + "arrayValue": { + "values": [ + { + "stringValue": "end_turn" + } + ] + } + } + } + ], + "droppedAttributesCount": 0, + "events": [ + { + "attributes": [ + { + "key": "attempt", + "value": { + "intValue": 1 + } + } + ], + "name": "gen_ai.request.attempt", + "timeUnixNano": "1790903552983035250", + "droppedAttributesCount": 0 + } + ], + "droppedEventsCount": 0, + "status": { + "code": 0 + }, + "links": [], + "droppedLinksCount": 0, + "flags": 257 + }, + { + "traceId": "eab5340b3073943f63df1ff8d5b42db3", + "spanId": "f2c724a68fdf61b0", + "name": "claude_code.interaction", + "kind": 1, + "startTimeUnixNano": "1790903554106000000", + "endTimeUnixNano": "1790903559582058834", + "attributes": [ + { + "key": "user.id", + "value": { + "stringValue": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "key": "session.id", + "value": { + "stringValue": "efddb30e-4074-43fe-97d6-d10785429cd5" + } + }, + { + "key": "span.type", + "value": { + "stringValue": "interaction" + } + }, + { + "key": "user_prompt", + "value": { + "stringValue": "Use Bash to run 'ls' in this directory, then use Read to read agent.py, and summarize in 2 sentences what it does." + } + }, + { + "key": "user_prompt_length", + "value": { + "intValue": 114 + } + }, + { + "key": "interaction.sequence", + "value": { + "intValue": 1 + } + }, + { + "key": "parent.source", + "value": { + "stringValue": "none" + } + }, + { + "key": "queued_sends", + "value": { + "intValue": 0 + } + }, + { + "key": "new_context", + "value": { + "stringValue": "[USER PROMPT]\nUse Bash to run 'ls' in this directory, then use Read to read agent.py, and summarize in 2 sentences what it does." + } + }, + { + "key": "interaction.duration_ms", + "value": { + "intValue": 5476 + } + } + ], + "droppedAttributesCount": 0, + "events": [], + "droppedEventsCount": 0, + "status": { + "code": 0 + }, + "links": [], + "droppedLinksCount": 0, + "flags": 257 + }, + { + "traceId": "eab5340b3073943f63df1ff8d5b42db3", + "spanId": "bb43215ef8fa36cd", + "parentSpanId": "f2c724a68fdf61b0", + "name": "claude_code.hook", + "kind": 1, + "startTimeUnixNano": "1790903554113000000", + "endTimeUnixNano": "1790903554125993917", + "attributes": [ + { + "key": "user.id", + "value": { + "stringValue": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "key": "session.id", + "value": { + "stringValue": "efddb30e-4074-43fe-97d6-d10785429cd5" + } + }, + { + "key": "span.type", + "value": { + "stringValue": "hook" + } + }, + { + "key": "hook_event", + "value": { + "stringValue": "UserPromptSubmit" + } + }, + { + "key": "hook_name", + "value": { + "stringValue": "UserPromptSubmit" + } + }, + { + "key": "num_hooks", + "value": { + "intValue": 3 + } + }, + { + "key": "hook_definitions", + "value": { + "stringValue": "[{\"type\":\"command\",\"command\":\"/workspace/.claude/hooks/notify.sh\"}]" + } + }, + { + "key": "duration_ms", + "value": { + "intValue": 13 + } + }, + { + "key": "num_success", + "value": { + "intValue": 3 + } + }, + { + "key": "num_blocking", + "value": { + "intValue": 0 + } + }, + { + "key": "num_non_blocking_error", + "value": { + "intValue": 0 + } + }, + { + "key": "num_cancelled", + "value": { + "intValue": 0 + } + } + ], + "droppedAttributesCount": 0, + "events": [], + "droppedEventsCount": 0, + "status": { + "code": 0 + }, + "links": [], + "droppedLinksCount": 0, + "flags": 257 + }, + { + "traceId": "eab5340b3073943f63df1ff8d5b42db3", + "spanId": "2deef999c66d8cbd", + "parentSpanId": "f2c724a68fdf61b0", + "name": "claude_code.llm_request", + "kind": 1, + "startTimeUnixNano": "1790903554137000000", + "endTimeUnixNano": "1790903556349230708", + "attributes": [ + { + "key": "user.id", + "value": { + "stringValue": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "key": "session.id", + "value": { + "stringValue": "efddb30e-4074-43fe-97d6-d10785429cd5" + } + }, + { + "key": "span.type", + "value": { + "stringValue": "llm_request" + } + }, + { + "key": "model", + "value": { + "stringValue": "claude-sonnet-5-5" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "anthropic" + } + }, + { + "key": "gen_ai.request.model", + "value": { + "stringValue": "claude-sonnet-5-5" + } + }, + { + "key": "llm_request.context", + "value": { + "stringValue": "interaction" + } + }, + { + "key": "speed", + "value": { + "stringValue": "normal" + } + }, + { + "key": "query_source", + "value": { + "stringValue": "sdk" + } + }, + { + "key": "query_source_safe", + "value": { + "stringValue": "sdk" + } + }, + { + "key": "system_prompt_hash", + "value": { + "stringValue": "sp_754c39bc2203" + } + }, + { + "key": "system_prompt_preview", + "value": { + "stringValue": "x-anthropic-billing-header: cc_version=2.1.286.d3f; cc_entrypoint=sdk-py;\n\nYou are a Claude agent, built on Anthropic's Claude Agent SDK." + } + }, + { + "key": "system_prompt_length", + "value": { + "intValue": 137 + } + }, + { + "key": "tools", + "value": { + "stringValue": "[{\"name\":\"Agent\",\"hash\":\"164e46314bb0\"},{\"name\":\"Bash\",\"hash\":\"81dc4be713e1\"},{\"name\":\"CronCreate\",\"hash\":\"e4c660878de6\"},{\"name\":\"CronDelete\",\"hash\":\"4e244a652bf3\"},{\"name\":\"CronList\",\"hash\":\"6154cd8fa452\"},{\"name\":\"DesignSync\",\"hash\":\"390c2da6fbb7\"},{\"name\":\"Edit\",\"hash\":\"6430d0c60f48\"},{\"name\":\"EnterWorktree\",\"hash\":\"3f353219c93a\"},{\"name\":\"ExitWorktree\",\"hash\":\"79e242d1cef5\"},{\"name\":\"Glob\",\"hash\":\"341f5d0a2e2f\"},{\"name\":\"Grep\",\"hash\":\"1d3c47f9148f\"},{\"name\":\"ListAgents\",\"hash\":\"0a3591a577c6\"},{\"name\":\"ListMcpResourcesTool\",\"hash\":\"80428e7012e5\"},{\"name\":\"LSP\",\"hash\":\"b7be7911ea66\"},{\"name\":\"Monitor\",\"hash\":\"53eb832de993\"},{\"name\":\"NotebookEdit\",\"hash\":\"d88b4bf2ec93\"},{\"name\":\"PushNotification\",\"hash\":\"74f8dcf21b80\"},{\"name\":\"Read\",\"hash\":\"680529a1e735\"},{\"name\":\"ReadMcpResourceDirTool\",\"hash\":\"f87a091f6f1e\"},{\"name\":\"ReadMcpResourceTool\",\"hash\":\"9f256f5afee4\"},{\"name\":\"ReportFindings\",\"hash\":\"d742f97bb17e\"},{\"name\":\"ScheduleWakeup\",\"hash\":\"24fdfa8e91c8\"},{\"name\":\"SendMessage\",\"hash\":\"eee44afb16ba\"},{\"name\":\"Skill\",\"hash\":\"c3282cbcede5\"},{\"name\":\"TaskStop\",\"hash\":\"b145464cdabc\"},{\"name\":\"WebFetch\",\"hash\":\"e1fbaacd430d\"},{\"name\":\"WebSearch\",\"hash\":\"79a806bff741\"},{\"name\":\"Workflow\",\"hash\":\"b09d3792832d\"},{\"name\":\"Write\",\"hash\":\"416c9b17ff1f\"},{\"name\":\"mcp__circleci-mcp-server__config_helper\",\"hash\":\"bcd90f18bf38\"},{\"name\":\"mcp__circleci-mcp-server__download_usage_api_data\",\"hash\":\"df3e8366d548\"},{\"name\":\"mcp__circleci-mcp-server__find_flaky_tests\",\"hash\":\"35d48aad6c1b\"},{\"name\":\"mcp__circleci-mcp-server__find_underused_resource_classes\",\"hash\":\"b6b544482103\"},{\"name\":\"mcp__circleci-mcp-server__get_build_failure_logs\",\"hash\":\"a84b7eb33376\"},{\"name\":\"mcp__circleci-mcp-server__get_job_test_results\",\"hash\":\"95a3008792d4\"},{\"name\":\"mcp__circleci-mcp-server__get_latest_pipeline_status\",\"hash\":\"ea60228bec14\"},{\"name\":\"mcp__circleci-mcp-server__list_artifacts\",\"hash\":\"8c9753f10d8a\"},{\"name\":\"mcp__circleci-mcp-server__list_component_versions\",\"hash\":\"cac90df13b07\"},{\"name\":\"mcp__circleci-mcp-server__list_followed_projects\",\"hash\":\"bf86651fa262\"},{\"name\":\"mcp__circleci-mcp-server__rerun_workflow\",\"hash\":\"c85db32f4ab5\"},{\"name\":\"mcp__circleci-mcp-server__run_pipeline\",\"hash\":\"0f2f6b8d2936\"},{\"name\":\"mcp__circleci-mcp-server__run_rollback_pipeline\",\"hash\":\"abfacf237ce4\"},{\"name\":\"mcp__playwright__browser_click\",\"hash\":\"91de7aecd638\"},{\"name\":\"mcp__playwright__browser_close\",\"hash\":\"e98f666ea071\"},{\"name\":\"mcp__playwright__browser_console_messages\",\"hash\":\"82ff489beb79\"},{\"name\":\"mcp__playwright__browser_drag\",\"hash\":\"65acccb5d2c1\"},{\"name\":\"mcp__playwright__browser_drop\",\"hash\":\"3c8a52e5451e\"},{\"name\":\"mcp__playwright__browser_emulate_media\",\"hash\":\"6756c94f272a\"},{\"name\":\"mcp__playwright__browser_evaluate\",\"hash\":\"004c2c32370c\"},{\"name\":\"mcp__playwright__browser_file_upload\",\"hash\":\"c85100e222ce\"},{\"name\":\"mcp__playwright__browser_fill_form\",\"hash\":\"c1e1e58fbdae\"},{\"name\":\"mcp__playwright__browser_find\",\"hash\":\"15bd7a67e0ee\"},{\"name\":\"mcp__playwright__browser_handle_dialog\",\"hash\":\"53ee7d0c23d0\"},{\"name\":\"mcp__playwright__browser_hover\",\"hash\":\"5298590d93e1\"},{\"name\":\"mcp__playwright__browser_navigate\",\"hash\":\"13af28143cf5\"},{\"name\":\"mcp__playwright__browser_navigate_back\",\"hash\":\"4d22b2a379fe\"},{\"name\":\"mcp__playwright__browser_network_request\",\"hash\":\"38fdda66d74c\"},{\"name\":\"mcp__playwright__browser_network_requests\",\"hash\":\"4a14c080f656\"},{\"name\":\"mcp__playwright__browser_press_key\",\"hash\":\"0e6f0a5adf21\"},{\"name\":\"mcp__playwright__browser_resize\",\"hash\":\"7288e0cc1a79\"},{\"name\":\"mcp__playwright__browser_run_code_unsafe\",\"hash\":\"01ca95060d3c\"},{\"name\":\"mcp__playwright__browser_select_option\",\"hash\":\"76837ea235f1\"},{\"name\":\"mcp__playwright__browser_snapshot\",\"hash\":\"3fd890550de1\"},{\"name\":\"mcp__playwright__browser_tabs\",\"hash\":\"522a8a555576\"},{\"name\":\"mcp__playwright__browser_take_screenshot\",\"hash\":\"233d844004d3\"},{\"name\":\"mcp__playwright__browser_type\",\"hash\":\"a09159e7094c\"},{\"name\":\"mcp__playwright__browser_wait_for\",\"hash\":\"844ffa5b2657\"}]" + } + }, + { + "key": "tools_count", + "value": { + "intValue": 67 + } + }, + { + "key": "new_context_message_count", + "value": { + "intValue": 2 + } + }, + { + "key": "system_reminders_count", + "value": { + "intValue": 3 + } + }, + { + "key": "new_context", + "value": { + "stringValue": "[USER]\nUse Bash to run 'ls' in this directory, then use Read to read agent.py, and summarize in 2 sentences what it does." + } + }, + { + "key": "system_reminders", + "value": { + "stringValue": "\nAs you answer the user's questions, you can use the following context:\n# currentDate\nToday's date is 2026-10-01.\n" + } + }, + { + "key": "duration_ms", + "value": { + "intValue": 2211 + } + }, + { + "key": "input_tokens", + "value": { + "intValue": 2 + } + }, + { + "key": "output_tokens", + "value": { + "intValue": 142 + } + }, + { + "key": "cache_read_tokens", + "value": { + "intValue": 64465 + } + }, + { + "key": "cache_creation_tokens", + "value": { + "intValue": 0 + } + }, + { + "key": "success", + "value": { + "boolValue": true + } + }, + { + "key": "attempt", + "value": { + "intValue": 1 + } + }, + { + "key": "response.has_tool_call", + "value": { + "boolValue": true + } + }, + { + "key": "ttft_ms", + "value": { + "intValue": 1501 + } + }, + { + "key": "first_content_ms", + "value": { + "intValue": 1502 + } + }, + { + "key": "effort", + "value": { + "stringValue": "medium" + } + }, + { + "key": "stop_reason", + "value": { + "stringValue": "tool_use" + } + }, + { + "key": "gen_ai.response.finish_reasons", + "value": { + "arrayValue": { + "values": [ + { + "stringValue": "tool_use" + } + ] + } + } + } + ], + "droppedAttributesCount": 0, + "events": [ + { + "attributes": [ + { + "key": "attempt", + "value": { + "intValue": 1 + } + } + ], + "name": "gen_ai.request.attempt", + "timeUnixNano": "1790903554143646000", + "droppedAttributesCount": 0 + } + ], + "droppedEventsCount": 0, + "status": { + "code": 0 + }, + "links": [], + "droppedLinksCount": 0, + "flags": 257 + }, + { + "traceId": "eab5340b3073943f63df1ff8d5b42db3", + "spanId": "854082e87e4e6073", + "parentSpanId": "f2c724a68fdf61b0", + "name": "claude_code.hook", + "kind": 1, + "startTimeUnixNano": "1790903556063000000", + "endTimeUnixNano": "1790903556065091708", + "attributes": [ + { + "key": "user.id", + "value": { + "stringValue": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "key": "session.id", + "value": { + "stringValue": "efddb30e-4074-43fe-97d6-d10785429cd5" + } + }, + { + "key": "span.type", + "value": { + "stringValue": "hook" + } + }, + { + "key": "hook_event", + "value": { + "stringValue": "PreToolUse" + } + }, + { + "key": "hook_name", + "value": { + "stringValue": "PreToolUse:Bash" + } + }, + { + "key": "num_hooks", + "value": { + "intValue": 1 + } + }, + { + "key": "hook_definitions", + "value": { + "stringValue": "[{\"type\":\"command\",\"command\":\"/workspace/.claude/hooks/notify.sh\"}]" + } + }, + { + "key": "duration_ms", + "value": { + "intValue": 2 + } + }, + { + "key": "num_success", + "value": { + "intValue": 1 + } + }, + { + "key": "num_blocking", + "value": { + "intValue": 0 + } + }, + { + "key": "num_non_blocking_error", + "value": { + "intValue": 0 + } + }, + { + "key": "num_cancelled", + "value": { + "intValue": 0 + } + } + ], + "droppedAttributesCount": 0, + "events": [], + "droppedEventsCount": 0, + "status": { + "code": 0 + }, + "links": [], + "droppedLinksCount": 0, + "flags": 257 + }, + { + "traceId": "eab5340b3073943f63df1ff8d5b42db3", + "spanId": "54f2045463e7a662", + "parentSpanId": "8424fac5bab20a92", + "name": "claude_code.tool.blocked_on_user", + "kind": 1, + "startTimeUnixNano": "1790903556066000000", + "endTimeUnixNano": "1790903556069516458", + "attributes": [ + { + "key": "user.id", + "value": { + "stringValue": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "key": "session.id", + "value": { + "stringValue": "efddb30e-4074-43fe-97d6-d10785429cd5" + } + }, + { + "key": "span.type", + "value": { + "stringValue": "tool.blocked_on_user" + } + }, + { + "key": "duration_ms", + "value": { + "intValue": 3 + } + }, + { + "key": "decision", + "value": { + "stringValue": "unknown" + } + }, + { + "key": "source", + "value": { + "stringValue": "unknown" + } + } + ], + "droppedAttributesCount": 0, + "events": [], + "droppedEventsCount": 0, + "status": { + "code": 0 + }, + "links": [], + "droppedLinksCount": 0, + "flags": 257 + }, + { + "traceId": "eab5340b3073943f63df1ff8d5b42db3", + "spanId": "8424fac5bab20a92", + "parentSpanId": "f2c724a68fdf61b0", + "name": "claude_code.tool", + "kind": 1, + "startTimeUnixNano": "1790903556066000000", + "endTimeUnixNano": "1790903556556444875", + "attributes": [ + { + "key": "user.id", + "value": { + "stringValue": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "key": "session.id", + "value": { + "stringValue": "efddb30e-4074-43fe-97d6-d10785429cd5" + } + }, + { + "key": "span.type", + "value": { + "stringValue": "tool" + } + }, + { + "key": "tool_name", + "value": { + "stringValue": "Bash" + } + }, + { + "key": "tool_name_safe", + "value": { + "stringValue": "Bash" + } + }, + { + "key": "full_command", + "value": { + "stringValue": "ls" + } + }, + { + "key": "bash_command_class", + "value": { + "stringValue": "file_search" + } + }, + { + "key": "bash_argv0", + "value": { + "stringValue": "ls" + } + }, + { + "key": "tool_use_id", + "value": { + "stringValue": "toolu_01DduwZEneZSy9fyFScexRKh" + } + }, + { + "key": "gen_ai.tool.call.id", + "value": { + "stringValue": "toolu_01DduwZEneZSy9fyFScexRKh" + } + }, + { + "key": "tool_input", + "value": { + "stringValue": "[TOOL INPUT: Bash]\n{\"command\":\"ls\",\"description\":\"List files in current directory\"}" + } + }, + { + "key": "duration_ms", + "value": { + "intValue": 490 + } + }, + { + "key": "new_context", + "value": { + "stringValue": "[TOOL RESULT: Bash]\n{\"stdout\":\"agent.py\",\"stderr\":\"\",\"interrupted\":false,\"isImage\":false,\"noOutputExpected\":false}" + } + } + ], + "droppedAttributesCount": 0, + "events": [ + { + "attributes": [ + { + "key": "bash_command", + "value": { + "stringValue": "ls" + } + }, + { + "key": "output", + "value": { + "stringValue": "agent.py" + } + } + ], + "name": "tool.output", + "timeUnixNano": "1790903556556378875", + "droppedAttributesCount": 0 + } + ], + "droppedEventsCount": 0, + "status": { + "code": 0 + }, + "links": [], + "droppedLinksCount": 0, + "flags": 257 + }, + { + "traceId": "eab5340b3073943f63df1ff8d5b42db3", + "spanId": "da4be56bb2765e6d", + "parentSpanId": "8424fac5bab20a92", + "name": "claude_code.tool.execution", + "kind": 1, + "startTimeUnixNano": "1790903556070000000", + "endTimeUnixNano": "1790903556556723292", + "attributes": [ + { + "key": "user.id", + "value": { + "stringValue": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "key": "session.id", + "value": { + "stringValue": "efddb30e-4074-43fe-97d6-d10785429cd5" + } + }, + { + "key": "span.type", + "value": { + "stringValue": "tool.execution" + } + }, + { + "key": "tool_use_id", + "value": { + "stringValue": "toolu_01DduwZEneZSy9fyFScexRKh" + } + }, + { + "key": "gen_ai.tool.call.id", + "value": { + "stringValue": "toolu_01DduwZEneZSy9fyFScexRKh" + } + }, + { + "key": "duration_ms", + "value": { + "intValue": 487 + } + }, + { + "key": "success", + "value": { + "boolValue": true + } + } + ], + "droppedAttributesCount": 0, + "events": [], + "droppedEventsCount": 0, + "status": { + "code": 0 + }, + "links": [], + "droppedLinksCount": 0, + "flags": 257 + }, + { + "traceId": "eab5340b3073943f63df1ff8d5b42db3", + "spanId": "136b9d479f0df250", + "parentSpanId": "f2c724a68fdf61b0", + "name": "claude_code.hook", + "kind": 1, + "startTimeUnixNano": "1790903556338000000", + "endTimeUnixNano": "1790903556338853041", + "attributes": [ + { + "key": "user.id", + "value": { + "stringValue": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "key": "session.id", + "value": { + "stringValue": "efddb30e-4074-43fe-97d6-d10785429cd5" + } + }, + { + "key": "span.type", + "value": { + "stringValue": "hook" + } + }, + { + "key": "hook_event", + "value": { + "stringValue": "PreToolUse" + } + }, + { + "key": "hook_name", + "value": { + "stringValue": "PreToolUse:Read" + } + }, + { + "key": "num_hooks", + "value": { + "intValue": 1 + } + }, + { + "key": "hook_definitions", + "value": { + "stringValue": "[{\"type\":\"command\",\"command\":\"/workspace/.claude/hooks/notify.sh\"}]" + } + }, + { + "key": "duration_ms", + "value": { + "intValue": 1 + } + }, + { + "key": "num_success", + "value": { + "intValue": 1 + } + }, + { + "key": "num_blocking", + "value": { + "intValue": 0 + } + }, + { + "key": "num_non_blocking_error", + "value": { + "intValue": 0 + } + }, + { + "key": "num_cancelled", + "value": { + "intValue": 0 + } + } + ], + "droppedAttributesCount": 0, + "events": [], + "droppedEventsCount": 0, + "status": { + "code": 0 + }, + "links": [], + "droppedLinksCount": 0, + "flags": 257 + }, + { + "traceId": "eab5340b3073943f63df1ff8d5b42db3", + "spanId": "e72fb4376fbc390a", + "parentSpanId": "272c1ac0706d67ea", + "name": "claude_code.tool.blocked_on_user", + "kind": 1, + "startTimeUnixNano": "1790903556339000000", + "endTimeUnixNano": "1790903556341383250", + "attributes": [ + { + "key": "user.id", + "value": { + "stringValue": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "key": "session.id", + "value": { + "stringValue": "efddb30e-4074-43fe-97d6-d10785429cd5" + } + }, + { + "key": "span.type", + "value": { + "stringValue": "tool.blocked_on_user" + } + }, + { + "key": "duration_ms", + "value": { + "intValue": 2 + } + }, + { + "key": "decision", + "value": { + "stringValue": "unknown" + } + }, + { + "key": "source", + "value": { + "stringValue": "unknown" + } + } + ], + "droppedAttributesCount": 0, + "events": [], + "droppedEventsCount": 0, + "status": { + "code": 0 + }, + "links": [], + "droppedLinksCount": 0, + "flags": 257 + }, + { + "traceId": "eab5340b3073943f63df1ff8d5b42db3", + "spanId": "272c1ac0706d67ea", + "parentSpanId": "f2c724a68fdf61b0", + "name": "claude_code.tool", + "kind": 1, + "startTimeUnixNano": "1790903556339000000", + "endTimeUnixNano": "1790903556342962542", + "attributes": [ + { + "key": "user.id", + "value": { + "stringValue": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "key": "session.id", + "value": { + "stringValue": "efddb30e-4074-43fe-97d6-d10785429cd5" + } + }, + { + "key": "span.type", + "value": { + "stringValue": "tool" + } + }, + { + "key": "tool_name", + "value": { + "stringValue": "Read" + } + }, + { + "key": "tool_name_safe", + "value": { + "stringValue": "Read" + } + }, + { + "key": "file_path", + "value": { + "stringValue": "/workspace/agent.py" + } + }, + { + "key": "tool_use_id", + "value": { + "stringValue": "toolu_013N7z8L1z2qM2q3mSiUkwD6" + } + }, + { + "key": "gen_ai.tool.call.id", + "value": { + "stringValue": "toolu_013N7z8L1z2qM2q3mSiUkwD6" + } + }, + { + "key": "tool_input", + "value": { + "stringValue": "[TOOL INPUT: Read]\n{\"file_path\":\"/workspace/agent.py\"}" + } + }, + { + "key": "duration_ms", + "value": { + "intValue": 4 + } + }, + { + "key": "new_context", + "value": { + "stringValue": "[TOOL RESULT: Read]\n{\"type\":\"text\",\"file\":{\"filePath\":\"/workspace/agent.py\",\"content\":\"import asyncio\\nimport os\\nimport sys\\n\\nfrom claude_agent_sdk import AssistantMessage, ClaudeAgentOptions, ResultMessage, TextBlock, query\\n\\nPROXY = os.environ.get(\\\"LITELLM_URL\\\", \\\"http://localhost:4000\\\")\\nKEY = os.environ[\\\"LITELLM_API_KEY\\\"]\\n\\nOTEL_ENV = {\\n \\\"CLAUDE_CODE_ENABLE_TELEMETRY\\\": \\\"1\\\",\\n \\\"CLAUDE_CODE_ENHANCED_TELEMETRY_BETA\\\": \\\"1\\\",\\n \\\"OTEL_TRACES_EXPORTER\\\": \\\"otlp\\\",\\n \\\"OTEL_METRICS_EXPORTER\\\": \\\"none\\\",\\n \\\"OTEL_LOGS_EXPORTER\\\": \\\"none\\\",\\n \\\"OTEL_EXPORTER_OTLP_PROTOCOL\\\": \\\"http/protobuf\\\",\\n \\\"OTEL_EXPORTER_OTLP_ENDPOINT\\\": PROXY,\\n \\\"OTEL_EXPORTER_OTLP_HEADERS\\\": f\\\"Authorization=Bearer {KEY}\\\",\\n \\\"OTEL_SERVICE_NAME\\\": \\\"claude-agent-sdk-demo\\\",\\n \\\"OTEL_TRACES_EXPORT_INTERVAL\\\": \\\"1000\\\",\\n \\\"OTEL_LOG_USER_PROMPTS\\\": \\\"1\\\",\\n \\\"OTEL_LOG_TOOL_DETAILS\\\": \\\"1\\\",\\n \\\"OTEL_LOG_TOOL_CONTENT\\\": \\\"1\\\",\\n \\\"ANTHROPIC_BASE_URL\\\": PROXY,\\n \\\"ANTHROPIC_AUTH_TOKEN\\\": KEY,\\n \\\"CLAUDE_CODE_PROPAGATE_TRACEPARENT\\\": \\\"1\\\",\\n}\\n\\n\\nasync def main(prompt: str) -> None:\\n options = ClaudeAgentOptions(\\n model=os.environ.get(\\\"AGENT_MODEL\\\", \\\"claude-sonnet-5-5\\\"),\\n allowed_tools=[\\\"Bash\\\", \\\"Read\\\", \\\"Glob\\\", \\\"Grep\\\"],\\n permission_mode=\\\"bypassPermissions\\\",\\n cwd=os.path.dirname(os.path.abspath(__file__)),\\n env=OTEL_ENV,\\n max_turns=8,\\n )\\n async for message in query(prompt=prompt, options=options):\\n if isinstance(message, AssistantMessage):\\n for block in message.content:\\n if isinstance(block, TextBlock):\\n print(block.text)\\n elif isinstance(message, ResultMessage):\\n print(f\\\"\\\\n[done] turns={message.num_turns} cost=${message.total_cost_usd} error={message.is_error}\\\")\\n await asyncio.sleep(3)\\n\\n\\nif __name__ == \\\"__main__\\\":\\n asyncio.run(main(sys.argv[1] if len(sys.argv) > 1 else \\\"List the files in this directory, read agent.py, and summarize in 2 sentences what it does.\\\"))\\n\",\"numLines\":51,\"startLine\":1,\"totalLines\":51}}" + } + } + ], + "droppedAttributesCount": 0, + "events": [ + { + "attributes": [ + { + "key": "file_path", + "value": { + "stringValue": "/workspace/agent.py" + } + }, + { + "key": "content", + "value": { + "stringValue": "import asyncio\nimport os\nimport sys\n\nfrom claude_agent_sdk import AssistantMessage, ClaudeAgentOptions, ResultMessage, TextBlock, query\n\nPROXY = os.environ.get(\"LITELLM_URL\", \"http://localhost:4000\")\nKEY = os.environ[\"LITELLM_API_KEY\"]\n\nOTEL_ENV = {\n \"CLAUDE_CODE_ENABLE_TELEMETRY\": \"1\",\n \"CLAUDE_CODE_ENHANCED_TELEMETRY_BETA\": \"1\",\n \"OTEL_TRACES_EXPORTER\": \"otlp\",\n \"OTEL_METRICS_EXPORTER\": \"none\",\n \"OTEL_LOGS_EXPORTER\": \"none\",\n \"OTEL_EXPORTER_OTLP_PROTOCOL\": \"http/protobuf\",\n \"OTEL_EXPORTER_OTLP_ENDPOINT\": PROXY,\n \"OTEL_EXPORTER_OTLP_HEADERS\": f\"Authorization=Bearer {KEY}\",\n \"OTEL_SERVICE_NAME\": \"claude-agent-sdk-demo\",\n \"OTEL_TRACES_EXPORT_INTERVAL\": \"1000\",\n \"OTEL_LOG_USER_PROMPTS\": \"1\",\n \"OTEL_LOG_TOOL_DETAILS\": \"1\",\n \"OTEL_LOG_TOOL_CONTENT\": \"1\",\n \"ANTHROPIC_BASE_URL\": PROXY,\n \"ANTHROPIC_AUTH_TOKEN\": KEY,\n \"CLAUDE_CODE_PROPAGATE_TRACEPARENT\": \"1\",\n}\n\n\nasync def main(prompt: str) -> None:\n options = ClaudeAgentOptions(\n model=os.environ.get(\"AGENT_MODEL\", \"claude-sonnet-5-5\"),\n allowed_tools=[\"Bash\", \"Read\", \"Glob\", \"Grep\"],\n permission_mode=\"bypassPermissions\",\n cwd=os.path.dirname(os.path.abspath(__file__)),\n env=OTEL_ENV,\n max_turns=8,\n )\n async for message in query(prompt=prompt, options=options):\n if isinstance(message, AssistantMessage):\n for block in message.content:\n if isinstance(block, TextBlock):\n print(block.text)\n elif isinstance(message, ResultMessage):\n print(f\"\\n[done] turns={message.num_turns} cost=${message.total_cost_usd} error={message.is_error}\")\n await asyncio.sleep(3)\n\n\nif __name__ == \"__main__\":\n asyncio.run(main(sys.argv[1] if len(sys.argv) > 1 else \"List the files in this directory, read agent.py, and summarize in 2 sentences what it does.\"))\n" + } + } + ], + "name": "tool.output", + "timeUnixNano": "1790903556342885417", + "droppedAttributesCount": 0 + } + ], + "droppedEventsCount": 0, + "status": { + "code": 0 + }, + "links": [], + "droppedLinksCount": 0, + "flags": 257 + }, + { + "traceId": "eab5340b3073943f63df1ff8d5b42db3", + "spanId": "2deab56c3fcadd65", + "parentSpanId": "272c1ac0706d67ea", + "name": "claude_code.tool.execution", + "kind": 1, + "startTimeUnixNano": "1790903556341000000", + "endTimeUnixNano": "1790903556342440709", + "attributes": [ + { + "key": "user.id", + "value": { + "stringValue": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "key": "session.id", + "value": { + "stringValue": "efddb30e-4074-43fe-97d6-d10785429cd5" + } + }, + { + "key": "span.type", + "value": { + "stringValue": "tool.execution" + } + }, + { + "key": "tool_use_id", + "value": { + "stringValue": "toolu_013N7z8L1z2qM2q3mSiUkwD6" + } + }, + { + "key": "gen_ai.tool.call.id", + "value": { + "stringValue": "toolu_013N7z8L1z2qM2q3mSiUkwD6" + } + }, + { + "key": "duration_ms", + "value": { + "intValue": 1 + } + }, + { + "key": "success", + "value": { + "boolValue": true + } + } + ], + "droppedAttributesCount": 0, + "events": [], + "droppedEventsCount": 0, + "status": { + "code": 0 + }, + "links": [], + "droppedLinksCount": 0, + "flags": 257 + }, + { + "traceId": "eab5340b3073943f63df1ff8d5b42db3", + "spanId": "6ce31fa350c73483", + "parentSpanId": "f2c724a68fdf61b0", + "name": "claude_code.hook", + "kind": 1, + "startTimeUnixNano": "1790903556344000000", + "endTimeUnixNano": "1790903556352695167", + "attributes": [ + { + "key": "user.id", + "value": { + "stringValue": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "key": "session.id", + "value": { + "stringValue": "efddb30e-4074-43fe-97d6-d10785429cd5" + } + }, + { + "key": "span.type", + "value": { + "stringValue": "hook" + } + }, + { + "key": "hook_event", + "value": { + "stringValue": "PostToolUse" + } + }, + { + "key": "hook_name", + "value": { + "stringValue": "PostToolUse:Read" + } + }, + { + "key": "num_hooks", + "value": { + "intValue": 3 + } + }, + { + "key": "hook_definitions", + "value": { + "stringValue": "[{\"type\":\"command\",\"command\":\"/workspace/.claude/hooks/notify.sh\"}]" + } + }, + { + "key": "duration_ms", + "value": { + "intValue": 9 + } + }, + { + "key": "num_success", + "value": { + "intValue": 3 + } + }, + { + "key": "num_blocking", + "value": { + "intValue": 0 + } + }, + { + "key": "num_non_blocking_error", + "value": { + "intValue": 0 + } + }, + { + "key": "num_cancelled", + "value": { + "intValue": 0 + } + } + ], + "droppedAttributesCount": 0, + "events": [], + "droppedEventsCount": 0, + "status": { + "code": 0 + }, + "links": [], + "droppedLinksCount": 0, + "flags": 257 + }, + { + "traceId": "eab5340b3073943f63df1ff8d5b42db3", + "spanId": "ae2da48ea097cc66", + "parentSpanId": "f2c724a68fdf61b0", + "name": "claude_code.hook", + "kind": 1, + "startTimeUnixNano": "1790903556557000000", + "endTimeUnixNano": "1790903556565797375", + "attributes": [ + { + "key": "user.id", + "value": { + "stringValue": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "key": "session.id", + "value": { + "stringValue": "efddb30e-4074-43fe-97d6-d10785429cd5" + } + }, + { + "key": "span.type", + "value": { + "stringValue": "hook" + } + }, + { + "key": "hook_event", + "value": { + "stringValue": "PostToolUse" + } + }, + { + "key": "hook_name", + "value": { + "stringValue": "PostToolUse:Bash" + } + }, + { + "key": "num_hooks", + "value": { + "intValue": 4 + } + }, + { + "key": "hook_definitions", + "value": { + "stringValue": "[{\"type\":\"command\",\"command\":\"/workspace/.claude/hooks/notify.sh\"}]" + } + }, + { + "key": "duration_ms", + "value": { + "intValue": 9 + } + }, + { + "key": "num_success", + "value": { + "intValue": 4 + } + }, + { + "key": "num_blocking", + "value": { + "intValue": 0 + } + }, + { + "key": "num_non_blocking_error", + "value": { + "intValue": 0 + } + }, + { + "key": "num_cancelled", + "value": { + "intValue": 0 + } + } + ], + "droppedAttributesCount": 0, + "events": [], + "droppedEventsCount": 0, + "status": { + "code": 0 + }, + "links": [], + "droppedLinksCount": 0, + "flags": 257 + }, + { + "traceId": "eab5340b3073943f63df1ff8d5b42db3", + "spanId": "97518db411b06070", + "parentSpanId": "f2c724a68fdf61b0", + "name": "claude_code.llm_request", + "kind": 1, + "startTimeUnixNano": "1790903556573000000", + "endTimeUnixNano": "1790903559563597125", + "attributes": [ + { + "key": "user.id", + "value": { + "stringValue": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "key": "session.id", + "value": { + "stringValue": "efddb30e-4074-43fe-97d6-d10785429cd5" + } + }, + { + "key": "span.type", + "value": { + "stringValue": "llm_request" + } + }, + { + "key": "model", + "value": { + "stringValue": "claude-sonnet-5-5" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "anthropic" + } + }, + { + "key": "gen_ai.request.model", + "value": { + "stringValue": "claude-sonnet-5-5" + } + }, + { + "key": "llm_request.context", + "value": { + "stringValue": "interaction" + } + }, + { + "key": "speed", + "value": { + "stringValue": "normal" + } + }, + { + "key": "query_source", + "value": { + "stringValue": "sdk" + } + }, + { + "key": "query_source_safe", + "value": { + "stringValue": "sdk" + } + }, + { + "key": "system_prompt_hash", + "value": { + "stringValue": "sp_754c39bc2203" + } + }, + { + "key": "system_prompt_preview", + "value": { + "stringValue": "x-anthropic-billing-header: cc_version=2.1.286.d3f; cc_entrypoint=sdk-py;\n\nYou are a Claude agent, built on Anthropic's Claude Agent SDK." + } + }, + { + "key": "system_prompt_length", + "value": { + "intValue": 137 + } + }, + { + "key": "tools", + "value": { + "stringValue": "[{\"name\":\"Agent\",\"hash\":\"164e46314bb0\"},{\"name\":\"Bash\",\"hash\":\"81dc4be713e1\"},{\"name\":\"CronCreate\",\"hash\":\"e4c660878de6\"},{\"name\":\"CronDelete\",\"hash\":\"4e244a652bf3\"},{\"name\":\"CronList\",\"hash\":\"6154cd8fa452\"},{\"name\":\"DesignSync\",\"hash\":\"390c2da6fbb7\"},{\"name\":\"Edit\",\"hash\":\"6430d0c60f48\"},{\"name\":\"EnterWorktree\",\"hash\":\"3f353219c93a\"},{\"name\":\"ExitWorktree\",\"hash\":\"79e242d1cef5\"},{\"name\":\"Glob\",\"hash\":\"341f5d0a2e2f\"},{\"name\":\"Grep\",\"hash\":\"1d3c47f9148f\"},{\"name\":\"ListAgents\",\"hash\":\"0a3591a577c6\"},{\"name\":\"ListMcpResourcesTool\",\"hash\":\"80428e7012e5\"},{\"name\":\"LSP\",\"hash\":\"b7be7911ea66\"},{\"name\":\"Monitor\",\"hash\":\"53eb832de993\"},{\"name\":\"NotebookEdit\",\"hash\":\"d88b4bf2ec93\"},{\"name\":\"PushNotification\",\"hash\":\"74f8dcf21b80\"},{\"name\":\"Read\",\"hash\":\"680529a1e735\"},{\"name\":\"ReadMcpResourceDirTool\",\"hash\":\"f87a091f6f1e\"},{\"name\":\"ReadMcpResourceTool\",\"hash\":\"9f256f5afee4\"},{\"name\":\"ReportFindings\",\"hash\":\"d742f97bb17e\"},{\"name\":\"ScheduleWakeup\",\"hash\":\"24fdfa8e91c8\"},{\"name\":\"SendMessage\",\"hash\":\"eee44afb16ba\"},{\"name\":\"Skill\",\"hash\":\"c3282cbcede5\"},{\"name\":\"TaskStop\",\"hash\":\"b145464cdabc\"},{\"name\":\"WebFetch\",\"hash\":\"e1fbaacd430d\"},{\"name\":\"WebSearch\",\"hash\":\"79a806bff741\"},{\"name\":\"Workflow\",\"hash\":\"b09d3792832d\"},{\"name\":\"Write\",\"hash\":\"416c9b17ff1f\"},{\"name\":\"mcp__circleci-mcp-server__config_helper\",\"hash\":\"bcd90f18bf38\"},{\"name\":\"mcp__circleci-mcp-server__download_usage_api_data\",\"hash\":\"df3e8366d548\"},{\"name\":\"mcp__circleci-mcp-server__find_flaky_tests\",\"hash\":\"35d48aad6c1b\"},{\"name\":\"mcp__circleci-mcp-server__find_underused_resource_classes\",\"hash\":\"b6b544482103\"},{\"name\":\"mcp__circleci-mcp-server__get_build_failure_logs\",\"hash\":\"a84b7eb33376\"},{\"name\":\"mcp__circleci-mcp-server__get_job_test_results\",\"hash\":\"95a3008792d4\"},{\"name\":\"mcp__circleci-mcp-server__get_latest_pipeline_status\",\"hash\":\"ea60228bec14\"},{\"name\":\"mcp__circleci-mcp-server__list_artifacts\",\"hash\":\"8c9753f10d8a\"},{\"name\":\"mcp__circleci-mcp-server__list_component_versions\",\"hash\":\"cac90df13b07\"},{\"name\":\"mcp__circleci-mcp-server__list_followed_projects\",\"hash\":\"bf86651fa262\"},{\"name\":\"mcp__circleci-mcp-server__rerun_workflow\",\"hash\":\"c85db32f4ab5\"},{\"name\":\"mcp__circleci-mcp-server__run_pipeline\",\"hash\":\"0f2f6b8d2936\"},{\"name\":\"mcp__circleci-mcp-server__run_rollback_pipeline\",\"hash\":\"abfacf237ce4\"},{\"name\":\"mcp__playwright__browser_click\",\"hash\":\"91de7aecd638\"},{\"name\":\"mcp__playwright__browser_close\",\"hash\":\"e98f666ea071\"},{\"name\":\"mcp__playwright__browser_console_messages\",\"hash\":\"82ff489beb79\"},{\"name\":\"mcp__playwright__browser_drag\",\"hash\":\"65acccb5d2c1\"},{\"name\":\"mcp__playwright__browser_drop\",\"hash\":\"3c8a52e5451e\"},{\"name\":\"mcp__playwright__browser_emulate_media\",\"hash\":\"6756c94f272a\"},{\"name\":\"mcp__playwright__browser_evaluate\",\"hash\":\"004c2c32370c\"},{\"name\":\"mcp__playwright__browser_file_upload\",\"hash\":\"c85100e222ce\"},{\"name\":\"mcp__playwright__browser_fill_form\",\"hash\":\"c1e1e58fbdae\"},{\"name\":\"mcp__playwright__browser_find\",\"hash\":\"15bd7a67e0ee\"},{\"name\":\"mcp__playwright__browser_handle_dialog\",\"hash\":\"53ee7d0c23d0\"},{\"name\":\"mcp__playwright__browser_hover\",\"hash\":\"5298590d93e1\"},{\"name\":\"mcp__playwright__browser_navigate\",\"hash\":\"13af28143cf5\"},{\"name\":\"mcp__playwright__browser_navigate_back\",\"hash\":\"4d22b2a379fe\"},{\"name\":\"mcp__playwright__browser_network_request\",\"hash\":\"38fdda66d74c\"},{\"name\":\"mcp__playwright__browser_network_requests\",\"hash\":\"4a14c080f656\"},{\"name\":\"mcp__playwright__browser_press_key\",\"hash\":\"0e6f0a5adf21\"},{\"name\":\"mcp__playwright__browser_resize\",\"hash\":\"7288e0cc1a79\"},{\"name\":\"mcp__playwright__browser_run_code_unsafe\",\"hash\":\"01ca95060d3c\"},{\"name\":\"mcp__playwright__browser_select_option\",\"hash\":\"76837ea235f1\"},{\"name\":\"mcp__playwright__browser_snapshot\",\"hash\":\"3fd890550de1\"},{\"name\":\"mcp__playwright__browser_tabs\",\"hash\":\"522a8a555576\"},{\"name\":\"mcp__playwright__browser_take_screenshot\",\"hash\":\"233d844004d3\"},{\"name\":\"mcp__playwright__browser_type\",\"hash\":\"a09159e7094c\"},{\"name\":\"mcp__playwright__browser_wait_for\",\"hash\":\"844ffa5b2657\"}]" + } + }, + { + "key": "tools_count", + "value": { + "intValue": 67 + } + }, + { + "key": "new_context_message_count", + "value": { + "intValue": 1 + } + }, + { + "key": "new_context", + "value": { + "stringValue": "[TOOL RESULT: toolu_013N7z8L1z2qM2q3mSiUkwD6]\n1\timport asyncio\n2\timport os\n3\timport sys\n4\t\n5\tfrom claude_agent_sdk import AssistantMessage, ClaudeAgentOptions, ResultMessage, TextBlock, query\n6\t\n7\tPROXY = os.environ.get(\"LITELLM_URL\", \"http://localhost:4000\")\n8\tKEY = os.environ[\"LITELLM_API_KEY\"]\n9\t\n10\tOTEL_ENV = {\n11\t \"CLAUDE_CODE_ENABLE_TELEMETRY\": \"1\",\n12\t \"CLAUDE_CODE_ENHANCED_TELEMETRY_BETA\": \"1\",\n13\t \"OTEL_TRACES_EXPORTER\": \"otlp\",\n14\t \"OTEL_METRICS_EXPORTER\": \"none\",\n15\t \"OTEL_LOGS_EXPORTER\": \"none\",\n16\t \"OTEL_EXPORTER_OTLP_PROTOCOL\": \"http/protobuf\",\n17\t \"OTEL_EXPORTER_OTLP_ENDPOINT\": PROXY,\n18\t \"OTEL_EXPORTER_OTLP_HEADERS\": f\"Authorization=Bearer {KEY}\",\n19\t \"OTEL_SERVICE_NAME\": \"claude-agent-sdk-demo\",\n20\t \"OTEL_TRACES_EXPORT_INTERVAL\": \"1000\",\n21\t \"OTEL_LOG_USER_PROMPTS\": \"1\",\n22\t \"OTEL_LOG_TOOL_DETAILS\": \"1\",\n23\t \"OTEL_LOG_TOOL_CONTENT\": \"1\",\n24\t \"ANTHROPIC_BASE_URL\": PROXY,\n25\t \"ANTHROPIC_AUTH_TOKEN\": KEY,\n26\t \"CLAUDE_CODE_PROPAGATE_TRACEPARENT\": \"1\",\n27\t}\n28\t\n29\t\n30\tasync def main(prompt: str) -> None:\n31\t options = ClaudeAgentOptions(\n32\t model=os.environ.get(\"AGENT_MODEL\", \"claude-sonnet-5-5\"),\n33\t allowed_tools=[\"Bash\", \"Read\", \"Glob\", \"Grep\"],\n34\t permission_mode=\"bypassPermissions\",\n35\t cwd=os.path.dirname(os.path.abspath(__file__)),\n36\t env=OTEL_ENV,\n37\t max_turns=8,\n38\t )\n39\t async for message in query(prompt=prompt, options=options):\n40\t if isinstance(message, AssistantMessage):\n41\t for block in message.content:\n42\t if isinstance(block, TextBlock):\n43\t print(block.text)\n44\t elif isinstance(message, ResultMessage):\n45\t print(f\"\\n[done] turns={message.num_turns} cost=${message.total_cost_usd} error={message.is_error}\")\n46\t await asyncio.sleep(3)\n47\t\n48\t\n49\tif __name__ == \"__main__\":\n50\t asyncio.run(main(sys.argv[1] if len(sys.argv) > 1 else \"List the files in this directory, read agent.py, and summarize in 2 sentences what it does.\"))\n51\t\n\n---\n\n[TOOL RESULT: toolu_01DduwZEneZSy9fyFScexRKh]\nagent.py" + } + }, + { + "key": "duration_ms", + "value": { + "intValue": 2990 + } + }, + { + "key": "input_tokens", + "value": { + "intValue": 2 + } + }, + { + "key": "output_tokens", + "value": { + "intValue": 201 + } + }, + { + "key": "cache_read_tokens", + "value": { + "intValue": 65763 + } + }, + { + "key": "cache_creation_tokens", + "value": { + "intValue": 0 + } + }, + { + "key": "success", + "value": { + "boolValue": true + } + }, + { + "key": "attempt", + "value": { + "intValue": 1 + } + }, + { + "key": "response.has_tool_call", + "value": { + "boolValue": false + } + }, + { + "key": "ttft_ms", + "value": { + "intValue": 2947 + } + }, + { + "key": "first_content_ms", + "value": { + "intValue": 2948 + } + }, + { + "key": "effort", + "value": { + "stringValue": "medium" + } + }, + { + "key": "response.model_output", + "value": { + "stringValue": "`agent.py` is a script that runs a Claude Agent SDK agent. The agent can use Bash, Read, Glob and Grep, runs with permissions bypassed, and is capped at 8 turns. It takes a prompt from the command line and prints the assistant's text and a final summary of turns, cost and error status. Its API traffic goes through a LiteLLM proxy, which is set by `LITELLM_URL` and authenticated with `LITELLM_API_KEY`. It also turns on OpenTelemetry tracing and sends the traces to that same proxy.\n\nThe directory contains only `agent.py`." + } + }, + { + "key": "stop_reason", + "value": { + "stringValue": "end_turn" + } + }, + { + "key": "gen_ai.response.finish_reasons", + "value": { + "arrayValue": { + "values": [ + { + "stringValue": "end_turn" + } + ] + } + } + } + ], + "droppedAttributesCount": 0, + "events": [ + { + "attributes": [ + { + "key": "attempt", + "value": { + "intValue": 1 + } + } + ], + "name": "gen_ai.request.attempt", + "timeUnixNano": "1790903556574496750", + "droppedAttributesCount": 0 + } + ], + "droppedEventsCount": 0, + "status": { + "code": 0 + }, + "links": [], + "droppedLinksCount": 0, + "flags": 257 + }, + { + "traceId": "eab5340b3073943f63df1ff8d5b42db3", + "spanId": "4683636de3a73da7", + "parentSpanId": "f2c724a68fdf61b0", + "name": "claude_code.hook", + "kind": 1, + "startTimeUnixNano": "1790903559566000000", + "endTimeUnixNano": "1790903559579699042", + "attributes": [ + { + "key": "user.id", + "value": { + "stringValue": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "key": "session.id", + "value": { + "stringValue": "efddb30e-4074-43fe-97d6-d10785429cd5" + } + }, + { + "key": "span.type", + "value": { + "stringValue": "hook" + } + }, + { + "key": "hook_event", + "value": { + "stringValue": "Stop" + } + }, + { + "key": "hook_name", + "value": { + "stringValue": "Stop" + } + }, + { + "key": "num_hooks", + "value": { + "intValue": 3 + } + }, + { + "key": "hook_definitions", + "value": { + "stringValue": "[{\"type\":\"command\",\"command\":\"/workspace/.claude/hooks/notify.sh\"}]" + } + }, + { + "key": "duration_ms", + "value": { + "intValue": 14 + } + }, + { + "key": "num_success", + "value": { + "intValue": 3 + } + }, + { + "key": "num_blocking", + "value": { + "intValue": 0 + } + }, + { + "key": "num_non_blocking_error", + "value": { + "intValue": 0 + } + }, + { + "key": "num_cancelled", + "value": { + "intValue": 0 + } + } + ], + "droppedAttributesCount": 0, + "events": [], + "droppedEventsCount": 0, + "status": { + "code": 0 + }, + "links": [], + "droppedLinksCount": 0, + "flags": 257 + } + ] + } + ] + } + ] +} diff --git a/tests/test_litellm/tracing/fixtures/claude_agent_sdk_export.json b/tests/test_litellm/tracing/fixtures/claude_agent_sdk_export.json new file mode 100644 index 00000000000..b803e8bb33d --- /dev/null +++ b/tests/test_litellm/tracing/fixtures/claude_agent_sdk_export.json @@ -0,0 +1,1051 @@ +{ + "resourceSpans": [ + { + "resource": { + "attributes": [ + { + "key": "service.name", + "value": { + "stringValue": "claude-agent-sdk-demo" + } + }, + { + "key": "os.type", + "value": { + "stringValue": "linux" + } + }, + { + "key": "os.version", + "value": { + "stringValue": "0.0.0" + } + }, + { + "key": "service.version", + "value": { + "stringValue": "2.1.286" + } + } + ], + "droppedAttributesCount": 0 + }, + "scopeSpans": [ + { + "scope": { + "name": "com.anthropic.claude_code.tracing", + "version": "1.0.0" + }, + "spans": [ + { + "traceId": "62bd44d020c91ca1582693f84716b195", + "spanId": "d5898adbea7afa2d", + "name": "claude_code.llm_request", + "kind": 1, + "startTimeUnixNano": "1790903451499000000", + "endTimeUnixNano": "1790903452928350791", + "attributes": [ + { + "key": "user.id", + "value": { + "stringValue": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "key": "session.id", + "value": { + "stringValue": "3047ee2f-3fe8-4ed3-a99d-8f79b68dde4d" + } + }, + { + "key": "span.type", + "value": { + "stringValue": "llm_request" + } + }, + { + "key": "model", + "value": { + "stringValue": "anthropic/claude-sonnet-5" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "anthropic" + } + }, + { + "key": "gen_ai.request.model", + "value": { + "stringValue": "anthropic/claude-sonnet-5" + } + }, + { + "key": "llm_request.context", + "value": { + "stringValue": "standalone" + } + }, + { + "key": "speed", + "value": { + "stringValue": "normal" + } + }, + { + "key": "query_source_safe", + "value": { + "stringValue": "generate_session_title" + } + }, + { + "key": "duration_ms", + "value": { + "intValue": 1429 + } + }, + { + "key": "input_tokens", + "value": { + "intValue": 1205 + } + }, + { + "key": "output_tokens", + "value": { + "intValue": 15 + } + }, + { + "key": "cache_read_tokens", + "value": { + "intValue": 0 + } + }, + { + "key": "cache_creation_tokens", + "value": { + "intValue": 0 + } + }, + { + "key": "success", + "value": { + "boolValue": true + } + }, + { + "key": "attempt", + "value": { + "intValue": 1 + } + }, + { + "key": "ttft_ms", + "value": { + "intValue": 951 + } + }, + { + "key": "first_content_ms", + "value": { + "intValue": 951 + } + }, + { + "key": "effort", + "value": { + "stringValue": "high" + } + }, + { + "key": "stop_reason", + "value": { + "stringValue": "end_turn" + } + }, + { + "key": "gen_ai.response.finish_reasons", + "value": { + "arrayValue": { + "values": [ + { + "stringValue": "end_turn" + } + ] + } + } + } + ], + "droppedAttributesCount": 0, + "events": [ + { + "attributes": [ + { + "key": "attempt", + "value": { + "intValue": 1 + } + } + ], + "name": "gen_ai.request.attempt", + "timeUnixNano": "1790903451502144375", + "droppedAttributesCount": 0 + } + ], + "droppedEventsCount": 0, + "status": { + "code": 0 + }, + "links": [], + "droppedLinksCount": 0, + "flags": 257 + }, + { + "traceId": "2538c9231567456f0885bd882b364b6e", + "spanId": "3865265b336909b9", + "name": "claude_code.interaction", + "kind": 1, + "startTimeUnixNano": "1790903452664000000", + "endTimeUnixNano": "1790903458583758250", + "attributes": [ + { + "key": "user.id", + "value": { + "stringValue": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "key": "session.id", + "value": { + "stringValue": "3047ee2f-3fe8-4ed3-a99d-8f79b68dde4d" + } + }, + { + "key": "span.type", + "value": { + "stringValue": "interaction" + } + }, + { + "key": "user_prompt", + "value": { + "stringValue": "Use Bash to run 'ls' in this directory, then use Read to read agent.py, and summarize in 2 sentences what it does." + } + }, + { + "key": "user_prompt_length", + "value": { + "intValue": 114 + } + }, + { + "key": "interaction.sequence", + "value": { + "intValue": 1 + } + }, + { + "key": "parent.source", + "value": { + "stringValue": "none" + } + }, + { + "key": "queued_sends", + "value": { + "intValue": 0 + } + }, + { + "key": "interaction.duration_ms", + "value": { + "intValue": 5920 + } + } + ], + "droppedAttributesCount": 0, + "events": [], + "droppedEventsCount": 0, + "status": { + "code": 0 + }, + "links": [], + "droppedLinksCount": 0, + "flags": 257 + }, + { + "traceId": "2538c9231567456f0885bd882b364b6e", + "spanId": "fb67da611ca242e7", + "parentSpanId": "3865265b336909b9", + "name": "claude_code.llm_request", + "kind": 1, + "startTimeUnixNano": "1790903452703000000", + "endTimeUnixNano": "1790903455547050333", + "attributes": [ + { + "key": "user.id", + "value": { + "stringValue": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "key": "session.id", + "value": { + "stringValue": "3047ee2f-3fe8-4ed3-a99d-8f79b68dde4d" + } + }, + { + "key": "span.type", + "value": { + "stringValue": "llm_request" + } + }, + { + "key": "model", + "value": { + "stringValue": "claude-sonnet-5-5" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "anthropic" + } + }, + { + "key": "gen_ai.request.model", + "value": { + "stringValue": "claude-sonnet-5-5" + } + }, + { + "key": "llm_request.context", + "value": { + "stringValue": "interaction" + } + }, + { + "key": "speed", + "value": { + "stringValue": "normal" + } + }, + { + "key": "query_source_safe", + "value": { + "stringValue": "sdk" + } + }, + { + "key": "duration_ms", + "value": { + "intValue": 2844 + } + }, + { + "key": "input_tokens", + "value": { + "intValue": 2 + } + }, + { + "key": "output_tokens", + "value": { + "intValue": 142 + } + }, + { + "key": "cache_read_tokens", + "value": { + "intValue": 0 + } + }, + { + "key": "cache_creation_tokens", + "value": { + "intValue": 64465 + } + }, + { + "key": "success", + "value": { + "boolValue": true + } + }, + { + "key": "attempt", + "value": { + "intValue": 1 + } + }, + { + "key": "ttft_ms", + "value": { + "intValue": 2192 + } + }, + { + "key": "first_content_ms", + "value": { + "intValue": 2193 + } + }, + { + "key": "effort", + "value": { + "stringValue": "medium" + } + }, + { + "key": "stop_reason", + "value": { + "stringValue": "tool_use" + } + }, + { + "key": "gen_ai.response.finish_reasons", + "value": { + "arrayValue": { + "values": [ + { + "stringValue": "tool_use" + } + ] + } + } + } + ], + "droppedAttributesCount": 0, + "events": [ + { + "attributes": [ + { + "key": "attempt", + "value": { + "intValue": 1 + } + } + ], + "name": "gen_ai.request.attempt", + "timeUnixNano": "1790903452703751917", + "droppedAttributesCount": 0 + } + ], + "droppedEventsCount": 0, + "status": { + "code": 0 + }, + "links": [], + "droppedLinksCount": 0, + "flags": 257 + }, + { + "traceId": "2538c9231567456f0885bd882b364b6e", + "spanId": "d2c78e2ec3fbd4ac", + "parentSpanId": "3865265b336909b9", + "name": "claude_code.tool", + "kind": 1, + "startTimeUnixNano": "1790903455353000000", + "endTimeUnixNano": "1790903455882872000", + "attributes": [ + { + "key": "user.id", + "value": { + "stringValue": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "key": "session.id", + "value": { + "stringValue": "3047ee2f-3fe8-4ed3-a99d-8f79b68dde4d" + } + }, + { + "key": "span.type", + "value": { + "stringValue": "tool" + } + }, + { + "key": "tool_name", + "value": { + "stringValue": "Bash" + } + }, + { + "key": "tool_name_safe", + "value": { + "stringValue": "Bash" + } + }, + { + "key": "full_command", + "value": { + "stringValue": "ls" + } + }, + { + "key": "bash_command_class", + "value": { + "stringValue": "file_search" + } + }, + { + "key": "bash_argv0", + "value": { + "stringValue": "ls" + } + }, + { + "key": "tool_use_id", + "value": { + "stringValue": "toolu_01YRNft6BWu1DFLL2Mw84c8p" + } + }, + { + "key": "gen_ai.tool.call.id", + "value": { + "stringValue": "toolu_01YRNft6BWu1DFLL2Mw84c8p" + } + }, + { + "key": "duration_ms", + "value": { + "intValue": 530 + } + } + ], + "droppedAttributesCount": 0, + "events": [ + { + "attributes": [ + { + "key": "bash_command", + "value": { + "stringValue": "ls" + } + }, + { + "key": "output", + "value": { + "stringValue": "agent.py" + } + } + ], + "name": "tool.output", + "timeUnixNano": "1790903455882826792", + "droppedAttributesCount": 0 + } + ], + "droppedEventsCount": 0, + "status": { + "code": 0 + }, + "links": [], + "droppedLinksCount": 0, + "flags": 257 + }, + { + "traceId": "2538c9231567456f0885bd882b364b6e", + "spanId": "eaad0dbd531ae699", + "parentSpanId": "d2c78e2ec3fbd4ac", + "name": "claude_code.tool.blocked_on_user", + "kind": 1, + "startTimeUnixNano": "1790903455354000000", + "endTimeUnixNano": "1790903455357501916", + "attributes": [ + { + "key": "user.id", + "value": { + "stringValue": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "key": "session.id", + "value": { + "stringValue": "3047ee2f-3fe8-4ed3-a99d-8f79b68dde4d" + } + }, + { + "key": "span.type", + "value": { + "stringValue": "tool.blocked_on_user" + } + }, + { + "key": "duration_ms", + "value": { + "intValue": 3 + } + }, + { + "key": "decision", + "value": { + "stringValue": "unknown" + } + }, + { + "key": "source", + "value": { + "stringValue": "unknown" + } + } + ], + "droppedAttributesCount": 0, + "events": [], + "droppedEventsCount": 0, + "status": { + "code": 0 + }, + "links": [], + "droppedLinksCount": 0, + "flags": 257 + }, + { + "traceId": "2538c9231567456f0885bd882b364b6e", + "spanId": "4c67ae4657c849ec", + "parentSpanId": "d2c78e2ec3fbd4ac", + "name": "claude_code.tool.execution", + "kind": 1, + "startTimeUnixNano": "1790903455357000000", + "endTimeUnixNano": "1790903455883180125", + "attributes": [ + { + "key": "user.id", + "value": { + "stringValue": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "key": "session.id", + "value": { + "stringValue": "3047ee2f-3fe8-4ed3-a99d-8f79b68dde4d" + } + }, + { + "key": "span.type", + "value": { + "stringValue": "tool.execution" + } + }, + { + "key": "tool_use_id", + "value": { + "stringValue": "toolu_01YRNft6BWu1DFLL2Mw84c8p" + } + }, + { + "key": "gen_ai.tool.call.id", + "value": { + "stringValue": "toolu_01YRNft6BWu1DFLL2Mw84c8p" + } + }, + { + "key": "duration_ms", + "value": { + "intValue": 526 + } + }, + { + "key": "success", + "value": { + "boolValue": true + } + } + ], + "droppedAttributesCount": 0, + "events": [], + "droppedEventsCount": 0, + "status": { + "code": 0 + }, + "links": [], + "droppedLinksCount": 0, + "flags": 257 + }, + { + "traceId": "2538c9231567456f0885bd882b364b6e", + "spanId": "38c6a9d76fa8b2a0", + "parentSpanId": "9570416bd7cb9814", + "name": "claude_code.tool.blocked_on_user", + "kind": 1, + "startTimeUnixNano": "1790903455540000000", + "endTimeUnixNano": "1790903455542367625", + "attributes": [ + { + "key": "user.id", + "value": { + "stringValue": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "key": "session.id", + "value": { + "stringValue": "3047ee2f-3fe8-4ed3-a99d-8f79b68dde4d" + } + }, + { + "key": "span.type", + "value": { + "stringValue": "tool.blocked_on_user" + } + }, + { + "key": "duration_ms", + "value": { + "intValue": 2 + } + }, + { + "key": "decision", + "value": { + "stringValue": "unknown" + } + }, + { + "key": "source", + "value": { + "stringValue": "unknown" + } + } + ], + "droppedAttributesCount": 0, + "events": [], + "droppedEventsCount": 0, + "status": { + "code": 0 + }, + "links": [], + "droppedLinksCount": 0, + "flags": 257 + }, + { + "traceId": "2538c9231567456f0885bd882b364b6e", + "spanId": "9570416bd7cb9814", + "parentSpanId": "3865265b336909b9", + "name": "claude_code.tool", + "kind": 1, + "startTimeUnixNano": "1790903455540000000", + "endTimeUnixNano": "1790903455544273500", + "attributes": [ + { + "key": "user.id", + "value": { + "stringValue": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "key": "session.id", + "value": { + "stringValue": "3047ee2f-3fe8-4ed3-a99d-8f79b68dde4d" + } + }, + { + "key": "span.type", + "value": { + "stringValue": "tool" + } + }, + { + "key": "tool_name", + "value": { + "stringValue": "Read" + } + }, + { + "key": "tool_name_safe", + "value": { + "stringValue": "Read" + } + }, + { + "key": "file_path", + "value": { + "stringValue": "/workspace/agent.py" + } + }, + { + "key": "tool_use_id", + "value": { + "stringValue": "toolu_013gThXdzSmWcztJH81MeH2p" + } + }, + { + "key": "gen_ai.tool.call.id", + "value": { + "stringValue": "toolu_013gThXdzSmWcztJH81MeH2p" + } + }, + { + "key": "duration_ms", + "value": { + "intValue": 4 + } + } + ], + "droppedAttributesCount": 0, + "events": [ + { + "attributes": [ + { + "key": "file_path", + "value": { + "stringValue": "/workspace/agent.py" + } + }, + { + "key": "content", + "value": { + "stringValue": "import asyncio\nimport os\nimport sys\n\nfrom claude_agent_sdk import AssistantMessage, ClaudeAgentOptions, ResultMessage, TextBlock, query\n\nPROXY = os.environ.get(\"LITELLM_URL\", \"http://localhost:4000\")\nKEY = os.environ[\"LITELLM_API_KEY\"]\n\nOTEL_ENV = {\n \"CLAUDE_CODE_ENABLE_TELEMETRY\": \"1\",\n \"CLAUDE_CODE_ENHANCED_TELEMETRY_BETA\": \"1\",\n \"OTEL_TRACES_EXPORTER\": \"otlp\",\n \"OTEL_METRICS_EXPORTER\": \"none\",\n \"OTEL_LOGS_EXPORTER\": \"none\",\n \"OTEL_EXPORTER_OTLP_PROTOCOL\": \"http/protobuf\",\n \"OTEL_EXPORTER_OTLP_ENDPOINT\": PROXY,\n \"OTEL_EXPORTER_OTLP_HEADERS\": f\"Authorization=Bearer {KEY}\",\n \"OTEL_SERVICE_NAME\": \"claude-agent-sdk-demo\",\n \"OTEL_TRACES_EXPORT_INTERVAL\": \"1000\",\n \"OTEL_LOG_USER_PROMPTS\": \"1\",\n \"OTEL_LOG_TOOL_DETAILS\": \"1\",\n \"OTEL_LOG_TOOL_CONTENT\": \"1\",\n \"ANTHROPIC_BASE_URL\": PROXY,\n \"ANTHROPIC_AUTH_TOKEN\": KEY,\n \"CLAUDE_CODE_PROPAGATE_TRACEPARENT\": \"1\",\n}\n\n\nasync def main(prompt: str) -> None:\n options = ClaudeAgentOptions(\n model=os.environ.get(\"AGENT_MODEL\", \"claude-sonnet-5-5\"),\n allowed_tools=[\"Bash\", \"Read\", \"Glob\", \"Grep\"],\n permission_mode=\"bypassPermissions\",\n cwd=os.path.dirname(os.path.abspath(__file__)),\n env=OTEL_ENV,\n max_turns=8,\n )\n async for message in query(prompt=prompt, options=options):\n if isinstance(message, AssistantMessage):\n for block in message.content:\n if isinstance(block, TextBlock):\n print(block.text)\n elif isinstance(message, ResultMessage):\n print(f\"\\n[done] turns={message.num_turns} cost=${message.total_cost_usd} error={message.is_error}\")\n await asyncio.sleep(3)\n\n\nif __name__ == \"__main__\":\n asyncio.run(main(sys.argv[1] if len(sys.argv) > 1 else \"List the files in this directory, read agent.py, and summarize in 2 sentences what it does.\"))\n" + } + } + ], + "name": "tool.output", + "timeUnixNano": "1790903455544200667", + "droppedAttributesCount": 0 + } + ], + "droppedEventsCount": 0, + "status": { + "code": 0 + }, + "links": [], + "droppedLinksCount": 0, + "flags": 257 + }, + { + "traceId": "2538c9231567456f0885bd882b364b6e", + "spanId": "297bf74886a1c53f", + "parentSpanId": "9570416bd7cb9814", + "name": "claude_code.tool.execution", + "kind": 1, + "startTimeUnixNano": "1790903455542000000", + "endTimeUnixNano": "1790903455543759625", + "attributes": [ + { + "key": "user.id", + "value": { + "stringValue": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "key": "session.id", + "value": { + "stringValue": "3047ee2f-3fe8-4ed3-a99d-8f79b68dde4d" + } + }, + { + "key": "span.type", + "value": { + "stringValue": "tool.execution" + } + }, + { + "key": "tool_use_id", + "value": { + "stringValue": "toolu_013gThXdzSmWcztJH81MeH2p" + } + }, + { + "key": "gen_ai.tool.call.id", + "value": { + "stringValue": "toolu_013gThXdzSmWcztJH81MeH2p" + } + }, + { + "key": "duration_ms", + "value": { + "intValue": 2 + } + }, + { + "key": "success", + "value": { + "boolValue": true + } + } + ], + "droppedAttributesCount": 0, + "events": [], + "droppedEventsCount": 0, + "status": { + "code": 0 + }, + "links": [], + "droppedLinksCount": 0, + "flags": 257 + }, + { + "traceId": "2538c9231567456f0885bd882b364b6e", + "spanId": "8945dbe8f891669c", + "parentSpanId": "3865265b336909b9", + "name": "claude_code.llm_request", + "kind": 1, + "startTimeUnixNano": "1790903455898000000", + "endTimeUnixNano": "1790903458566793167", + "attributes": [ + { + "key": "user.id", + "value": { + "stringValue": "0000000000000000000000000000000000000000000000000000000000000000" + } + }, + { + "key": "session.id", + "value": { + "stringValue": "3047ee2f-3fe8-4ed3-a99d-8f79b68dde4d" + } + }, + { + "key": "span.type", + "value": { + "stringValue": "llm_request" + } + }, + { + "key": "model", + "value": { + "stringValue": "claude-sonnet-5-5" + } + }, + { + "key": "gen_ai.system", + "value": { + "stringValue": "anthropic" + } + }, + { + "key": "gen_ai.request.model", + "value": { + "stringValue": "claude-sonnet-5-5" + } + }, + { + "key": "llm_request.context", + "value": { + "stringValue": "interaction" + } + }, + { + "key": "speed", + "value": { + "stringValue": "normal" + } + }, + { + "key": "query_source_safe", + "value": { + "stringValue": "sdk" + } + }, + { + "key": "duration_ms", + "value": { + "intValue": 2669 + } + }, + { + "key": "input_tokens", + "value": { + "intValue": 2 + } + }, + { + "key": "output_tokens", + "value": { + "intValue": 180 + } + }, + { + "key": "cache_read_tokens", + "value": { + "intValue": 64465 + } + }, + { + "key": "cache_creation_tokens", + "value": { + "intValue": 1298 + } + }, + { + "key": "success", + "value": { + "boolValue": true + } + }, + { + "key": "attempt", + "value": { + "intValue": 1 + } + }, + { + "key": "ttft_ms", + "value": { + "intValue": 2657 + } + }, + { + "key": "first_content_ms", + "value": { + "intValue": 2657 + } + }, + { + "key": "effort", + "value": { + "stringValue": "medium" + } + }, + { + "key": "stop_reason", + "value": { + "stringValue": "end_turn" + } + }, + { + "key": "gen_ai.response.finish_reasons", + "value": { + "arrayValue": { + "values": [ + { + "stringValue": "end_turn" + } + ] + } + } + } + ], + "droppedAttributesCount": 0, + "events": [ + { + "attributes": [ + { + "key": "attempt", + "value": { + "intValue": 1 + } + } + ], + "name": "gen_ai.request.attempt", + "timeUnixNano": "1790903455898714250", + "droppedAttributesCount": 0 + } + ], + "droppedEventsCount": 0, + "status": { + "code": 0 + }, + "links": [], + "droppedLinksCount": 0, + "flags": 257 + } + ] + } + ] + } + ] +} diff --git a/tests/test_litellm/tracing/test_decode.py b/tests/test_litellm/tracing/test_decode.py index 21a79dd6b87..b928cb3166c 100644 --- a/tests/test_litellm/tracing/test_decode.py +++ b/tests/test_litellm/tracing/test_decode.py @@ -55,13 +55,94 @@ def _kv(key: str, value: str | int) -> KeyValue: return KeyValue(key=key, value=AnyValue(string_value=value)) -def _export(*spans: Span, service: str = "svc", scope: str = "test") -> bytes: +def _export(*spans: Span, service: str = "svc", scope: str = "test", agent_name: str = "") -> bytes: resource_spans = ResourceSpans(scope_spans=[ScopeSpans(spans=list(spans))]) resource_spans.resource.attributes.append(_kv("service.name", service)) + if agent_name: + resource_spans.resource.attributes.append(_kv("gen_ai.agent.name", agent_name)) resource_spans.scope_spans[0].scope.name = scope return ExportTraceServiceRequest(resource_spans=[resource_spans]).SerializeToString() +@pytest.mark.parametrize( + ("name", "attributes"), + [ + ("research_agent", {"openinference.span.kind": "AGENT", "metadata": '{"lc_agent_name":"research_agent"}'}), + ("research_agent", {"openinference.span.kind": "AGENT", "metadata": '{"ls_integration":"langgraph"}'}), + ("research_agent._execute_core", {"openinference.span.kind": "AGENT", "graph.node.id": "research_agent"}), + ("agent", {"openinference.span.kind": "AGENT", "gen_ai.agent.name": "research_agent"}), + ("openclaw.harness.run", {"openclaw.agent": "research_agent"}), + ( + "invoke_agent research_agent", + {"gen_ai.operation.name": "invoke_agent", "gen_ai.agent.name": "research_agent"}, + ), + ], + ids=["deepagents", "langgraph", "crewai", "hermes", "openclaw", "genai"], +) +def test_framework_agent_identity_is_independent_of_service(name: str, attributes: dict[str, str]): + span = _span(name, b"\x02" * 8, **attributes) + row = decode_otlp(_export(span, service="shared-deployment"), "application/x-protobuf")[0] + assert row["AgentName"] == "research_agent" + assert row["ServiceName"] == "shared-deployment" + assert row["SpanName"] == name + + +@pytest.mark.parametrize("name", ["ClaudeAgentSDK.query", "FunctionAgent.run"]) +def test_resource_agent_name_labels_instrumentors_without_an_agent_attribute(name: str): + span = _span(name, b"\x02" * 8, openinference__span__kind="AGENT") + row = decode_otlp(_export(span, agent_name="research_agent"), "application/x-protobuf")[0] + assert row["AgentName"] == "research_agent" + + +def test_span_agent_name_takes_precedence_over_resource_default(): + span = _span("invoke_agent child", b"\x02" * 8, gen_ai__agent__name="child") + row = decode_otlp(_export(span, agent_name="research_agent"), "application/x-protobuf")[0] + assert row["AgentName"] == "child" + + +@pytest.mark.parametrize( + ("scope", "span_name", "configured_name", "expected"), + [ + ("hermes-otel-plugin", "hermes-agent", "research_agent", "research_agent"), + ("hermes-otel-plugin", "child", "research_agent", "child"), + ("hermes-otel-plugin", "hermes-agent", "", "hermes-agent"), + ("other-plugin", "hermes-agent", "research_agent", "hermes-agent"), + ], +) +def test_hermes_resource_name_replaces_only_its_plugin_default( + scope: str, span_name: str, configured_name: str, expected: str +): + span = _span("agent", b"\x02" * 8, gen_ai__agent__name=span_name) + row = decode_otlp(_export(span, scope=scope, agent_name=configured_name), "application/x-protobuf")[0] + assert row["AgentName"] == expected + + +@pytest.mark.parametrize("agent_name", ["research_agent", ""]) +def test_openinference_middleware_is_not_a_separate_agent(agent_name: str): + span = _span( + "PatchToolCallsMiddleware.before_agent", b"\x02" * 8, b"\x01" * 8, + openinference__span__kind="AGENT", metadata=json.dumps({"lc_agent_name": agent_name}), + ) + row = decode_otlp(_export(span, scope="openinference.instrumentation.langchain"), "application/x-protobuf")[0] + assert (row["ObservationType"], row["AgentName"]) == ("framework", agent_name) + + +@pytest.mark.parametrize("scope", ["test", "openinference.instrumentation.langchain"]) +@pytest.mark.parametrize("kind", ["CHAIN", "AGENT"]) +@pytest.mark.parametrize("metadata", ["not json", "[]", '{"lc_agent_name":null}', "{}"]) +def test_unnamed_framework_does_not_invent_an_agent_from_service(metadata: str, scope: str, kind: str): + span = _span("workflow", b"\x02" * 8, openinference__span__kind=kind, metadata=metadata) + row = decode_otlp(_export(span, scope=scope), "application/x-protobuf")[0] + assert row["AgentName"] == "" + + +@pytest.mark.parametrize("name,expected", [("support", "support"), ("LangGraph", "")]) +def test_langgraph_distinguishes_configured_graph_name_from_default(name: str, expected: str): + span = _span(name, b"\x02" * 8, openinference__span__kind="CHAIN", metadata='{"ls_integration":"langgraph"}') + row = decode_otlp(_export(span, scope="openinference.instrumentation.langchain"), "application/x-protobuf")[0] + assert row["AgentName"] == expected + + def _span(name: str, span_id: bytes, parent: bytes = b"", **attributes: str | int) -> Span: return Span( trace_id=bytes.fromhex(TRACE_ID), @@ -498,3 +579,17 @@ def test_token_counts_outside_storage_range_are_rejected(count): exported = _span("root", b"\x01" * 8, gen_ai__usage__input_tokens=count) with pytest.raises(decode.InvalidOTLPPayloadError, match="storage range"): decode_otlp(_export(exported)) + + +def test_claude_agent_sdk_rows_carry_framework_tool_names_and_arguments(): + fixture = Path(__file__).parent / "fixtures" / "claude_agent_sdk_detailed_export.json" + rows = decode_otlp(fixture.read_bytes(), "application/json") + sdk_llms = [r for r in rows if r["ObservationType"] == "llm" and r["SpanAttributes"]["query_source_safe"] == "sdk"] + assert sdk_llms and {r["Framework"] for r in sdk_llms} == {"claude-agent-sdk"} + tools = {r["SpanName"]: r for r in rows if r["ObservationType"] == "tool"} + assert set(tools) == {"Bash", "Read"} + assert json.loads(tools["Bash"]["Input"])["command"] == tools["Bash"]["SpanAttributes"]["full_command"] + assert "tool_input" not in tools["Bash"]["SpanAttributes"] + root = next(r for r in rows if r["ObservationType"] == "agent") + assert "user_prompt" not in root["SpanAttributes"] + assert json.loads(root["Input"])[0]["role"] == "user" diff --git a/tests/test_litellm/tracing/test_receiver.py b/tests/test_litellm/tracing/test_receiver.py index 0d9aa8d034d..36a10ea1e42 100644 --- a/tests/test_litellm/tracing/test_receiver.py +++ b/tests/test_litellm/tracing/test_receiver.py @@ -20,7 +20,7 @@ from litellm.tracing.types import TraceScope pytestmark = pytest.mark.requires_rust_extension FIXTURE = Path(__file__).parent / "fixtures" / "langsmith_deep_agent_export.json" -TENANT = Tenant(team_id="team-research", api_key_hash="hashed-key", org_id="org-1") +TENANT = Tenant(team_id="team-research", api_key_hash="hashed-key", org_id="org-1", user_id="user-1") def _fake_store() -> MagicMock: @@ -38,6 +38,7 @@ def _spoofed_export() -> bytes: KeyValue(key="service.name", value=AnyValue(string_value="svc")), KeyValue(key="litellm.team_id", value=AnyValue(string_value="someone-elses-team")), KeyValue(key="litellm.api_key_hash", value=AnyValue(string_value="someone-elses-key")), + KeyValue(key="litellm.user_id", value=AnyValue(string_value="someone-elses-user")), ] ) return ExportTraceServiceRequest(resource_spans=[resource_spans]).SerializeToString() @@ -64,6 +65,8 @@ async def test_ingest_overwrites_client_supplied_tenant_attributes(): assert row["TeamId"] == "team-research" assert row["ResourceAttributes"]["litellm.team_id"] == "team-research" assert row["ResourceAttributes"]["litellm.api_key_hash"] == "hashed-key" + assert row["UserId"] == TENANT.user_id + assert row["ResourceAttributes"]["litellm.user_id"] == TENANT.user_id @pytest.mark.asyncio @@ -103,7 +106,7 @@ async def test_empty_export_writes_nothing(): async def test_reads_delegate_to_store(): store = _fake_store() tracing = TraceReceiver(store) - scope: TraceScope = {"team_ids": ("team-research",), "api_key_hash": ""} + scope: Final[TraceScope] = {"all_teams": 0, "user_id": "", "team_ids": ("team-research",)} assert await tracing.get_trace("t1", scope) is None store.get_trace.assert_awaited_once_with("t1", scope, "") diff --git a/tests/test_litellm/tracing/test_store.py b/tests/test_litellm/tracing/test_store.py index 3f43e42842c..18a5865db7f 100644 --- a/tests/test_litellm/tracing/test_store.py +++ b/tests/test_litellm/tracing/test_store.py @@ -2,11 +2,19 @@ Tests for the pure read-side helpers in litellm/tracing/store.py (no ClickHouse needed). """ -from typing import Any +from typing import Any, Final from unittest.mock import AsyncMock, MagicMock import pytest +from litellm.rust_bridge.trace_queries import ( + LIST_TRACES, + TRACE_SPANS, + SPAN_ERROR, + SpanErrorParams, + SpanErrorRow, + SpendRow, +) from litellm.tracing.store import ( TraceStore, agent_nodes, @@ -48,6 +56,11 @@ def _row( "input_tokens": 0, "output_tokens": 0, "litellm_request_id": "", + "team_id": "", + "api_key_hash": "", + "user_id": "", + "status_message": "", + "error_truncated": False, **extra, } @@ -221,6 +234,47 @@ def test_agent_nodes_ignores_spans_of_unknown_agents(): assert agent_nodes(spans) == () +def test_trace_groups_normalized_names_and_preserves_span_labels(): + rows = [ + _row("root", "", "invoke_agent research_agent", "agent", "research_agent"), + _row("r1", "root", "researcher._execute_core", "agent", "researcher"), + _row("r2", "r1", "invoke_agent researcher", "agent", "researcher"), + _row("llm", "r2", "chat", "llm", "researcher"), + ] + result = trace_from_rows("t1", rows) + assert result is not None + assert result["summary"]["agent_names"] == ("research_agent", "researcher") + assert result["summary"]["name"] == "invoke_agent research_agent" + agents = {agent["name"]: agent for agent in result["agents"]} + assert agents["researcher"]["parent_agent"] == "research_agent" + assert agents["researcher"]["invocations"] == 2 + assert agents["researcher"]["llm_calls"] == 1 + + +def test_trace_frameworks_are_the_sorted_distinct_span_frameworks(): + rows = [ + _row("root", "", "claude_code.interaction", "agent", "claude-code", framework="claude-code"), + _llm_row("llm", "root", "claude-code", "msg_1", framework="claude-agent-sdk"), + _row("tool", "root", "Bash", "tool", "claude-code", framework="claude-code"), + _row("other", "root", "step", "chain", "claude-code", framework=""), + ] + validated_rows: Final = TRACE_SPANS.response.validate_python({"data": rows}).data + trace = trace_from_rows("t1", validated_rows) + assert trace is not None + assert trace["summary"]["frameworks"] == ("claude-agent-sdk", "claude-code") + spans = {span["span_id"]: span for span in trace["spans"]} + assert (spans["llm"]["framework"], spans["other"]["framework"]) == ("claude-agent-sdk", "") + assert trace["agents"][0]["llm_calls"] == 1 + assert trace["agents"][0]["tool_calls"] == 1 + + +def test_spans_without_a_framework_column_report_none(): + trace = trace_from_rows("t1", _deep_agent_rows()) + assert trace is not None + assert trace["summary"]["frameworks"] == () + assert {span["framework"] for span in trace["spans"]} == {""} + + # ---------------------------------------------------------------- list helpers @@ -238,25 +292,40 @@ def test_invalid_cursor_is_rejected(cursor): def test_trace_summary_from_row(): - summary = trace_summary_from_row( + rows: Final = LIST_TRACES.response.validate_python( { - "trace_id": "t1", - "name": "deep_research_agent", - "service": "agent-demo", - "input_preview": "hi", - "start_ms": 1790742989377, - "duration_ms": 51385, - "status": "STATUS_CODE_OK", - "span_count": "126", - "agent_count": "2", - "llm_calls": "7", - "tool_calls": "26", - "error_count": "1", - "input_tokens": "30175", - "output_tokens": "2620", - "models": ["claude-sonnet-4-5"], + "data": [ + { + "trace_id": "t1", + "trace_ref": "ref", + "team_id": "team", + "api_key_hash": "key", + "user_id": "owner", + "agent_invocations": 2, + "agent_names": ["deep_research_agent"], + "request_ids": [], + "name": "deep_research_agent", + "service": "agent-demo", + "input_preview": "hi", + "start_ms": 1790742989377, + "duration_ms": 51385, + "status": "STATUS_CODE_OK", + "span_count": "126", + "agent_count": "2", + "llm_calls": "7", + "tool_calls": "26", + "error_count": "1", + "input_tokens": "30175", + "output_tokens": "2620", + "models": ["claude-sonnet-4-5"], + "frameworks": ["claude-agent-sdk", "claude-code"], + } + ] } - ) + ).data + summary: Final = trace_summary_from_row(rows[0]) + assert summary["agent_names"] == ("deep_research_agent",) + assert summary["frameworks"] == ("claude-agent-sdk", "claude-code") assert summary["status"] == "ok" assert (summary["span_count"], summary["error_count"]) == (126, 1) assert summary["start_time"] == "2026-09-30T04:36:29.377000+00:00" @@ -285,18 +354,18 @@ async def test_list_traces_sets_next_cursor_on_full_page(): } client.query = AsyncMock(return_value=[row, {**row, "trace_id": "t1", "trace_ref": "ref1", "start_ms": 900}]) store = TraceStore(client) - scope: TraceScope = {"team_ids": ("team-a",), "api_key_hash": ""} + scope: Final[TraceScope] = {"all_teams": 0, "user_id": "", "team_ids": ("team-a",)} page = await store.list_traces(scope, 0, 2000, limit=2) assert [t["trace_id"] for t in page["data"]] == ["t2", "t1"] assert page["next_cursor"] is not None assert decode_cursor(page["next_cursor"]) == (900, "ref1") params = client.query.call_args.args[1] - assert params["team_ids"] == ("team-a",) and params["limit"] == 2 and params["cursor_ms"] == 0 + assert params.team_ids == ("team-a",) and params.limit == 2 and params.cursor_ms == 0 page = await store.list_traces(scope, 0, 2000, cursor=page["next_cursor"], limit=3) assert page["next_cursor"] is None - assert client.query.call_args.args[1]["cursor_trace_id"] == "ref1" + assert client.query.call_args.args[1].cursor_trace_id == "ref1" @pytest.mark.asyncio @@ -304,13 +373,13 @@ async def test_get_span_not_found_and_found(): client = MagicMock() client.query = AsyncMock(return_value=[]) store = TraceStore(client) - scope: TraceScope = {"team_ids": (), "api_key_hash": ""} - assert await store.get_span("t", "s", scope) is None + scope: Final[TraceScope] = {"all_teams": 1, "user_id": "", "team_ids": ()} + assert await store.get_span("t", "s", scope, "ref") is None stored_input = '[{"role": "user", "content": "hi"}]' client.query = AsyncMock( return_value=[{"span_id": "s", "input": stored_input, "output": '{"ok": true}', "attributes": {"k": "v"}}] ) - assert await store.get_span("t", "s", scope) == { + assert await store.get_span("t", "s", scope, "ref") == { "span_id": "s", "input": stored_input, "output": '{"ok": true}', @@ -347,24 +416,24 @@ async def test_trace_cost_is_scoped_and_counts_repeated_request_once(): }, { "request_id": "request-other-key", - "response_id": "response-1", + "response_id": "unrelated-response", "team_id": "team-a", "api_key": "key-c", "spend": 50.0, "start_ms": T0 // MS, }, ] - client.query = AsyncMock(side_effect=[spans, spend]) + client.query = AsyncMock(side_effect=[spans, tuple(SpendRow.model_validate({**row, "user": ""}) for row in spend)]) store = TraceStore(client) - scope: TraceScope = {"team_ids": ("team-a",), "api_key_hash": ""} + scope: Final[TraceScope] = {"all_teams": 0, "user_id": "", "team_ids": ("team-a",)} - trace = await store.get_trace("trace-1", scope) + trace = await store.get_trace("trace-1", scope, "ref") assert trace is not None assert trace["summary"]["spend"] == 0.25 assert trace["agents"][0]["spend"] == 0.25 assert [span["spend"] for span in trace["spans"]] == [None, 0.25, 0.25] - assert [call.args[0] for call in client.query.await_args_list] == ["trace_spans", "spend_by_response_ids"] + assert [call.args[0].name for call in client.query.await_args_list] == ["trace_spans", "spend_by_response_ids"] @pytest.mark.asyncio @@ -403,19 +472,19 @@ async def test_run_list_uses_matching_spend_and_leaves_missing_cost_unavailable( "start_ms": 1000, } ] - client.query = AsyncMock(side_effect=[rows, spend]) - scope: TraceScope = {"team_ids": ("team-a",), "api_key_hash": ""} + client.query = AsyncMock(side_effect=[rows, tuple(SpendRow.model_validate({**row, "user": ""}) for row in spend)]) + scope: Final[TraceScope] = {"all_teams": 0, "user_id": "", "team_ids": ("team-a",)} page = await TraceStore(client).list_traces(scope, 0, 2000) assert [run["spend"] for run in page["data"]] == [0.25, None] - assert [call.args[0] for call in client.query.await_args_list] == ["list_traces", "spend_by_response_ids"] + assert [call.args[0].name for call in client.query.await_args_list] == ["list_traces", "spend_by_response_ids"] @pytest.mark.asyncio async def test_ambiguous_cache_response_id_keeps_cost_unavailable(): client = MagicMock() - span = _llm_row("llm-1", "", "agent", "response-1", team_id="", api_key_hash="key-a") + span: Final = _llm_row("llm-1", "", "agent", "response-1", team_id="", user_id="user", api_key_hash="key-a") spend = [ { "request_id": request_id, @@ -427,11 +496,13 @@ async def test_ambiguous_cache_response_id_keeps_cost_unavailable(): } for request_id, cost in (("response-1", 0.25), ("response-1_cache_hit123", 0.0)) ] - client.query = AsyncMock(side_effect=[[span], spend]) + client.query = AsyncMock( + side_effect=[[span], tuple(SpendRow.model_validate({**row, "user": "user"}) for row in spend)] + ) store = TraceStore(client) - scope: TraceScope = {"team_ids": ("",), "api_key_hash": "key-a"} + scope: Final[TraceScope] = {"all_teams": 0, "user_id": "user", "team_ids": ()} - trace = await store.get_trace("trace-1", scope) + trace = await store.get_trace("trace-1", scope, "ref") assert trace is not None assert trace["summary"]["spend"] is None @@ -447,12 +518,12 @@ async def test_diagnostic_continuation_preserves_content_version_scope_and_unico client = MagicMock() client.query = AsyncMock( side_effect=[ - [{"span_id": "span-1", "message": "first 🧪", "total_chars": len(message), "version": version}], - [{"span_id": "span-1", "message": "\nlast", "total_chars": len(message), "version": version}], + [SpanErrorRow(span_id="span-1", message="first 🧪", total_chars=len(message), version=version)], + [SpanErrorRow(span_id="span-1", message="\nlast", total_chars=len(message), version=version)], ] ) store = TraceStore(client) - scope = {"team_ids": ("team-a",), "api_key_hash": "key-a"} + scope: Final[TraceScope] = {"all_teams": 0, "user_id": "", "team_ids": ("team-a",)} first = await store.get_span_error("trace-1", "span-1", scope, "scoped-run") assert first is not None and first["next_cursor"] is not None last = await store.get_span_error("trace-1", "span-1", scope, "scoped-run", first["next_cursor"]) @@ -460,15 +531,15 @@ async def test_diagnostic_continuation_preserves_content_version_scope_and_unico assert first["message"] + last["message"] == message assert last["next_cursor"] is None client.query.assert_awaited_with( - "span_error", - { + SPAN_ERROR, + SpanErrorParams( **scope, - "trace_id": "trace-1", - "span_id": "span-1", - "trace_ref": "scoped-run", - "error_offset": len(first["message"]), - "error_version": version, - }, + trace_id="trace-1", + span_id="span-1", + trace_ref="scoped-run", + error_offset=len(first["message"]), + error_version=version, + ), ) @@ -478,5 +549,123 @@ async def test_malformed_diagnostic_cursor_never_reaches_storage(cursor): client = MagicMock() client.query = AsyncMock() with pytest.raises(ValueError, match="Invalid diagnostic cursor"): - await TraceStore(client).get_span_error("trace", "span", {"team_ids": (), "api_key_hash": ""}, cursor=cursor) + await TraceStore(client).get_span_error( + "trace", "span", {"all_teams": 1, "user_id": "", "team_ids": ()}, cursor=cursor + ) client.query.assert_not_awaited() + + +@pytest.mark.parametrize( + ("trace_team", "trace_user", "trace_key", "spend_team", "spend_user", "spend_key", "known"), + ( + ("team", "", "export", "team", "", "request", False), + ("team", "", "export", "team", "", "export", True), + ("", "user", "export", "", "user", "request", True), + ("", "", "key", "", "", "key", True), + ("team", "user", "key", "other-team", "user", "key", False), + ("", "user", "export", "", "other-user", "request", False), + ("", "", "export", "", "", "request", False), + ("", "", "", "", "", "", False), + ("", "", "master", "", "", "", False), + ), +) +def test_cost_attribution_requires_shared_ownership_after_visibility( + trace_team: str, + trace_user: str, + trace_key: str, + spend_team: str, + spend_user: str, + spend_key: str, + known: bool, +) -> None: + rows: Final = ( + _row("agent", "", "agent", "agent", "agent", team_id=trace_team, user_id=trace_user, api_key_hash=trace_key), + _llm_row("llm", "agent", "agent", "response", team_id=trace_team, user_id=trace_user, api_key_hash=trace_key), + ) + spend: Final = SpendRow( + request_id="request", + response_id="response", + team_id=spend_team, + user=spend_user, + api_key=spend_key, + spend=0.25, + start_ms=T0 // MS, + ) + trace: Final = trace_from_rows("trace", rows, "visible-reference", (spend,)) + assert trace is not None + expected: Final = spend.spend if known else None + assert trace["summary"]["spend"] == expected + assert trace["agents"][0]["spend"] == expected + assert trace["spans"][1]["spend"] == expected + summary: Final = trace_summary_from_row( + { + "trace_id": "trace", + "team_id": trace_team, + "user_id": trace_user, + "api_key_hash": trace_key, + "request_ids": ("response",), + "name": "agent", + "service": "service", + "input_preview": "", + "start_ms": T0 // MS, + "duration_ms": 10, + "status": "STATUS_CODE_OK", + "span_count": 2, + "agent_count": 1, + "llm_calls": 1, + "tool_calls": 0, + "input_tokens": 0, + "output_tokens": 0, + "models": (), + }, + (spend,), + ) + assert summary["spend"] == expected + + +@pytest.mark.parametrize("failure", ("missing_id", "missing_spend", "duplicate_spend")) +def test_incomplete_llm_cost_never_becomes_a_partial_trace_or_agent_total(failure: str) -> None: + second_id: Final = "" if failure == "missing_id" else "second" + rows: Final = ( + _row("agent", "", "agent", "agent", "agent", team_id="team", api_key_hash="export"), + _llm_row("first", "agent", "agent", "first", team_id="team", api_key_hash="export"), + _llm_row("second", "agent", "agent", second_id, team_id="team", api_key_hash="export"), + ) + first: Final = SpendRow( + request_id="first", + response_id="first", + team_id="team", + user="", + api_key="export", + spend=0.25, + start_ms=0, + ) + second: Final = first.model_copy(update={"request_id": "second", "response_id": "second"}) + spend: Final = ( + (first, second, second.model_copy(update={"request_id": "duplicate"})) + if failure == "duplicate_spend" + else (first,) + ) + trace: Final = trace_from_rows("trace", rows, "ref", spend) + assert trace is not None + assert trace["spans"][1]["spend"] == first.spend + assert trace["spans"][2]["spend"] is None + assert trace["summary"]["spend"] is None + assert trace["agents"][0]["spend"] is None + + +@pytest.mark.asyncio +async def test_trace_id_collision_requires_a_visible_reference_before_reading_content() -> None: + from litellm.rust_bridge.trace_queries import TraceIdentityRow + from litellm.tracing.store import AmbiguousTraceError + + storage: Final = MagicMock() + storage.query = AsyncMock(return_value=(TraceIdentityRow(trace_ref="first"), TraceIdentityRow(trace_ref="second"))) + store: Final = TraceStore(storage) + scope: Final[TraceScope] = {"all_teams": 1, "user_id": "", "team_ids": ()} + with pytest.raises(AmbiguousTraceError, match="provide trace_ref"): + await store.get_trace("shared-id", scope) + with pytest.raises(AmbiguousTraceError, match="provide trace_ref"): + await store.get_span("shared-id", "span", scope) + with pytest.raises(AmbiguousTraceError, match="provide trace_ref"): + await store.get_span_error("shared-id", "span", scope) diff --git a/tests/test_litellm_rust/test_traces.py b/tests/test_litellm_rust/test_traces.py index efc0a44f311..e0208312f18 100644 --- a/tests/test_litellm_rust/test_traces.py +++ b/tests/test_litellm_rust/test_traces.py @@ -7,8 +7,15 @@ from typing import Final from urllib.parse import parse_qs, urlsplit import pytest +from pydantic import JsonValue from litellm.rust_bridge._native import NativeTraceConfig, NativeTraceStorage, trace_decode_otlp +from litellm.rust_bridge.trace_queries import ( + TRACE_SPANS, + ActivityAvailability, + LensAccessParams, + TraceSpansParams, +) from litellm.rust_bridge.traces import ( ClickHouseStorage, NormalizedSpan, @@ -28,15 +35,48 @@ def _native_storage(database: str, url: str, retention_days: int = 14) -> Native return NativeTraceStorage(NativeTraceConfig(database, url, retention_days)) +@pytest.fixture +def span_row() -> dict[str, JsonValue]: + return { + "span_id": "span-1", + "parent_span_id": "", + "name": "root", + "type": "agent", + "agent": "", + "framework": "", + "status": "STATUS_CODE_OK", + "status_message": "", + "error_truncated": 0, + "start_ns": "1000000000", + "duration_ns": "1000", + "service": "test", + "input_preview": "hello", + "model": "", + "input_tokens": 0, + "output_tokens": 0, + "litellm_request_id": "", + "team_id": "", + "api_key_hash": "", + "user_id": "", + } + + +@pytest.fixture +def span_params() -> dict[str, str | int | list[str]]: + return {"trace_id": "trace-1", "trace_ref": "", "all_teams": 1, "user_id": "", "team_ids": []} + + @pytest.mark.asyncio -async def test_trace_reader_projects_connection_and_parameters(recording_server: RecordingServer) -> None: - recording_server.enqueue(ResponseSpec(body={"data": [{"trace_id": "trace-1"}]})) +async def test_trace_reader_projects_connection_and_parameters( + recording_server: RecordingServer, span_row: dict[str, JsonValue], span_params: dict[str, str | int | list[str]] +) -> None: + recording_server.enqueue(ResponseSpec(body={"data": [span_row]})) url: Final = recording_server.base_url.replace("http://", "http://reader:p%40ss%2Fword%25@") storage: Final = _native_storage("trace_test", url + "?database=wrong") - rows: Final = json.loads(await storage.query("trace_spans", {"trace_id": "trace-1"})) + rows: Final = json.loads(await storage.query("trace_spans", span_params)) request: Final = recording_server.requests[0] parameters: Final = parse_qs(urlsplit(request.path).query) - assert rows == {"data": [{"trace_id": "trace-1"}]} + assert rows == {"data": [span_row]} assert b"o.TraceId = {trace_id:String}" in request.raw_body assert parameters["database"] == ["trace_test"] assert parameters["param_trace_id"] == ["trace-1"] @@ -47,11 +87,13 @@ async def test_trace_reader_projects_connection_and_parameters(recording_server: @pytest.mark.asyncio -async def test_trace_reader_rejects_success_status_with_embedded_error(recording_server: RecordingServer) -> None: +async def test_trace_reader_rejects_success_status_with_embedded_error( + recording_server: RecordingServer, span_params: dict[str, str | int | list[str]] +) -> None: recording_server.enqueue(ResponseSpec(body={"data": [], "exception": "query failed"})) storage: Final = _native_storage("trace_test", recording_server.base_url) with pytest.raises(RuntimeError, match="invalid or failed JSON"): - await storage.query("trace_spans", {}) + await storage.query("trace_spans", span_params) @pytest.mark.asyncio @@ -87,7 +129,7 @@ async def test_from_env_reads_with_clickhouse_url( recording_server.enqueue(ResponseSpec(body={"data": []})) monkeypatch.setenv("CLICKHOUSE_URL", recording_server.base_url) monkeypatch.delenv("CLICKHOUSE_READER_URL", raising=False) - scope: Final[TraceScope] = {"team_ids": (), "api_key_hash": ""} + scope: Final[TraceScope] = {"all_teams": 1, "user_id": "", "team_ids": ()} page: Final = await TraceReceiver.from_env().list_traces(scope, 0, 1) assert page == {"data": (), "next_cursor": None} assert len(recording_server.requests) == 1 @@ -95,7 +137,7 @@ async def test_from_env_reads_with_clickhouse_url( @pytest.mark.asyncio async def test_schema_setup_uses_configured_retention(recording_server: RecordingServer) -> None: - recording_server.expected_requests = 8 + recording_server.expected_requests = None storage: Final = _native_storage("trace_test", recording_server.base_url, 7) await storage.ensure_schema() ttl_statements: Final = tuple( @@ -192,6 +234,7 @@ def test_decode_and_tenant_stamping_share_resources_without_crossing_groups() -> "litellm.team_id": "team-a", "litellm.api_key_hash": "key-a", "litellm.org_id": "org-a", + "litellm.user_id": "", } assert second[0]["ResourceAttributes"]["litellm.team_id"] == "team-b" assert rows[0]["ResourceAttributes"] == {"shared": "x" * 128, "litellm.team_id": "spoofed"} @@ -253,31 +296,54 @@ async def test_insert_validates_values_without_pydantic_copy(recording_server: R assert stored["SpanAttributes"] == attributes -@pytest.mark.parametrize("role", ["proxy_admin", "proxy_admin_viewer", "internal_user"]) -def test_trace_sql_endpoint_executes_for_admin_and_preserves_clickhouse_envelope( - recording_server: RecordingServer, role: str +@pytest.mark.parametrize( + ("role", "user_id", "expected_status"), + ( + ("proxy_admin", None, 200), + ("proxy_admin_viewer", None, 200), + ("internal_user", "user", 200), + ("internal_user", None, 403), + ), +) +def test_trace_sql_endpoint_enforces_ownership_and_preserves_clickhouse_envelope( + recording_server: RecordingServer, role: str, user_id: str | None, expected_status: int ) -> None: from fastapi import FastAPI from fastapi.testclient import TestClient from litellm.proxy._types import UserAPIKeyAuth + from litellm.proxy.auth.authorization_dependencies import get_log_team_lookup from litellm.proxy.auth.user_api_key_auth import user_api_key_auth from litellm.proxy.tracing_endpoints import provide_receiver, provide_trace_query_secret, router - envelope: Final = {"meta": [{"name": "answer", "type": "UInt8"}], "data": [{"answer": 42}], "rows": 1} - recording_server.expected_requests = 12 - for _ in range(11): - recording_server.enqueue(ResponseSpec(body="")) - recording_server.enqueue(ResponseSpec(body=envelope)) + envelope: Final = { + "meta": [{"name": "answer", "type": "UInt8"}], + "data": [{"answer": 42}], + "rows": 1, + "statistics": {"elapsed": 0.01, "rows_read": 1, "bytes_read": 1}, + } + recording_server.expected_requests = 12 if expected_status == 200 else 0 + if expected_status == 200: + for _ in range(11): + recording_server.enqueue(ResponseSpec(body="")) + recording_server.enqueue(ResponseSpec(body=envelope)) storage: Final = ClickHouseStorage(TraceStorageConfig(recording_server.base_url, "trace_test")) app: Final = FastAPI() app.include_router(router) app.dependency_overrides[provide_trace_query_secret] = lambda: "test-master-secret" - app.dependency_overrides[user_api_key_auth] = lambda: UserAPIKeyAuth(user_role=role, token="test") + app.dependency_overrides[user_api_key_auth] = lambda: UserAPIKeyAuth(user_role=role, user_id=user_id, token="test") app.dependency_overrides[provide_receiver] = lambda: TraceReceiver(TraceStore(storage)) + + async def permitted_teams(auth: UserAPIKeyAuth) -> tuple[str, ...]: + return () + + app.dependency_overrides[get_log_team_lookup] = lambda: permitted_teams with TestClient(app) as client: result: Final = client.post("/v1/traces/query", json={"sql": "SELECT 42 AS answer"}) - assert result.status_code == 200, result.text + assert result.status_code == expected_status, result.text + if expected_status == 403: + assert result.json() == {"detail": "Not allowed to view logs"} + return assert result.json() == envelope assert recording_server.requests[-1].raw_body == b"SELECT 42 AS answer" assert client.post("/v1/traces/query", json={"sql": " "}).status_code == 400 @@ -326,9 +392,18 @@ def test_trace_help_endpoint_runs_native_schema_and_metadata_discovery(recording assert body["attributes"][1]["fields"][0]["expression"] == "ResourceAttributes['custom.resource']" -@pytest.mark.parametrize("clickhouse_status, expected_status", [(400, 400), (404, 400), (500, 503), (503, 503)]) +@pytest.mark.parametrize( + ("clickhouse_status", "body", "expected_status"), + ( + (400, b"ClickHouse rejected the query", 400), + (404, b"ClickHouse rejected the query", 400), + (500, b"ClickHouse rejected the query", 503), + (503, b"ClickHouse rejected the query", 503), + (200, b'{"data":[]}', 503), + ), +) def test_trace_sql_endpoint_distinguishes_query_errors_from_reader_failures( - recording_server: RecordingServer, clickhouse_status: int, expected_status: int + recording_server: RecordingServer, clickhouse_status: int, body: bytes, expected_status: int ) -> None: from fastapi import FastAPI from fastapi.testclient import TestClient @@ -340,8 +415,13 @@ def test_trace_sql_endpoint_distinguishes_query_errors_from_reader_failures( recording_server.expected_requests = 13 for _ in range(11): recording_server.enqueue(ResponseSpec(body="")) - recording_server.enqueue(ResponseSpec(status=clickhouse_status, body=b"ClickHouse rejected the query")) - envelope: Final = {"meta": [{"name": "answer", "type": "UInt8"}], "data": [{"answer": 42}], "rows": 1} + recording_server.enqueue(ResponseSpec(status=clickhouse_status, body=body)) + envelope: Final = { + "meta": [{"name": "answer", "type": "UInt8"}], + "data": [{"answer": 42}], + "rows": 1, + "statistics": {"elapsed": 0.01, "rows_read": 1, "bytes_read": 1}, + } recording_server.enqueue(ResponseSpec(body=envelope)) storage: Final = ClickHouseStorage(TraceStorageConfig(recording_server.base_url, "trace_test")) app: Final = FastAPI() @@ -360,15 +440,39 @@ def test_trace_sql_endpoint_distinguishes_query_errors_from_reader_failures( @pytest.mark.asyncio async def test_trace_receiver_reads_with_only_one_clickhouse_url( - recording_server: RecordingServer, monkeypatch: pytest.MonkeyPatch + recording_server: RecordingServer, + monkeypatch: pytest.MonkeyPatch, + span_row: dict[str, JsonValue], + span_params: dict[str, str | int | list[str]], ) -> None: monkeypatch.setenv("CLICKHOUSE_URL", recording_server.base_url) monkeypatch.setenv("CLICKHOUSE_DATABASE", "trace_test") monkeypatch.delenv("CLICKHOUSE_READER_URL", raising=False) - recording_server.enqueue(ResponseSpec(body={"data": [{"trace_id": "trace-1"}]})) + recording_server.enqueue(ResponseSpec(body={"data": [span_row]})) receiver: Final = TraceReceiver.from_env() - rows: Final = await receiver.store.storage.query("trace_spans", {"trace_id": "trace-1"}) - assert rows == [{"trace_id": "trace-1"}] + rows: Final = await receiver.store.storage.query(TRACE_SPANS, TraceSpansParams.model_validate(span_params)) + assert rows == ( + { + **span_row, + "start_ns": int(str(span_row["start_ns"])), + "duration_ns": int(str(span_row["duration_ns"])), + "error_truncated": False, + }, + ) parameters: Final = parse_qs(urlsplit(recording_server.requests[0].path).query) assert parameters["database"] == ["trace_test"] assert parameters["readonly"] == ["1"] + + +@pytest.mark.asyncio +async def test_lens_read_uses_the_shared_native_query_and_returns_typed_rows( + recording_server: RecordingServer, +) -> None: + recording_server.enqueue(ResponseSpec(body={"data": [{"traces": 0, "requests": 1}]})) + storage: Final = ClickHouseStorage(TraceStorageConfig(recording_server.base_url, "trace_test")) + rows: Final = await storage.lens_availability(LensAccessParams(all_teams=0, team="team-a", key_hash="key-a")) + assert rows == (ActivityAvailability(traces=False, requests=True),) + parameters: Final = parse_qs(urlsplit(recording_server.requests[0].path).query) + assert parameters["param_all_teams"] == ["0"] + assert parameters["param_team"] == ["team-a"] + assert parameters["param_key_hash"] == ["key-a"] diff --git a/tests/unit/completion_extras/litellm_responses_transformation/test_completion_extras_litellm_responses_transformation_transformation.py b/tests/unit/completion_extras/litellm_responses_transformation/test_completion_extras_litellm_responses_transformation_transformation.py index 282b84104a6..60a6c96564c 100644 --- a/tests/unit/completion_extras/litellm_responses_transformation/test_completion_extras_litellm_responses_transformation_transformation.py +++ b/tests/unit/completion_extras/litellm_responses_transformation/test_completion_extras_litellm_responses_transformation_transformation.py @@ -3950,6 +3950,27 @@ def test_stored_reasoning_items_win_over_thinking_blocks(): assert reasoning_items[0]["id"] == "rs_real" +@pytest.mark.parametrize("missing_id", [None, ""]) +def test_a_stored_reasoning_item_without_an_id_is_replayed_without_inventing_one(missing_id): + """The Responses API rejects every id it did not mint, so no id beats a made-up one.""" + handler = LiteLLMResponsesTransformationHandler() + stored_item = {"type": "reasoning", "summary": [], "encrypted_content": "enc_abc"} + messages = [ + { + "role": "assistant", + "content": "Denver is sunny.", + "reasoning_items": [stored_item if missing_id is None else {**stored_item, "id": missing_id}], + }, + ] + + input_items, _ = handler.convert_chat_completion_messages_to_responses_api(messages) + + (reasoning_item,) = [item for item in input_items if item.get("type") == "reasoning"] + assert "id" not in reasoning_item + assert reasoning_item["encrypted_content"] == "enc_abc" + assert reasoning_item["summary"] == [] + + def test_convert_chat_completion_messages_to_responses_api_tool_result_with_tool_reference(): """Tool-search tool_reference blocks have no Responses API equivalent: skip them, never stringify them.""" from litellm.completion_extras.litellm_responses_transformation.transformation import ( diff --git a/tests/unit/enterprise/proxy/management_endpoints/test_project_endpoints_prisma.py b/tests/unit/enterprise/proxy/management_endpoints/test_project_endpoints_prisma.py index 36878fa698c..226755e7b8e 100644 --- a/tests/unit/enterprise/proxy/management_endpoints/test_project_endpoints_prisma.py +++ b/tests/unit/enterprise/proxy/management_endpoints/test_project_endpoints_prisma.py @@ -9,12 +9,10 @@ from fastapi import HTTPException, Request load_dotenv() import time -import logging import pytest import litellm -from litellm._logging import verbose_proxy_logger from litellm.proxy.management_endpoints.team_endpoints import ( new_team, ) @@ -30,7 +28,6 @@ from litellm.proxy.proxy_server import ( from litellm.proxy.common_utils.user_api_key_cache import UserApiKeyCache from litellm.proxy.utils import PrismaClient, ProxyLogging -verbose_proxy_logger.setLevel(level=logging.DEBUG) from litellm.caching.caching import DualCache diff --git a/tests/unit/experimental_mcp_client/test_mcp_client.py b/tests/unit/experimental_mcp_client/test_mcp_client.py index 508ab447326..015d12c3d5e 100644 --- a/tests/unit/experimental_mcp_client/test_mcp_client.py +++ b/tests/unit/experimental_mcp_client/test_mcp_client.py @@ -3028,9 +3028,101 @@ async def test_configured_upstream_revision_is_offered_and_checked(revision, acc await client.list_tools(raise_on_error=True) -@pytest.mark.parametrize("revision", ["2026-07-28", "unknown", "", None]) +@pytest.mark.parametrize("revision", ["unknown", "", None]) def test_upstream_protocol_configuration_rejects_unavailable_modes(revision): from pydantic import ValidationError with pytest.raises(ValidationError): MCPClient(protocol_version=revision) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("accepted", [True, False]) +async def test_modern_upstream_requests_are_self_contained_without_initialization(accepted: bool) -> None: + from queue import SimpleQueue + + from mcp.types import DiscoverResult, ToolsCapability + + methods: Final[SimpleQueue[str]] = SimpleQueue() + + def respond(request: httpx2.Request) -> httpx2.Response: + if request.method != "POST": + return httpx2.Response(405) + payload: Final = _JSONRPC_MESSAGE_ADAPTER.validate_json(request.content) + assert isinstance(payload, JSONRPCRequest) + methods.put(payload.method) + assert payload.method != "initialize", "Modern operations must not establish a legacy session" + assert "mcp-session-id" not in request.headers + assert request.headers["mcp-protocol-version"] == "2026-07-28" + assert request.headers["authorization"] == "Bearer upstream-credential" + assert payload.params is not None + metadata: Final = payload.params["_meta"] + assert metadata["io.modelcontextprotocol/protocolVersion"] == "2026-07-28" + assert "io.modelcontextprotocol/clientCapabilities" in metadata + if payload.method == "server/discover": + discovery: Final = DiscoverResult( + supported_versions=["2026-07-28"] if accepted else ["2025-11-25"], + capabilities=ServerCapabilities(tools=ToolsCapability()), + instructions="modern instructions", + ) + return httpx2.Response( + 200, + json={ + "jsonrpc": "2.0", + "id": payload.id, + "result": discovery.model_dump(by_alias=True, exclude_none=True), + }, + ) + assert accepted, "Rejected negotiation must prevent upstream execution" + if payload.method == "tools/list": + return httpx2.Response( + 200, + json={ + "jsonrpc": "2.0", + "id": payload.id, + "result": { + "resultType": "complete", + "cacheScope": "private", + "ttlMs": 0, + "tools": [{"name": "add", "inputSchema": {"type": "object"}}], + }, + }, + ) + assert payload.method == "tools/call" + assert payload.params["arguments"] == {"a": 2, "b": 3} + return httpx2.Response( + 200, + json={ + "jsonrpc": "2.0", + "id": payload.id, + "result": {"resultType": "complete", "content": [{"type": "text", "text": "5"}], "isError": False}, + }, + ) + + client: Final = _MockTransportClient( + respond, + server_url="https://example.com/mcp", + protocol_version="2026-07-28", + auth_type=MCPAuth.bearer_token, + auth_value="upstream-credential", + ) + params: Final = CallToolRequestParams(name="add", arguments={"a": 2, "b": 3}) + if accepted: + result: Final = await client.call_tool(params, raise_on_error=True) + assert result.content[0].text == "5" + assert not result.is_error + assert client._last_initialize_instructions == "modern instructions" + assert tuple(methods.get_nowait() for _ in range(methods.qsize())) == ( + "server/discover", + "tools/call", + "tools/list", + ) + else: + with pytest.raises((MCPError, RuntimeError), match="protocol version"): + await client.call_tool(params, raise_on_error=True) + assert tuple(methods.get_nowait() for _ in range(methods.qsize())) == ("server/discover",) + + +def test_modern_upstream_rejects_legacy_sse_transport() -> None: + with pytest.raises(ValueError, match="transport"): + MCPClient(protocol_version="2026-07-28", transport_type=MCPTransport.sse) diff --git a/tests/unit/integrations/otel/test_otel_v2_config_baggage_parenting_guardrails.py b/tests/unit/integrations/otel/test_otel_v2_config_baggage_parenting_guardrails.py index 86837f7f46c..20f8c89b5ff 100644 --- a/tests/unit/integrations/otel/test_otel_v2_config_baggage_parenting_guardrails.py +++ b/tests/unit/integrations/otel/test_otel_v2_config_baggage_parenting_guardrails.py @@ -12,6 +12,7 @@ import asyncio import logging +from typing import Final import pytest @@ -110,6 +111,61 @@ def test_excluded_services_from_env_csv(monkeypatch): assert OpenTelemetryV2Config().excluded_services == frozenset({"redis", "postgresql"}) +@pytest.mark.parametrize("name", ["EXCLUDED_SERVICES", "excluded_services", "Excluded_Services"]) +def test_a_bare_excluded_services_env_var_is_ignored(monkeypatch, name): + for env_name in ("LITELLM_OTEL_EXCLUDED_SERVICES", "EXCLUDED_SERVICES", "excluded_services", "Excluded_Services"): + monkeypatch.delenv(env_name, raising=False) + monkeypatch.setenv(name, "redis,postgres") + assert OpenTelemetryV2Config().excluded_services == frozenset() + + +@pytest.mark.parametrize( + ("set_env_name", "env_value", "case_sensitive", "env_ignore_empty", "env_parse_none_str"), + [ + pytest.param("otel_service_name", "lower", True, False, None, id="case-sensitive"), + pytest.param("OTEL_SERVICE_NAME", "", False, True, None, id="ignore-empty"), + pytest.param("OTEL_ENDPOINT", "null", False, False, "null", id="parse-none"), + pytest.param("excluded_services", "redis", True, False, None, id="bare-exclusion"), + ], +) +def test_env_source_preserves_runtime_options( + monkeypatch: pytest.MonkeyPatch, + set_env_name: str, + env_value: str, + case_sensitive: bool, + env_ignore_empty: bool, + env_parse_none_str: str | None, +) -> None: + for env_name in ( + "OTEL_SERVICE_NAME", + "otel_service_name", + "OTEL_ENDPOINT", + "OTEL_EXPORTER_OTLP_ENDPOINT", + "LITELLM_OTEL_EXCLUDED_SERVICES", + "EXCLUDED_SERVICES", + "excluded_services", + "Excluded_Services", + ): + monkeypatch.delenv(env_name, raising=False) + monkeypatch.setenv(set_env_name, env_value) + config: Final = OpenTelemetryV2Config( + _case_sensitive=case_sensitive, + _env_ignore_empty=env_ignore_empty, + _env_parse_none_str=env_parse_none_str, + ) + assert config.service_name == "litellm" + assert config.endpoint is None + assert config.excluded_services == frozenset() + + +def test_the_documented_env_var_wins_over_a_bare_excluded_services(monkeypatch): + for env_name in ("LITELLM_OTEL_EXCLUDED_SERVICES", "EXCLUDED_SERVICES", "excluded_services", "Excluded_Services"): + monkeypatch.delenv(env_name, raising=False) + monkeypatch.setenv("EXCLUDED_SERVICES", "postgres") + monkeypatch.setenv("LITELLM_OTEL_EXCLUDED_SERVICES", "redis") + assert OpenTelemetryV2Config().excluded_services == frozenset({"redis"}) + + def test_excluded_services_config_wins_over_env(monkeypatch): monkeypatch.setenv("LITELLM_OTEL_EXCLUDED_SERVICES", "redis") assert OpenTelemetryV2Config(excluded_services=["postgres"]).excluded_services == frozenset({"postgresql"}) diff --git a/tests/unit/integrations/otel/test_otel_v2_destinations.py b/tests/unit/integrations/otel/test_otel_v2_destinations.py index 5a7057203e4..cb986e61229 100644 --- a/tests/unit/integrations/otel/test_otel_v2_destinations.py +++ b/tests/unit/integrations/otel/test_otel_v2_destinations.py @@ -1116,6 +1116,18 @@ class TestProviderWiring: assert self._fan_out_of(preset)._excluded_db_systems == frozenset({"redis"}) + @pytest.mark.parametrize("otel", [None, True, "on", "", []], ids=["null", "true", "on", "empty_string", "empty_list"]) + def test_a_non_mapping_otel_block_falls_back_to_the_published_logger_config(self, monkeypatch, otel): + monkeypatch.setattr(litellm, "callback_settings", {"otel": otel}, raising=False) + preset = OpenTelemetryV2( + config=OpenTelemetryV2Config(exporters=[ExporterSpec(kind="in_memory")], excluded_services=["redis"]), + callback_name="langfuse_otel", + ) + + publish_global_otel_v2_provider([], lambda _p: None, registered=preset) + + assert self._fan_out_of(preset)._excluded_db_systems == frozenset({"redis"}) + def test_otel_after_a_preset_reuses_it_and_still_takes_callback_settings_exclusions(self, monkeypatch): """``callbacks: [langfuse_otel, otel]`` keeps one v2 logger, exactly as before ``excluded_services`` existed, and the exclusion still comes from diff --git a/tests/unit/litellm_core_utils/event_loop_lag.py b/tests/unit/litellm_core_utils/event_loop_lag.py index 1cac0365547..5697b3203f5 100644 --- a/tests/unit/litellm_core_utils/event_loop_lag.py +++ b/tests/unit/litellm_core_utils/event_loop_lag.py @@ -1,4 +1,5 @@ import asyncio +import gc import time from collections.abc import Awaitable, Callable from typing import Final, TypeVar @@ -32,7 +33,11 @@ async def timed_with_loop_lags(run: Callable[[], Awaitable[T]]) -> tuple[T, floa finally: finished.set() - (result, took), lags = await asyncio.gather(timed(), loop_wake_lags(finished)) + gc.freeze() + try: + (result, took), lags = await asyncio.gather(timed(), loop_wake_lags(finished)) + finally: + gc.unfreeze() return result, took, lags diff --git a/tests/unit/litellm_proxy_extras/test_litellm_proxy_extras_utils.py b/tests/unit/litellm_proxy_extras/test_litellm_proxy_extras_utils.py index 5540cf54193..ea4a25283a1 100644 --- a/tests/unit/litellm_proxy_extras/test_litellm_proxy_extras_utils.py +++ b/tests/unit/litellm_proxy_extras/test_litellm_proxy_extras_utils.py @@ -3,6 +3,7 @@ import os import re import sys import threading +from dataclasses import dataclass from pathlib import Path from typing import Final @@ -709,6 +710,9 @@ class TestSpendLogsPartitionDetectionMissingPsycopg: _ATTEMPT_BUDGET = 4 +_P3009_MIGRATION_NAME = "20260415120000_health_check_latest_per_model_index" +_P3009_STARTED_AT = "2026-10-02 23:20:56.439594 UTC" +_P3009_DEADLOCK_LOGS = "ERROR: deadlock detected\nDETAIL: Process 72 waits for ShareLock on transaction 991" _P3005_STDERR = """Error: P3005 @@ -730,6 +734,80 @@ ERROR: relation "SomeTable" already exists """ +def _p3009_stderr(migration_name: str, started_at: str) -> str: + return ( + "Error: P3009\n\n" + "migrate found failed migrations in the target database, new migrations will not be applied. " + "Read more about how to resolve migration issues in a production database: " + "https://pris.ly/d/migrate-resolve\n" + f"The `{migration_name}` migration started at {started_at} failed\n" + ) + + +@dataclass(frozen=True, slots=True) +class _LedgerRow: + migration_name: str + started_at: str + finished: bool = False + rolled_back: bool = False + logs: str | None = None + + +@dataclass(frozen=True, slots=True) +class _LedgerCursor: + row: tuple[object, ...] | None = None + + def fetchone(self) -> tuple[object, ...] | None: + return self.row + + def fetchall(self) -> tuple[tuple[object, ...], ...]: + return () + + +class _LedgerConnection: + def __init__(self, ledger: "_FakeLedger") -> None: + self.ledger = ledger + + def __enter__(self) -> "_LedgerConnection": + return self + + def __exit__(self, *args: object) -> None: + return None + + def execute(self, query: object, params: tuple[object, ...] = ()) -> _LedgerCursor: + return self.ledger.execute(query, params) + + +class _FakeLedger: + def __init__(self, at_error: tuple[_LedgerRow, ...], after_peer: tuple[_LedgerRow, ...]) -> None: + self.rows = at_error + self.after_peer = after_peer + self._peer_observed = False + + def connect(self, *args: object, **kwargs: object) -> _LedgerConnection: + return _LedgerConnection(self) + + def execute(self, query: object, params: tuple[object, ...]) -> _LedgerCursor: + text: Final = str(query) + if "WHERE migration_name = %s" not in text or not params: + return _LedgerCursor() + if not self._peer_observed: + self.rows = self.after_peer + self._peer_observed = True + matching: Final = tuple( + row + for row in self.rows + if row.migration_name == params[0] and (len(params) == 1 or row.started_at == params[1]) + ) + if "rolled_back_at IS NULL" in text: + unresolved: Final = next((row for row in matching if not row.finished and not row.rolled_back), None) + return _LedgerCursor((unresolved.logs,) if unresolved else None) + if "IS NOT NULL" in text: + resolved: Final = next((row for row in matching if row.finished or row.rolled_back), None) + return _LedgerCursor((1,) if resolved else None) + return _LedgerCursor() + + @pytest.mark.parametrize( "pooled,direct,expected", ( @@ -759,7 +837,15 @@ class _MigrateDeployHarness: `prisma migrate deploy` outcomes, with every recovery command faked out so nothing touches a database or the packaged migrations directory.""" - def __init__(self, monkeypatch, tmp_path, outcomes, repeat_last=False, confirmed_migrations=()): + def __init__( + self, + monkeypatch, + tmp_path, + outcomes, + repeat_last=False, + confirmed_migrations=(), + ledger: "_FakeLedger | None" = None, + ): import subprocess as subprocess_module import litellm_proxy_extras.utils as utils_module @@ -772,7 +858,11 @@ class _MigrateDeployHarness: self._subprocess_module = subprocess_module self.confirmed_migrations = set(confirmed_migrations) - monkeypatch.delenv("DATABASE_URL", raising=False) + if ledger is None: + monkeypatch.delenv("DATABASE_URL", raising=False) + else: + monkeypatch.setenv("DATABASE_URL", "postgresql://u:p@localhost:9/x") + monkeypatch.setattr("psycopg.connect", ledger.connect) monkeypatch.setenv("LITELLM_MIGRATION_DIR", str(tmp_path)) monkeypatch.setattr(utils_module.prisma_toolchain, "run_prisma", self._fake_run) monkeypatch.setattr(utils_module, "_get_prisma_env", lambda: {}) @@ -819,6 +909,84 @@ class _MigrateDeployHarness: return True +class TestConcurrentP3009Recovery: + @pytest.mark.parametrize( + "after_peer", + ( + (_LedgerRow(_P3009_MIGRATION_NAME, _P3009_STARTED_AT, rolled_back=True, logs=_P3009_DEADLOCK_LOGS),), + ( + _LedgerRow(_P3009_MIGRATION_NAME, _P3009_STARTED_AT, rolled_back=True, logs=_P3009_DEADLOCK_LOGS), + _LedgerRow(_P3009_MIGRATION_NAME, "2026-10-02 23:21:11.539224 UTC"), + ), + (_LedgerRow(_P3009_MIGRATION_NAME, _P3009_STARTED_AT, finished=True, logs=_P3009_DEADLOCK_LOGS),), + ), + ids=("rolled-back", "rolled-back-beside-a-fresh-in-flight-row", "finished"), + ) + def test_a_p3009_row_a_peer_already_recovered_is_retried( + self, + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + after_peer: tuple[_LedgerRow, ...], + ) -> None: + deadlocked_row: Final = _LedgerRow( + _P3009_MIGRATION_NAME, + _P3009_STARTED_AT, + logs=_P3009_DEADLOCK_LOGS, + ) + harness: Final = _MigrateDeployHarness( + monkeypatch, + tmp_path, + [_p3009_stderr(_P3009_MIGRATION_NAME, _P3009_STARTED_AT), "ok"], + ledger=_FakeLedger(at_error=(deadlocked_row,), after_peer=after_peer), + ) + + assert harness.run() is True + assert len(harness.deploy_calls) == 2 + + @pytest.mark.parametrize( + "ledger_rows", + ( + ( + _LedgerRow( + _P3009_MIGRATION_NAME, + _P3009_STARTED_AT, + logs='ERROR: syntax error at or near "SLECT"', + ), + ), + ( + _LedgerRow( + _P3009_MIGRATION_NAME, + _P3009_STARTED_AT, + logs='ERROR: syntax error at or near "SLECT"', + ), + _LedgerRow( + _P3009_MIGRATION_NAME, + "2026-10-02 23:19:40.120000 UTC", + rolled_back=True, + logs=_P3009_DEADLOCK_LOGS, + ), + ), + ), + ids=("only-row", "beside-a-recovered-earlier-attempt"), + ) + def test_an_unresolved_p3009_row_without_the_deadlock_marker_stops( + self, + monkeypatch: pytest.MonkeyPatch, + tmp_path: Path, + ledger_rows: tuple[_LedgerRow, ...], + ) -> None: + harness: Final = _MigrateDeployHarness( + monkeypatch, + tmp_path, + [_p3009_stderr(_P3009_MIGRATION_NAME, _P3009_STARTED_AT)], + ledger=_FakeLedger(at_error=ledger_rows, after_peer=ledger_rows), + ) + + with pytest.raises(RuntimeError, match="Migration completion could not be verified"): + harness.run() + assert len(harness.deploy_calls) == 1 + + class TestMigrateDeployAttemptAccounting: def test_a_push_created_database_finishes_bootstrapping(self, monkeypatch, tmp_path): harness = _MigrateDeployHarness( @@ -1029,6 +1197,84 @@ class TestJWTKeyMappingCascade: +class TestStripPrismaQueryParams: + """The psycopg URL the job connects with is derived from the Prisma-dialect + DATABASE_URL, whose TLS params mean something else to libpq.""" + + @staticmethod + def _query(url: str) -> dict[str, str]: + from urllib.parse import parse_qsl, urlparse + + return dict(parse_qsl(urlparse(url).query)) + + def test_prisma_ca_sslcert_becomes_sslrootcert_with_verify_full(self): + url = "postgresql://u:p@writer:5432/db?schema=public&sslmode=require&sslcert=/tmp/pinned.pem&sslaccept=strict" + + cleaned = ProxyExtrasDBManager._strip_prisma_query_params(url) + + assert self._query(cleaned) == {"sslmode": "verify-full", "sslrootcert": "/tmp/pinned.pem"} + assert cleaned.startswith("postgresql://u:p@writer:5432/db?") + + @pytest.mark.parametrize("sslmode", ["prefer", "require"]) + @pytest.mark.parametrize("sslaccept", ["strict", "unknown-mode-prisma-treats-as-strict"]) + def test_strict_verifies_chain_and_hostname_whatever_sslmode_prisma_was_given(self, sslmode, sslaccept): + url = f"postgresql://writer/db?sslmode={sslmode}&sslcert=/certs/ca.pem&sslaccept={sslaccept}" + + cleaned = ProxyExtrasDBManager._strip_prisma_query_params(url) + + assert self._query(cleaned) == {"sslmode": "verify-full", "sslrootcert": "/certs/ca.pem"} + + def test_strict_with_tls_disabled_stays_off(self): + url = "postgresql://writer/db?sslmode=disable&sslcert=/certs/ca.pem&sslaccept=strict" + + cleaned = ProxyExtrasDBManager._strip_prisma_query_params(url) + + assert self._query(cleaned) == {"sslmode": "disable"} + + @pytest.mark.parametrize("sslaccept", ["&sslaccept=accept_invalid_certs", ""]) + def test_without_strict_the_ca_is_dropped_so_libpq_checks_nothing_like_prisma(self, sslaccept): + url = f"postgresql://writer/db?sslmode=require&sslcert=/certs/ca.pem{sslaccept}" + + cleaned = ProxyExtrasDBManager._strip_prisma_query_params(url) + + assert self._query(cleaned) == {"sslmode": "require"} + + def test_a_ca_alone_without_strict_or_sslmode_leaves_libpq_its_defaults(self): + cleaned = ProxyExtrasDBManager._strip_prisma_query_params("postgresql://writer/db?sslcert=/certs/ca.pem") + + assert cleaned == "postgresql://writer/db" + + def test_a_libpq_client_certificate_pair_is_left_alone(self): + url = "postgresql://writer/db?sslmode=verify-full&sslrootcert=/ca.pem&sslcert=/client.crt&sslkey=/client.key" + + cleaned = ProxyExtrasDBManager._strip_prisma_query_params(url) + + assert self._query(cleaned) == { + "sslmode": "verify-full", + "sslrootcert": "/ca.pem", + "sslcert": "/client.crt", + "sslkey": "/client.key", + } + + def test_an_explicit_sslrootcert_wins_over_the_prisma_sslcert(self): + url = "postgresql://writer/db?sslmode=require&sslrootcert=/ca.pem&sslcert=/pinned.pem&sslaccept=strict" + + cleaned = ProxyExtrasDBManager._strip_prisma_query_params(url) + + assert self._query(cleaned) == {"sslmode": "verify-full", "sslrootcert": "/ca.pem"} + + def test_prisma_only_params_are_dropped_and_plain_urls_pass_through(self): + url = "postgresql://u:p@pooler:6543/db?schema=tenant&pgbouncer=true&connection_limit=5&connect_timeout=3" + + cleaned = ProxyExtrasDBManager._strip_prisma_query_params(url) + + assert cleaned == "postgresql://u:p@pooler:6543/db?connect_timeout=3" + assert ( + ProxyExtrasDBManager._strip_prisma_query_params("postgresql://u:p@writer/db") + == "postgresql://u:p@writer/db" + ) + + class TestBuildRequestLogIndexes: """The migration job hands the index build the direct database URL and the schema the migrations target, waits for it, and reports its result.""" diff --git a/tests/unit/llms/anthropic/chat/test_anthropic_chat_handler.py b/tests/unit/llms/anthropic/chat/test_anthropic_chat_handler.py index 1e0d2e55373..29dc6da2677 100644 --- a/tests/unit/llms/anthropic/chat/test_anthropic_chat_handler.py +++ b/tests/unit/llms/anthropic/chat/test_anthropic_chat_handler.py @@ -10,6 +10,7 @@ import pytest import litellm from litellm._uuid import uuid from litellm.constants import RESPONSE_FORMAT_TOOL_NAME +from litellm.litellm_core_utils.prompt_templates.factory import anthropic_messages_pt from litellm.llms.anthropic.chat.handler import ModelResponseIterator, make_call from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler from litellm.types.llms.openai import ( @@ -206,22 +207,83 @@ def test_streaming_thinking_blocks_are_replayable_after_signature_delta(): {"type": "thinking", "thinking": "Step 1. "}, {"type": "thinking", "thinking": "Step 2."}, ) - expected_thinking_block = { - "type": "thinking", - "thinking": "Step 1. Step 2.", - "signature": "sig-final", - } + expected_signature_block = {"type": "thinking", "thinking": "", "signature": "sig-final"} assert reasoning_content == "Step 1. Step 2." - assert thinking_blocks == (*expected_delta_blocks, expected_thinking_block) + assert thinking_blocks == (*expected_delta_blocks, expected_signature_block) + assert "".join(block.get("thinking") or "" for block in thinking_blocks) == reasoning_content assert parsed_chunks[1].choices[0].delta.provider_specific_fields == { "thinking_blocks": [expected_delta_blocks[0]] } assert parsed_chunks[-1].choices[0].delta.provider_specific_fields == { - "thinking_blocks": [expected_thinking_block] + "thinking_blocks": [expected_signature_block] } +def test_streamed_signed_thinking_round_trips_to_the_next_turn_once(): + iterator: Final = ModelResponseIterator(streaming_response=MagicMock(), sync_stream=True, json_mode=False) + thinking_parts: Final = ("Paris needs both tools. ", "Call weather first.") + thinking_text: Final = "".join(thinking_parts) + events: Final = ( + { + "type": "message_start", + "message": { + "id": "msg_paris", + "type": "message", + "role": "assistant", + "model": "claude-sonnet-4-5", + "content": [], + "stop_reason": None, + "usage": {"input_tokens": 20, "output_tokens": 1}, + }, + }, + {"type": "content_block_start", "index": 0, "content_block": {"type": "thinking", "thinking": ""}}, + {"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": thinking_parts[0]}}, + {"type": "content_block_delta", "index": 0, "delta": {"type": "thinking_delta", "thinking": thinking_parts[1]}}, + {"type": "content_block_delta", "index": 0, "delta": {"type": "signature_delta", "signature": "sig-paris"}}, + {"type": "content_block_stop", "index": 0}, + { + "type": "content_block_start", + "index": 1, + "content_block": {"type": "tool_use", "id": "toolu_paris", "name": "get_weather", "input": {}}, + }, + { + "type": "content_block_delta", + "index": 1, + "delta": {"type": "input_json_delta", "partial_json": '{"city": "Paris"}'}, + }, + {"type": "content_block_stop", "index": 1}, + {"type": "message_delta", "delta": {"stop_reason": "tool_use", "stop_sequence": None}, "usage": {"output_tokens": 30}}, + {"type": "message_stop"}, + ) + user_message: Final = {"role": "user", "content": "What's the weather in Paris?"} + + streamed: Final = litellm.stream_chunk_builder( + chunks=[iterator.chunk_parser(event) for event in events], messages=[user_message] + ) + assistant: Final = streamed.choices[0].message + + assert assistant.reasoning_content == thinking_text + assert assistant.thinking_blocks == [{"type": "thinking", "thinking": thinking_text, "signature": "sig-paris"}] + assert [call.id for call in assistant.tool_calls] == ["toolu_paris"] + + saved_history: Final = json.loads( + json.dumps( + [ + user_message, + assistant.model_dump(), + {"role": "tool", "tool_call_id": "toolu_paris", "content": "22C and sunny"}, + ] + ) + ) + replayed: Final = anthropic_messages_pt(messages=saved_history, model="claude-sonnet-4-5", llm_provider="anthropic") + + assert replayed[1]["content"][0] == {"type": "thinking", "thinking": thinking_text, "signature": "sig-paris"} + replayed_tool_use_ids: Final = [block["id"] for block in replayed[1]["content"] if block["type"] == "tool_use"] + assert replayed_tool_use_ids == ["toolu_paris"] + assert replayed[2]["content"][0]["type"] == "tool_result" + + def test_streaming_unsigned_thinking_deltas_keep_reasoning_content(): model_response_iterator = ModelResponseIterator( streaming_response=MagicMock(), sync_stream=True, json_mode=False diff --git a/tests/unit/llms/bedrock/chat/test_invoke_handler.py b/tests/unit/llms/bedrock/chat/test_invoke_handler.py index ed8b7023977..43b689e499d 100644 --- a/tests/unit/llms/bedrock/chat/test_invoke_handler.py +++ b/tests/unit/llms/bedrock/chat/test_invoke_handler.py @@ -3,6 +3,7 @@ import binascii import itertools import datetime import json +import re import struct from collections.abc import AsyncIterator, Mapping, Sequence from typing import Final @@ -21,7 +22,7 @@ from litellm.llms.bedrock.chat.invoke_handler import ( make_sync_call, ) from litellm.exceptions import MidStreamFallbackError -from litellm.llms.bedrock.common_utils import BedrockError +from litellm.llms.bedrock.common_utils import BedrockError, get_bedrock_stream_event_statuses from litellm.llms.custom_httpx.http_handler import AsyncHTTPHandler, HTTPHandler from litellm.types.utils import ModelResponseStream @@ -717,19 +718,28 @@ async def test_async_invoke_streaming_non_200_forwards_bedrock_response_headers( assert exc_info.value.response.headers["x-amzn-requestid"] == "req-non200-async" -def _bedrock_event_stream_frame(chunk: Mapping[str, object]) -> bytes: +def _event_stream_frame(event_type: str, payload: bytes) -> bytes: def header(name: str, value: str) -> bytes: return bytes([len(name)]) + name.encode() + bytes([7]) + struct.pack(">H", len(value)) + value.encode() - headers: Final = header(":event-type", "chunk") + header(":content-type", "application/json") + header( + headers: Final = header(":event-type", event_type) + header(":content-type", "application/json") + header( ":message-type", "event" ) - payload: Final = json.dumps({"bytes": base64.b64encode(json.dumps(chunk).encode()).decode()}).encode() prelude: Final = struct.pack(">II", 12 + len(headers) + len(payload) + 4, len(headers)) body: Final = prelude + struct.pack(">I", binascii.crc32(prelude)) + headers + payload return body + struct.pack(">I", binascii.crc32(body)) +def _bedrock_event_stream_frame(chunk: Mapping[str, object]) -> bytes: + return _event_stream_frame( + "chunk", json.dumps({"bytes": base64.b64encode(json.dumps(chunk).encode()).decode()}).encode() + ) + + +def _converse_event_frame(event_type: str, body: Mapping[str, object]) -> bytes: + return _event_stream_frame(event_type, json.dumps(body).encode()) + + def _openai_stream_chunk(delta: Mapping[str, str], finish_reason: str | None = None) -> Mapping[str, object]: return { "id": "chatcmpl-1", @@ -925,3 +935,193 @@ async def test_async_converse_stream_with_an_empty_200_body_raises_instead_of_an _ = [chunk async for chunk in stream] _assert_empty_stream_surfaced_as_bad_gateway(exc_info.value) + + +_UPSTREAM_REJECTION: Final = "structured output schema uses unsupported regex negative look-ahead" +_CUSTOMER_REJECTION_EVENT_TYPE: Final = "validationException" + + +def _modeled_exception_event_types() -> tuple[str, ...]: + statuses: Final = get_bedrock_stream_event_statuses() + assert statuses is not None + return tuple(sorted(name for name, status in statuses.items() if status is not None)) + + +def _modeled_status(event_type: str) -> int: + statuses: Final = get_bedrock_stream_event_statuses() + assert statuses is not None + status: Final = statuses[event_type] + assert status is not None + return status + + +_CONVERSE_CONTENT_FRAMES: Final = ( + _converse_event_frame("messageStart", {"role": "assistant"}), + _converse_event_frame("contentBlockDelta", {"contentBlockIndex": 0, "delta": {"text": "hi"}}), + _converse_event_frame("contentBlockStop", {"contentBlockIndex": 0}), + _converse_event_frame("messageStop", {"stopReason": "end_turn"}), +) + + +def _unknown_event_frame() -> bytes: + return _converse_event_frame("somethingBedrockAddedLater", {"message": _UPSTREAM_REJECTION}) + + +@pytest.mark.parametrize("event_type", _modeled_exception_event_types()) +def test_iter_bytes_raises_the_modeled_error_for_an_exception_named_event_frame(event_type: str) -> None: + decoder: Final = AWSEventStreamDecoder(model="us.moonshotai.kimi-k3") + frame: Final = _converse_event_frame(event_type, {"message": _UPSTREAM_REJECTION}) + + with pytest.raises(BedrockError) as exc_info: + list(decoder.iter_bytes(iter([frame]), response_headers=_event_stream_headers())) + + assert exc_info.value.status_code == _modeled_status(event_type) + assert exc_info.value.status_code != 200 + assert exc_info.value.message.startswith(event_type) + assert _UPSTREAM_REJECTION in exc_info.value.message + + +@pytest.mark.asyncio +async def test_aiter_bytes_raises_the_modeled_error_for_an_exception_named_event_frame() -> None: + event_type: Final = _CUSTOMER_REJECTION_EVENT_TYPE + + async def _chunks() -> AsyncIterator[bytes]: + yield _converse_event_frame(event_type, {"message": _UPSTREAM_REJECTION}) + + decoder: Final = AWSEventStreamDecoder(model="us.moonshotai.kimi-k3") + + with pytest.raises(BedrockError) as exc_info: + _ = [chunk async for chunk in decoder.aiter_bytes(_chunks(), response_headers=_event_stream_headers())] + + assert exc_info.value.status_code == _modeled_status(event_type) + assert _UPSTREAM_REJECTION in exc_info.value.message + + +def _assert_unknown_event_stream_error(error: BedrockError, body: bytes) -> None: + assert error.status_code == 502 + assert "HTTP 200" in error.message + assert "none of its 1 events carried a known event type" in error.message + assert "somethingBedrockAddedLater" in error.message + assert _UPSTREAM_REJECTION in error.message + assert f"{len(body)} bytes received" in error.message + assert "req-empty-1" in error.message + + +def test_iter_bytes_raises_when_no_event_carries_a_known_event_type() -> None: + decoder: Final = AWSEventStreamDecoder(model="us.moonshotai.kimi-k3") + body: Final = _unknown_event_frame() + + with pytest.raises(BedrockError) as exc_info: + list(decoder.iter_bytes(iter([body]), response_headers=_event_stream_headers())) + + _assert_unknown_event_stream_error(exc_info.value, body) + + +@pytest.mark.asyncio +async def test_aiter_bytes_raises_when_no_event_carries_a_known_event_type() -> None: + body: Final = _unknown_event_frame() + + async def _chunks() -> AsyncIterator[bytes]: + yield body + + decoder: Final = AWSEventStreamDecoder(model="us.moonshotai.kimi-k3") + + with pytest.raises(BedrockError) as exc_info: + _ = [chunk async for chunk in decoder.aiter_bytes(_chunks(), response_headers=_event_stream_headers())] + + _assert_unknown_event_stream_error(exc_info.value, body) + + +def test_iter_bytes_keeps_a_stream_whose_unknown_event_sits_beside_known_frames() -> None: + decoder: Final = AWSEventStreamDecoder(model="us.moonshotai.kimi-k3") + frames: Final = (_CONVERSE_CONTENT_FRAMES[0], _unknown_event_frame(), *_CONVERSE_CONTENT_FRAMES[1:]) + + chunks: Final = list(decoder.iter_bytes(iter(frames), response_headers=_event_stream_headers())) + + texts: Final = [chunk.choices[0].delta.content for chunk in chunks if isinstance(chunk, ModelResponseStream)] + assert "".join(text or "" for text in texts) == "hi" + finish_reasons: Final = [ + chunk.choices[0].finish_reason for chunk in chunks if isinstance(chunk, ModelResponseStream) + ] + assert "stop" in finish_reasons + + +def _assert_exception_event_surfaced_with_its_modeled_status(error: BaseException, event_type: str) -> None: + assert not isinstance(error, litellm.BadGatewayError) + assert getattr(error, "status_code", None) == _modeled_status(event_type) + assert event_type in str(error) + assert _UPSTREAM_REJECTION in str(error) + + +def test_converse_stream_with_an_exception_event_frame_raises_instead_of_an_empty_turn( + _aws_test_credentials: None, +) -> None: + event_type: Final = _CUSTOMER_REJECTION_EVENT_TYPE + frame: Final = _converse_event_frame(event_type, {"message": _UPSTREAM_REJECTION}) + response: Final = MagicMock(status_code=200, headers=_event_stream_headers()) + response.iter_bytes = lambda chunk_size=None: iter([frame]) + client: Final = HTTPHandler() + client.post = MagicMock(return_value=response) + + with pytest.raises(Exception, match=re.escape(_UPSTREAM_REJECTION)) as exc_info: + list( + litellm.completion( + model="bedrock/us.moonshotai.kimi-k3", + messages=[{"role": "user", "content": "hi"}], + stream=True, + client=client, + ) + ) + + _assert_exception_event_surfaced_with_its_modeled_status(exc_info.value, event_type) + + +@pytest.mark.asyncio +async def test_async_converse_stream_with_an_exception_event_frame_raises_instead_of_an_empty_turn( + _aws_test_credentials: None, +) -> None: + event_type: Final = _CUSTOMER_REJECTION_EVENT_TYPE + + async def _aiter_bytes(chunk_size: int | None = None) -> AsyncIterator[bytes]: + yield _converse_event_frame(event_type, {"message": _UPSTREAM_REJECTION}) + + response: Final = MagicMock(status_code=200, headers=_event_stream_headers()) + response.aiter_bytes = _aiter_bytes + client: Final = AsyncHTTPHandler() + client.post = AsyncMock(return_value=response) + + stream: Final = await litellm.acompletion( + model="bedrock/us.moonshotai.kimi-k3", + messages=[{"role": "user", "content": "hi"}], + stream=True, + client=client, + ) + with pytest.raises(Exception, match=re.escape(_UPSTREAM_REJECTION)) as exc_info: + _ = [chunk async for chunk in stream] + + _assert_exception_event_surfaced_with_its_modeled_status(exc_info.value, event_type) + + +def test_converse_stream_made_only_of_unknown_events_raises_instead_of_an_empty_turn( + _aws_test_credentials: None, +) -> None: + response: Final = MagicMock(status_code=200, headers=_event_stream_headers()) + response.iter_bytes = lambda chunk_size=None: iter([_unknown_event_frame()]) + client: Final = HTTPHandler() + client.post = MagicMock(return_value=response) + + with pytest.raises(MidStreamFallbackError) as exc_info: + list( + litellm.completion( + model="bedrock/us.moonshotai.kimi-k3", + messages=[{"role": "user", "content": "hi"}], + stream=True, + client=client, + ) + ) + + assert exc_info.value.status_code == 502 + assert exc_info.value.is_pre_first_chunk is True + assert isinstance(exc_info.value.original_exception, litellm.BadGatewayError) + assert "somethingBedrockAddedLater" in str(exc_info.value) + assert _UPSTREAM_REJECTION in str(exc_info.value) diff --git a/tests/unit/llms/bedrock/test_bedrock_common_utils.py b/tests/unit/llms/bedrock/test_bedrock_common_utils.py index e5118f90e44..52d539d8ea7 100644 --- a/tests/unit/llms/bedrock/test_bedrock_common_utils.py +++ b/tests/unit/llms/bedrock/test_bedrock_common_utils.py @@ -981,3 +981,72 @@ def test_unmapped_openai_family_model_routes_to_converse(): assert BedrockModelInfo.get_bedrock_route(unmapped) == "converse" imported: Final = "bedrock/openai/arn:aws:bedrock:us-east-1:123456789012:imported-model/abc123" assert BedrockModelInfo.get_bedrock_route(imported) == "openai" + + +def test_bedrock_stream_event_statuses_cover_every_modeled_member_of_both_stream_shapes(): + pytest.importorskip("botocore") + from botocore.loaders import Loader + from botocore.model import ServiceModel + + import litellm.llms.bedrock.common_utils as mod + + mod.get_bedrock_stream_event_statuses.cache_clear() + statuses = mod.get_bedrock_stream_event_statuses() + assert statuses is not None + + service_model = ServiceModel(Loader().load_service_model("bedrock-runtime", "service-2")) + for shape_name in ("ConverseStreamOutput", "ResponseStream"): + for name, member in service_model.shape_for(shape_name).members.items(): + modeled = (member.metadata or {}).get("error", {}).get("httpStatusCode") + assert statuses[name] == (None if modeled is None else int(modeled)) + assert mod.bedrock_stream_event_error_status(name) == statuses[name] + + assert any(status is not None for status in statuses.values()) + assert any(status is None for status in statuses.values()) + assert mod.bedrock_stream_event_error_status("notAModeledEvent") is None + assert mod.bedrock_stream_event_error_status(None) is None + + +def test_bedrock_stream_event_statuses_load_failure_returns_none(): + from unittest.mock import patch + + import litellm.llms.bedrock.common_utils as mod + + pytest.importorskip("botocore") + mod.get_bedrock_stream_event_statuses.cache_clear() + with patch("botocore.loaders.Loader.load_service_model", side_effect=Exception("no data")): + assert mod._load_bedrock_stream_event_statuses() is None + assert mod.get_bedrock_stream_event_statuses() is None + assert mod.bedrock_stream_event_error_status("validationException") is None + mod.get_bedrock_stream_event_statuses.cache_clear() + + +@pytest.mark.parametrize( + ("headers", "expected_status", "expected_message"), + [ + ({":message-type": "error"}, 400, '{"message":"upstream failed"}'), + ( + {":message-type": "exception", ":exception-type": "somethingNotModeled"}, + 400, + 'somethingNotModeled {"message":"upstream failed"}', + ), + ( + {":message-type": "exception", ":exception-type": "throttlingException"}, + 429, + 'throttlingException {"message":"upstream failed"}', + ), + ], +) +def test_build_bedrock_stream_error_resolves_status_from_the_exception_type( + headers: dict[str, str], expected_status: int, expected_message: str +): + pytest.importorskip("botocore") + from litellm.llms.bedrock.common_utils import build_bedrock_stream_error, get_bedrock_response_stream_shape + + error = build_bedrock_stream_error( + {"status_code": 400, "headers": headers, "body": b'{"message":"upstream failed"}'}, + get_bedrock_response_stream_shape(), + ) + + assert error.status_code == expected_status + assert error.message == expected_message diff --git a/tests/unit/llms/chatgpt/responses/test_chatgpt_responses_transformation.py b/tests/unit/llms/chatgpt/responses/test_chatgpt_responses_transformation.py index 0b04dd0ed78..88868844bb1 100644 --- a/tests/unit/llms/chatgpt/responses/test_chatgpt_responses_transformation.py +++ b/tests/unit/llms/chatgpt/responses/test_chatgpt_responses_transformation.py @@ -6,6 +6,7 @@ Source: litellm/llms/chatgpt/responses/transformation.py import json from collections.abc import Generator +from typing import Final from unittest.mock import MagicMock, patch import httpx @@ -30,6 +31,46 @@ def local_model_cost_map(monkeypatch: pytest.MonkeyPatch) -> Generator[None, Non class TestChatGPTResponsesAPITransformation: + @pytest.mark.parametrize( + ("requested_tier", "expected_tier"), + [("default", "default"), ("priority", "priority"), ("fast", "priority")], + ) + @pytest.mark.parametrize("effort", ["low", "high"]) + def test_chatgpt_preserves_service_tier(self, requested_tier: str, expected_tier: str, effort: str) -> None: + config: Final = ChatGPTResponsesAPIConfig() + request: Final = config.transform_responses_api_request( + model="chatgpt/gpt-6.1-sol", + input=[{"role": "user", "content": "Reply with OK"}], + response_api_optional_request_params={ + "service_tier": requested_tier, + "reasoning": {"effort": effort}, + "max_output_tokens": 16, + "prompt_cache_options": {"ttl": "30m"}, + }, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + + assert request["service_tier"] == expected_tier + assert request["reasoning"] == {"effort": effort} + assert request["stream"] is True + assert request["store"] is False + assert "max_output_tokens" not in request + assert "prompt_cache_options" not in request + + @pytest.mark.parametrize("requested_tier", [None, "auto", "flex", "unknown"]) + def test_chatgpt_does_not_introduce_unsupported_service_tier(self, requested_tier: str | None) -> None: + config: Final = ChatGPTResponsesAPIConfig() + request: Final = config.transform_responses_api_request( + model="chatgpt/gpt-6.1-sol", + input=[{"role": "user", "content": "Reply with OK"}], + response_api_optional_request_params={} if requested_tier is None else {"service_tier": requested_tier}, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + + assert "service_tier" not in request + @pytest.mark.parametrize( "model_name", [ @@ -55,7 +96,6 @@ class TestChatGPTResponsesAPITransformation: assert isinstance(config, ChatGPTResponsesAPIConfig) assert config.custom_llm_provider == LlmProviders.CHATGPT - @pytest.mark.parametrize( "model_name", [ @@ -92,14 +132,10 @@ class TestChatGPTResponsesAPITransformation: url = config.get_complete_url(api_base=None, litellm_params={}) assert url == "https://chatgpt.example.com/responses" - custom_url = config.get_complete_url( - api_base="https://custom.chatgpt.com", litellm_params={} - ) + custom_url = config.get_complete_url(api_base="https://custom.chatgpt.com", litellm_params={}) assert custom_url == "https://custom.chatgpt.com/responses" - url_with_slash = config.get_complete_url( - api_base="https://chatgpt.example.com/", litellm_params={} - ) + url_with_slash = config.get_complete_url(api_base="https://chatgpt.example.com/", litellm_params={}) assert url_with_slash == "https://chatgpt.example.com/responses" @patch("litellm.llms.chatgpt.responses.transformation.Authenticator") @@ -162,9 +198,7 @@ class TestChatGPTResponsesAPITransformation: "user": "user_123", "temperature": 0.2, "top_p": 0.9, - "context_management": [ - {"type": "compaction", "compact_threshold": 200000} - ], + "context_management": [{"type": "compaction", "compact_threshold": 200000}], "metadata": {"foo": "bar"}, "max_output_tokens": 123, "stream_options": {"include_usage": True}, @@ -203,9 +237,7 @@ class TestChatGPTResponsesAPITransformation: ("chatgpt/gpt-5.3-codex", "gpt-5.3-codex"), ], ) - def test_chatgpt_non_stream_sse_response_parsing( - self, model_name: str, response_model: str - ): + def test_chatgpt_non_stream_sse_response_parsing(self, model_name: str, response_model: str): config = ChatGPTResponsesAPIConfig() response_payload = { "id": "resp_test", @@ -228,9 +260,7 @@ class TestChatGPTResponsesAPITransformation: "", ] ) - raw_response = httpx.Response( - 200, headers={"content-type": "text/event-stream"}, text=sse_body - ) + raw_response = httpx.Response(200, headers={"content-type": "text/event-stream"}, text=sse_body) logging_obj = MagicMock() parsed = config.transform_response_api_response( @@ -248,9 +278,7 @@ class TestChatGPTResponsesAPITransformation: ("chatgpt/gpt-5.3-codex", "gpt-5.3-codex"), ], ) - def test_chatgpt_non_stream_sse_response_recovers_output_items( - self, model_name: str, response_model: str - ): + def test_chatgpt_non_stream_sse_response_recovers_output_items(self, model_name: str, response_model: str): config = ChatGPTResponsesAPIConfig() response_payload = { "id": "resp_test", @@ -273,9 +301,7 @@ class TestChatGPTResponsesAPITransformation: "", ] ) - raw_response = httpx.Response( - 200, headers={"content-type": "text/event-stream"}, text=sse_body - ) + raw_response = httpx.Response(200, headers={"content-type": "text/event-stream"}, text=sse_body) logging_obj = MagicMock() parsed = config.transform_response_api_response( @@ -315,9 +341,7 @@ class TestChatGPTResponsesAPITransformation: "", ] ) - raw_response = httpx.Response( - 200, headers={"content-type": "text/event-stream"}, text=sse_body - ) + raw_response = httpx.Response(200, headers={"content-type": "text/event-stream"}, text=sse_body) logging_obj = MagicMock() parsed = config.transform_response_api_response( @@ -350,9 +374,7 @@ class TestChatGPTResponsesAPITransformation: "", ] ) - raw_response = httpx.Response( - 502, headers={"content-type": "text/event-stream"}, text=sse_body - ) + raw_response = httpx.Response(502, headers={"content-type": "text/event-stream"}, text=sse_body) logging_obj = MagicMock() with pytest.raises(OpenAIError) as exc_info: diff --git a/tests/unit/llms/exa_ai/__init__.py b/tests/unit/llms/exa_ai/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/unit/llms/exa_ai/search/__init__.py b/tests/unit/llms/exa_ai/search/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/unit/llms/exa_ai/search/test_transformation.py b/tests/unit/llms/exa_ai/search/test_transformation.py new file mode 100644 index 00000000000..5e5eb24f23b --- /dev/null +++ b/tests/unit/llms/exa_ai/search/test_transformation.py @@ -0,0 +1,33 @@ +from typing import Final +from unittest.mock import Mock + +import httpx +import pytest + +from litellm.llms.exa_ai.search.transformation import ExaAISearchConfig + + +@pytest.mark.parametrize( + ("content_fields", "expected_snippet"), + [ + ({"text": "full text"}, "full text"), + ({"highlights": ["first highlight", "second highlight"]}, "first highlight\n\nsecond highlight"), + ({"summary": "a summary"}, "a summary"), + ({"text": "full text", "highlights": ["a highlight"], "summary": "a summary"}, "full text"), + ({"highlights": ["a highlight"], "summary": "a summary"}, "a highlight"), + ({"text": "", "highlights": ["a highlight"]}, "a highlight"), + ({"highlights": [], "summary": "a summary"}, "a summary"), + ({}, ""), + ], +) +def test_transform_search_response_snippet_falls_back_through_content_modes( + content_fields: dict[str, str | list[str]], expected_snippet: str +): + raw_response: Final = httpx.Response( + 200, + json={"results": [{"title": "Title", "url": "https://example.com", **content_fields}]}, + ) + + response: Final = ExaAISearchConfig().transform_search_response(raw_response, logging_obj=Mock()) + + assert response.results[0].snippet == expected_snippet diff --git a/tests/unit/llms/laya/__init__.py b/tests/unit/llms/laya/__init__.py new file mode 100644 index 00000000000..8b137891791 --- /dev/null +++ b/tests/unit/llms/laya/__init__.py @@ -0,0 +1 @@ + diff --git a/tests/unit/llms/laya/test_common_utils.py b/tests/unit/llms/laya/test_common_utils.py new file mode 100644 index 00000000000..408bd300beb --- /dev/null +++ b/tests/unit/llms/laya/test_common_utils.py @@ -0,0 +1,20 @@ +from collections.abc import Mapping + +import pytest + +from litellm.llms.laya.common_utils import laya_response_model + + +@pytest.mark.parametrize( + ("routing", "requested", "expected"), + [ + ({"model": "multilingual"}, "english", "multilingual"), + (None, "english", "english"), + ({"model": 42}, "english", "english"), + (None, None, "unknown"), + ], +) +def test_laya_identity_tracks_the_checkpoint_not_the_shared_agent_name( + routing: Mapping[str, object] | None, requested: str | None, expected: str +) -> None: + assert laya_response_model({"model": "laya-rl-agent", "routing": routing}, requested) == expected diff --git a/tests/unit/llms/oci/test_oci_common_utils.py b/tests/unit/llms/oci/test_oci_common_utils.py index d306d7351dd..e66645c4dcd 100644 --- a/tests/unit/llms/oci/test_oci_common_utils.py +++ b/tests/unit/llms/oci/test_oci_common_utils.py @@ -5,10 +5,16 @@ Covers schema utilities, signing helpers, and credential resolution paths that require no real OCI credentials or network calls. """ -import pytest +import sys +import types +from types import MappingProxyType +from typing import Final from unittest.mock import MagicMock, patch +import pytest + from litellm.llms.oci.common_utils import ( + _OCI_REALM_DOMAINS, OCI_API_VERSION, OCIError, OCIRequestWrapper, @@ -40,7 +46,8 @@ def test_oci_api_version_constant(): def test_sha256_base64_known_value(): - import base64, hashlib + import base64 + import hashlib data = b"hello" expected = base64.b64encode(hashlib.sha256(data).digest()).decode() @@ -60,9 +67,7 @@ def test_sha256_base64_empty(): def test_build_signature_string_request_target(): headers = {"host": "example.com", "date": "Mon, 01 Jan 2024 00:00:00 GMT"} - result = build_signature_string( - "POST", "/20231130/actions/chat", headers, ["(request-target)", "host", "date"] - ) + result = build_signature_string("POST", "/20231130/actions/chat", headers, ["(request-target)", "host", "date"]) lines = result.split("\n") assert lines[0] == "(request-target): post /20231130/actions/chat" assert lines[1] == "host: example.com" @@ -161,12 +166,10 @@ def test_get_oci_base_url_explicit_api_base(): ], ) def test_get_oci_base_url_strips_trailing_action_path(api_base): - assert ( - get_oci_base_url({}, api_base=api_base) - == "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com" - ) + assert get_oci_base_url({}, api_base=api_base) == "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com" +@pytest.mark.usefixtures("without_oci_sdk", "isolated_region_metadata") def test_get_oci_base_url_from_region(): url = get_oci_base_url({"oci_region": "eu-frankfurt-1"}) assert url == "https://inference.generativeai.eu-frankfurt-1.oci.oraclecloud.com" @@ -192,6 +195,7 @@ def test_get_oci_base_url_rejects_unsafe_region(region): get_oci_base_url({"oci_region": region}) +@pytest.mark.usefixtures("without_oci_sdk", "isolated_region_metadata") def test_get_oci_base_url_empty_region_falls_back_to_default(monkeypatch): monkeypatch.delenv("OCI_REGION", raising=False) url = get_oci_base_url({"oci_region": ""}) @@ -209,11 +213,248 @@ def test_get_oci_base_url_empty_region_falls_back_to_default(monkeypatch): "ap", ], ) +@pytest.mark.usefixtures("without_oci_sdk", "isolated_region_metadata") def test_get_oci_base_url_accepts_valid_region(region): url = get_oci_base_url({"oci_region": region}) assert url == f"https://inference.generativeai.{region}.oci.oraclecloud.com" +_NON_COMMERCIAL_REALMS: Final = ( + ("oc2", "us-luke-1", "oraclegovcloud.com"), + ("oc3", "us-gov-ashburn-1", "oraclegovcloud.com"), + ("oc4", "uk-gov-london-1", "oraclegovcloud.uk"), + ("oc19", "eu-frankfurt-2", "oraclecloud.eu"), +) +_UNKNOWN_REGION: Final = "xx-nowhere-1" +_UNKNOWN_REALM_COMPARTMENT: Final = "ocid1.compartment.oc99..aaaaaaaaexample" +_UNKNOWN_REGION_METADATA: Final = '{"realmKey": "OCX", "realmDomainComponent": "example.test", "regionKey": "XNW", "regionIdentifier": "xx-nowhere-1"}' + + +def _compartment(realm): + return f"ocid1.compartment.{realm}..aaaaaaaaexample" + + +def _params(region: str, compartment_id: object = None) -> MappingProxyType[str, object]: + return MappingProxyType({"oci_region": region, "oci_compartment_id": compartment_id}) + + +@pytest.fixture +def without_oci_sdk(monkeypatch): + monkeypatch.setitem(sys.modules, "oci", None) + monkeypatch.setitem(sys.modules, "oci.regions", None) + + +@pytest.fixture +def isolated_region_metadata(monkeypatch, tmp_path): + monkeypatch.delenv("OCI_REGION_METADATA", raising=False) + monkeypatch.delenv("OCI_COMPARTMENT_ID", raising=False) + monkeypatch.setenv("HOME", str(tmp_path)) + return tmp_path + + +def test_realm_table_matches_installed_sdk(): + # Realm domains per the OCI Python SDK's oci.regions_definitions.REALMS (v2.187.0, checked 2026-09-27) + definitions: Final = pytest.importorskip("oci.regions_definitions") + assert ( + MappingProxyType({realm: definitions.REALMS.get(realm) for realm in _OCI_REALM_DOMAINS}) == _OCI_REALM_DOMAINS + ) + + +@pytest.mark.usefixtures("isolated_region_metadata") +@pytest.mark.parametrize(("realm", "region", "second_level_domain"), _NON_COMMERCIAL_REALMS) +def test_get_oci_base_url_resolves_realm_from_region_via_sdk(realm, region, second_level_domain): + pytest.importorskip("oci.regions") + # Realm domains per the OCI Python SDK's oci.regions_definitions (v2.187.0, checked 2026-09-27) + url: Final = get_oci_base_url(_params(region)) + assert url == f"https://inference.generativeai.{region}.oci.{second_level_domain}" + + +@pytest.mark.usefixtures("without_oci_sdk", "isolated_region_metadata") +@pytest.mark.parametrize(("realm", "region", "second_level_domain"), _NON_COMMERCIAL_REALMS) +def test_get_oci_base_url_resolves_realm_from_compartment_ocid(realm, region, second_level_domain): + url: Final = get_oci_base_url(_params(region, _compartment(realm))) + assert url == f"https://inference.generativeai.{region}.oci.{second_level_domain}" + + +@pytest.mark.usefixtures("without_oci_sdk", "isolated_region_metadata") +def test_get_oci_base_url_resolves_realm_from_compartment_env(monkeypatch): + monkeypatch.setenv("OCI_COMPARTMENT_ID", _compartment("oc2")) + url: Final = get_oci_base_url(_params("us-luke-1")) + assert url == "https://inference.generativeai.us-luke-1.oci.oraclegovcloud.com" + + +@pytest.mark.usefixtures("without_oci_sdk", "isolated_region_metadata") +def test_get_oci_base_url_reads_realm_key_case_insensitively(): + url: Final = get_oci_base_url(_params("us-luke-1", _compartment("OC2"))) + assert url == "https://inference.generativeai.us-luke-1.oci.oraclegovcloud.com" + + +@pytest.mark.usefixtures("without_oci_sdk", "isolated_region_metadata") +def test_get_oci_base_url_keeps_commercial_compartment_commercial(): + url: Final = get_oci_base_url(_params("us-chicago-1", _compartment("oc1"))) + assert url == "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com" + + +@pytest.mark.usefixtures("without_oci_sdk", "isolated_region_metadata") +@pytest.mark.parametrize("compartment_id", (None, "not-an-ocid", _UNKNOWN_REALM_COMPARTMENT, 42)) +def test_get_oci_base_url_without_sdk_defaults_to_commercial_when_realm_unknown(compartment_id): + url: Final = get_oci_base_url(_params(_UNKNOWN_REGION, compartment_id)) + assert url == f"https://inference.generativeai.{_UNKNOWN_REGION}.oci.oraclecloud.com" + + +@pytest.mark.usefixtures("without_oci_sdk", "isolated_region_metadata") +def test_get_oci_base_url_compartment_realm_wins_over_region_metadata(monkeypatch): + monkeypatch.setenv( + "OCI_REGION_METADATA", '{"regionIdentifier": "us-luke-1", "realmDomainComponent": "example.test"}' + ) + url: Final = get_oci_base_url(_params("us-luke-1", _compartment("oc2"))) + assert url == "https://inference.generativeai.us-luke-1.oci.oraclegovcloud.com" + + +@pytest.mark.usefixtures("without_oci_sdk", "isolated_region_metadata") +def test_get_oci_base_url_without_sdk_uses_region_metadata_env(monkeypatch): + monkeypatch.setenv("OCI_REGION_METADATA", _UNKNOWN_REGION_METADATA) + url: Final = get_oci_base_url(_params(_UNKNOWN_REGION, _UNKNOWN_REALM_COMPARTMENT)) + assert url == f"https://inference.generativeai.{_UNKNOWN_REGION}.oci.example.test" + + +@pytest.mark.usefixtures("without_oci_sdk", "isolated_region_metadata") +def test_get_oci_base_url_without_sdk_region_metadata_leaves_other_regions_commercial(monkeypatch): + monkeypatch.setenv("OCI_REGION_METADATA", _UNKNOWN_REGION_METADATA) + url: Final = get_oci_base_url(_params("us-chicago-1")) + assert url == "https://inference.generativeai.us-chicago-1.oci.oraclecloud.com" + + +@pytest.mark.usefixtures("without_oci_sdk") +def test_get_oci_base_url_without_sdk_uses_regions_config_file(isolated_region_metadata): + oci_dir: Final = isolated_region_metadata / ".oci" + oci_dir.mkdir() + (oci_dir / "regions-config.json").write_text(f"[{_UNKNOWN_REGION_METADATA}]") + url: Final = get_oci_base_url(_params(_UNKNOWN_REGION)) + assert url == f"https://inference.generativeai.{_UNKNOWN_REGION}.oci.example.test" + + +@pytest.mark.usefixtures("without_oci_sdk") +def test_get_oci_base_url_without_sdk_keeps_valid_regions_config_entries_next_to_a_bad_one(isolated_region_metadata): + oci_dir: Final = isolated_region_metadata / ".oci" + oci_dir.mkdir() + (oci_dir / "regions-config.json").write_text( + f'[{{"regionIdentifier": "us-langley-1"}}, {_UNKNOWN_REGION_METADATA}]' + ) + url: Final = get_oci_base_url(_params(_UNKNOWN_REGION)) + assert url == f"https://inference.generativeai.{_UNKNOWN_REGION}.oci.example.test" + + +@pytest.mark.usefixtures("without_oci_sdk") +@pytest.mark.parametrize("content", (b"\xff\xfe\x00[", b'{"regionIdentifier": "xx-nowhere-1"}', b"not json")) +def test_get_oci_base_url_without_sdk_ignores_unusable_regions_config_file(isolated_region_metadata, content): + oci_dir: Final = isolated_region_metadata / ".oci" + oci_dir.mkdir() + (oci_dir / "regions-config.json").write_bytes(content) + url: Final = get_oci_base_url(_params(_UNKNOWN_REGION)) + assert url == f"https://inference.generativeai.{_UNKNOWN_REGION}.oci.oraclecloud.com" + + +@pytest.mark.usefixtures("without_oci_sdk", "isolated_region_metadata") +@pytest.mark.parametrize( + "metadata", + ( + '{"regionIdentifier": "xx-nowhere-1", "realmDomainComponent": "evil.com/#"}', + '{"regionIdentifier": "xx-nowhere-1", "realmDomainComponent": "-internal"}', + '{"regionIdentifier": "xx-nowhere-1"}', + "not json", + ), +) +def test_get_oci_base_url_without_sdk_ignores_invalid_region_metadata(monkeypatch, metadata): + monkeypatch.setenv("OCI_REGION_METADATA", metadata) + url: Final = get_oci_base_url(_params(_UNKNOWN_REGION)) + assert url == f"https://inference.generativeai.{_UNKNOWN_REGION}.oci.oraclecloud.com" + + +def _fake_oci_regions(endpoint_for=None): + module: Final = types.ModuleType("oci.regions") + if endpoint_for is not None: + module.endpoint_for = endpoint_for + return module + + +@pytest.mark.usefixtures("isolated_region_metadata") +def test_get_oci_base_url_uses_sdk_region_registry_when_realm_unknown(monkeypatch): + endpoint_for: Final = MagicMock( + side_effect=lambda service, region, service_endpoint_template: service_endpoint_template.format( + region=region, secondLevelDomain="example.test" + ) + ) + monkeypatch.setitem(sys.modules, "oci", types.ModuleType("oci")) + monkeypatch.setitem(sys.modules, "oci.regions", _fake_oci_regions(endpoint_for)) + url: Final = get_oci_base_url(_params(_UNKNOWN_REGION, _UNKNOWN_REALM_COMPARTMENT)) + assert url == f"https://inference.generativeai.{_UNKNOWN_REGION}.oci.example.test" + endpoint_for.assert_called_once_with( + "generative_ai_inference", + region=_UNKNOWN_REGION, + service_endpoint_template="https://inference.generativeai.{region}.oci.{secondLevelDomain}", + ) + + +@pytest.mark.usefixtures("isolated_region_metadata") +def test_get_oci_base_url_skips_sdk_region_registry_when_compartment_realm_known(monkeypatch): + def endpoint_for(service, region, service_endpoint_template): + raise AssertionError("registry consulted") + + monkeypatch.setitem(sys.modules, "oci", types.ModuleType("oci")) + monkeypatch.setitem(sys.modules, "oci.regions", _fake_oci_regions(endpoint_for)) + url: Final = get_oci_base_url(_params("us-luke-1", _compartment("oc2"))) + assert url == "https://inference.generativeai.us-luke-1.oci.oraclegovcloud.com" + + +@pytest.mark.usefixtures("isolated_region_metadata") +def test_get_oci_base_url_prefers_sdk_region_registry_over_hand_parsed_metadata(monkeypatch): + def endpoint_for(service, region, service_endpoint_template): + return service_endpoint_template.format(region=region, secondLevelDomain="sdk.test") + + monkeypatch.setitem(sys.modules, "oci", types.ModuleType("oci")) + monkeypatch.setitem(sys.modules, "oci.regions", _fake_oci_regions(endpoint_for)) + monkeypatch.setenv("OCI_REGION_METADATA", _UNKNOWN_REGION_METADATA) + url: Final = get_oci_base_url(_params(_UNKNOWN_REGION)) + assert url == f"https://inference.generativeai.{_UNKNOWN_REGION}.oci.sdk.test" + + +@pytest.mark.usefixtures("isolated_region_metadata") +def test_get_oci_base_url_falls_back_to_metadata_when_sdk_registry_lacks_endpoint_for(monkeypatch): + monkeypatch.setitem(sys.modules, "oci", types.ModuleType("oci")) + monkeypatch.setitem(sys.modules, "oci.regions", _fake_oci_regions()) + monkeypatch.setenv("OCI_REGION_METADATA", _UNKNOWN_REGION_METADATA) + url: Final = get_oci_base_url(_params(_UNKNOWN_REGION)) + assert url == f"https://inference.generativeai.{_UNKNOWN_REGION}.oci.example.test" + + +@pytest.mark.usefixtures("without_oci_sdk", "isolated_region_metadata") +@pytest.mark.parametrize( + ("metadata", "second_level_domain"), + ( + ('{"regionIdentifier": "XX-NOWHERE-1", "realmDomainComponent": "Example.Test"}', "example.test"), + ('{"regionIdentifier": "xx-nowhere-1", "realmDomainComponent": "internal"}', "internal"), + ), +) +def test_get_oci_base_url_without_sdk_normalizes_region_metadata_like_the_sdk( + monkeypatch, metadata, second_level_domain +): + monkeypatch.setenv("OCI_REGION_METADATA", metadata) + url: Final = get_oci_base_url(_params(_UNKNOWN_REGION)) + assert url == f"https://inference.generativeai.{_UNKNOWN_REGION}.oci.{second_level_domain}" + + +@pytest.mark.usefixtures("without_oci_sdk") +def test_get_oci_base_url_without_sdk_tolerates_unresolvable_home(monkeypatch): + def no_passwd_entry(uid): + raise KeyError(uid) + + monkeypatch.delenv("HOME", raising=False) + monkeypatch.setattr("pwd.getpwuid", no_passwd_entry) + url: Final = get_oci_base_url(_params(_UNKNOWN_REGION)) + assert url == f"https://inference.generativeai.{_UNKNOWN_REGION}.oci.oraclecloud.com" + + # --------------------------------------------------------------------------- # validate_oci_environment # --------------------------------------------------------------------------- @@ -247,17 +488,13 @@ def test_sign_with_oci_signer_exception_wrapped(): bad_signer = MagicMock() bad_signer.do_request_sign.side_effect = RuntimeError("signing failed") with pytest.raises(OCIError, match="Failed to sign request"): - sign_with_oci_signer( - {}, {"oci_signer": bad_signer}, {"key": "val"}, "https://example.com" - ) + sign_with_oci_signer({}, {"oci_signer": bad_signer}, {"key": "val"}, "https://example.com") def test_sign_with_oci_signer_success(): signer = MagicMock() signer.do_request_sign.return_value = None - headers, body = sign_with_oci_signer( - {}, {"oci_signer": signer}, {"key": "val"}, "https://example.com" - ) + headers, body = sign_with_oci_signer({}, {"oci_signer": signer}, {"key": "val"}, "https://example.com") assert isinstance(body, bytes) signer.do_request_sign.assert_called_once() @@ -270,9 +507,7 @@ def test_sign_with_oci_signer_success(): def test_sign_oci_request_routes_to_signer(): signer = MagicMock() signer.do_request_sign.return_value = None - headers, body = sign_oci_request( - {}, {"oci_signer": signer}, {}, "https://example.com" - ) + headers, body = sign_oci_request({}, {"oci_signer": signer}, {}, "https://example.com") signer.do_request_sign.assert_called_once() diff --git a/tests/unit/llms/openai/responses/test_openai_responses_transformation.py b/tests/unit/llms/openai/responses/test_openai_responses_transformation.py index 9ade658b94c..7c5616a7ef8 100644 --- a/tests/unit/llms/openai/responses/test_openai_responses_transformation.py +++ b/tests/unit/llms/openai/responses/test_openai_responses_transformation.py @@ -15,6 +15,7 @@ from litellm.llms.openai.responses.transformation import OpenAIResponsesAPIConfi from litellm.llms.openai_like.responses.transformation import OpenAILikeResponsesConfig from litellm.llms.openrouter.responses.transformation import OpenRouterResponsesAPIConfig from litellm.llms.xai.responses.transformation import XAIResponsesAPIConfig +from litellm.responses.litellm_completion_transformation.transformation import LiteLLMCompletionResponsesConfig from litellm.types.llms.openai import ( ImageGenerationPartialImageEvent, OutputTextDeltaEvent, @@ -23,6 +24,7 @@ from litellm.types.llms.openai import ( ResponsesAPIStreamEvents, ) from litellm.types.router import GenericLiteLLMParams +from litellm.types.utils import Choices, Message, ModelResponse _ARTIFACT_FIELD_PATTERN: Final = r'^(?!__.*__$)[^\p{Cc}\p{Cf}\p{Zl}\p{Zp}"\\./[\]]{1,200}$' @@ -946,6 +948,80 @@ class TestOpenAIResponsesAPIConfig: assert norm["input"][1]["type"] == "custom_tool_call" assert "namespace" not in norm["input"][1] + @staticmethod + def _claude_turn_bridged_to_responses_output() -> list: + claude_turn = ModelResponse( + id="chatcmpl-claude", + model="claude-sonnet-4-5", + choices=[ + Choices( + finish_reason="stop", + index=0, + message=Message( + role="assistant", + content="Paris is 22C and sunny.", + reasoning_content="Check Paris first.", + thinking_blocks=[ + {"type": "thinking", "thinking": "Check Paris first.", "signature": "sig-paris"} + ], + ), + ) + ], + ) + bridged = LiteLLMCompletionResponsesConfig.transform_chat_completion_response_to_responses_api_response( + request_input="Weather in Paris?", responses_api_request={}, chat_completion_response=claude_turn + ) + return list(bridged.output) + + @pytest.mark.parametrize("config", [OpenAIResponsesAPIConfig(), AzureOpenAIResponsesAPIConfig()]) + def test_claude_reasoning_minted_by_the_bridge_is_dropped_before_the_history_reaches_openai(self, config): + saved_claude_turn = json.loads( + json.dumps([item.model_dump() for item in self._claude_turn_bridged_to_responses_output()]) + ) + bridge_reasoning = [item for item in saved_claude_turn if item["type"] == "reasoning"] + assert len(bridge_reasoning) == 1 + openai_reasoning = { + "id": "rs_08d3a89dbb92277a006abf04f4266087d0b4eedacd7848f306", + "type": "reasoning", + "summary": [], + "encrypted_content": "gAAAAABo-opaque-openai-blob", + } + history = [ + {"role": "user", "content": "Weather in Paris?"}, + *saved_claude_turn, + openai_reasoning, + {"role": "user", "content": "And Berlin?"}, + ] + + request = config.transform_responses_api_request( + model="gpt-5.6", + input=history, + response_api_optional_request_params={}, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + + outbound = request["input"] + assert len(outbound) == len(history) - 1 + assert [item["id"] for item in outbound if item.get("type") == "reasoning"] == [openai_reasoning["id"]] + assert LiteLLMCompletionResponsesConfig._decode_thinking_blocks_from_input_item(bridge_reasoning[0]) == ( + {"type": "thinking", "thinking": "Check Paris first.", "signature": "sig-paris"}, + ) + + def test_bridge_minted_reasoning_is_dropped_when_handed_back_as_pydantic_output_items(self): + history = [*self._claude_turn_bridged_to_responses_output(), {"role": "user", "content": "And Berlin?"}] + + request = self.config.transform_responses_api_request( + model="gpt-5.6", + input=history, + response_api_optional_request_params={}, + litellm_params=GenericLiteLLMParams(), + headers={}, + ) + + assert len(request["input"]) == len(history) - 1 + assert all(item.get("type") != "reasoning" for item in request["input"]) + class TestAzureResponsesAPIConfig: def setup_method(self): diff --git a/tests/unit/llms/test_oss_decision.py b/tests/unit/llms/test_oss_decision.py new file mode 100644 index 00000000000..05c5d2bbff5 --- /dev/null +++ b/tests/unit/llms/test_oss_decision.py @@ -0,0 +1,60 @@ +from typing import Final + +import pytest + +from litellm.llms.oss_decision import OssDecisionProvider, oss_connection, validate_oss_request + +pytestmark: Final = pytest.mark.parametrize("provider", ["laya", "bespoke"]) + + +@pytest.mark.parametrize( + ("base", "key", "expected_base", "expected_key"), + [ + (None, None, "http://decision.test/root", "oss-env-key"), + ("http://custom.test/", None, "http://custom.test", None), + ("http://custom.test/", "explicit-key", "http://custom.test", "explicit-key"), + ], +) +def test_oss_credentials_stay_with_their_configured_destination( + monkeypatch: pytest.MonkeyPatch, + provider: OssDecisionProvider, + base: str | None, + key: str | None, + expected_base: str, + expected_key: str | None, +) -> None: + monkeypatch.setenv(f"{provider.upper()}_API_BASE", "http://decision.test/root/") + monkeypatch.setenv(f"{provider.upper()}_API_KEY", "oss-env-key") + monkeypatch.setenv("TYPESAFE_API_KEY", "never-send-this") + monkeypatch.setenv("NIMBLE_API_KEY", "never-send-nimble-search-key") + connection: Final = oss_connection(provider, base, key) + assert (connection.api_base, connection.api_key) == (expected_base, expected_key) + assert "key" not in repr(connection) + + +@pytest.mark.parametrize( + "base", + ["", "ftp://laya.test", "http://user:password@laya.test", "https://laya.test?key=x", "http://laya.test/#x"], +) +def test_oss_rejects_ambiguous_server_urls(provider: OssDecisionProvider, base: str) -> None: + with pytest.raises(ValueError, match=provider): + oss_connection(provider, base) + + +def test_oss_missing_server_does_not_fall_back_to_typesafe( + monkeypatch: pytest.MonkeyPatch, provider: OssDecisionProvider +) -> None: + monkeypatch.delenv(f"{provider.upper()}_API_BASE", raising=False) + monkeypatch.setenv("TYPESAFE_API_BASE", "https://typesafe.test") + monkeypatch.setenv("NIMBLE_API_BASE", "https://nimble-search.test") + with pytest.raises(ValueError, match=f"{provider.upper()}_API_BASE"): + oss_connection(provider) + + +def test_oss_request_accepts_the_name_ollama_serves_nimble_under_only_for_bespoke(provider: OssDecisionProvider) -> None: + body: Final = {"model": "nimble"} + if provider == "bespoke": + assert validate_oss_request(provider, body) == "nimble" + return + with pytest.raises(ValueError, match=f"{provider} model must be one of"): + validate_oss_request(provider, body) diff --git a/tests/unit/passthrough/test_passthrough_main.py b/tests/unit/passthrough/test_passthrough_main.py index 82825ec2802..729b03b7df4 100644 --- a/tests/unit/passthrough/test_passthrough_main.py +++ b/tests/unit/passthrough/test_passthrough_main.py @@ -205,8 +205,8 @@ def mock_request(): self.query_params = QueryParams() self.method = method self.request_body = request_body or {} - # Add url attribute that the actual code expects - self.url = "http://localhost:8000/test" + self.url = httpx.URL("http://localhost:8000/test") + self.scope = {"type": "http", "method": method, "path": "/test"} async def body(self) -> bytes: return bytes(json.dumps(self.request_body), "utf-8") diff --git a/tests/unit/proxy/_experimental/mcp_server/test_mcp_logging.py b/tests/unit/proxy/_experimental/mcp_server/test_mcp_logging.py index 41d0e2cb59b..44ba40afdd1 100644 --- a/tests/unit/proxy/_experimental/mcp_server/test_mcp_logging.py +++ b/tests/unit/proxy/_experimental/mcp_server/test_mcp_logging.py @@ -111,7 +111,7 @@ async def test_mcp_cost_tracking(): local_mcp_server_manager = MCPServerManager() with patch( - "litellm.proxy._experimental.mcp_server.mcp_server_manager.MCPClient", + "litellm.proxy._experimental.mcp_server.upstream.MCPClient", mock_client_constructor, ): # Load the server config @@ -244,7 +244,7 @@ async def test_mcp_cost_tracking_per_tool(): local_mcp_server_manager = MCPServerManager() with patch( - "litellm.proxy._experimental.mcp_server.mcp_server_manager.MCPClient", + "litellm.proxy._experimental.mcp_server.upstream.MCPClient", mock_client_constructor, ): # Load the server config with per-tool costs @@ -417,7 +417,7 @@ async def test_mcp_tool_call_hook(): local_mcp_server_manager = MCPServerManager() with patch( - "litellm.proxy._experimental.mcp_server.mcp_server_manager.MCPClient", + "litellm.proxy._experimental.mcp_server.upstream.MCPClient", mock_client_constructor, ): # Load the server config diff --git a/tests/unit/proxy/_experimental/mcp_server/test_mcp_partial_update.py b/tests/unit/proxy/_experimental/mcp_server/test_mcp_partial_update.py index af4f4cbeb17..a3c52dc16b7 100644 --- a/tests/unit/proxy/_experimental/mcp_server/test_mcp_partial_update.py +++ b/tests/unit/proxy/_experimental/mcp_server/test_mcp_partial_update.py @@ -13,6 +13,7 @@ from unittest.mock import AsyncMock, MagicMock import pytest from prisma import Json, models +from fastapi import HTTPException from litellm.proxy._experimental.mcp_server.db import ( create_mcp_server, @@ -35,7 +36,7 @@ def _mock_prisma(): mock_prisma.db.litellm_mcpservertable.update = AsyncMock(return_value=row) mock_prisma.db.litellm_mcpservertable.create = AsyncMock(return_value=row) mock_prisma.db.litellm_mcpservertable.find_first = AsyncMock(return_value=None) - mock_prisma.db.litellm_mcpservertable.find_unique = AsyncMock(return_value=None) + mock_prisma.db.litellm_mcpservertable.find_unique = AsyncMock(return_value=row) tx_client = MagicMock() tx_client.execute_raw = AsyncMock() tx_client.litellm_mcpservertable = mock_prisma.db.litellm_mcpservertable @@ -1141,6 +1142,42 @@ async def test_set_mcp_server_pinned_tools_writes_the_snapshot_and_null_clears_i @pytest.mark.asyncio async def test_set_mcp_server_pinned_tools_on_a_missing_server_writes_nothing(): mock_prisma = _mock_prisma() + mock_prisma.db.litellm_mcpservertable.find_unique.return_value = None assert await set_mcp_server_pinned_tools(mock_prisma, "ghost", None, "admin") is None mock_prisma.db.litellm_mcpservertable.update.assert_not_awaited() + + +@pytest.mark.asyncio +@pytest.mark.parametrize("protocol_only", [False, True]) +async def test_protocol_update_revalidates_current_stored_configuration_before_writing(protocol_only: bool): + prisma = _mock_prisma() + table = prisma.db.litellm_mcpservertable + table.find_unique.return_value = models.LiteLLM_MCPServerTable.model_construct( + server_id="test-server", transport="sse" if protocol_only else "http", + mcp_info={} if protocol_only else {"protocol_version": "2026-07-28"}, env={}, env_vars=[], + ) + payload = UpdateMCPServerRequest.model_validate({ + "server_id": "test-server", + **({"mcp_info": {"protocol_version": "2026-07-28"}} if protocol_only else {"transport": "sse", "url": "https://upstream.example/sse"}), + }) + with pytest.raises(HTTPException) as error: + await update_mcp_server(prisma, payload, "admin") + assert error.value.status_code == 400 + assert "Modern MCP requires HTTP or stdio" in str(error.value.detail) + table.update.assert_not_awaited() + + +@pytest.mark.asyncio +@pytest.mark.parametrize("clear_alias", [False, True]) +async def test_protocol_update_preserves_missing_server_without_writing(clear_alias: bool): + prisma = _mock_prisma() + table = prisma.db.litellm_mcpservertable + table.find_unique.return_value = None + payload = UpdateMCPServerRequest.model_validate({ + "server_id": "missing", "mcp_info": {"protocol_version": "2026-07-28"}, + **({"alias": None} if clear_alias else {}), + }) + result = await update_mcp_server(prisma, payload, "admin") + assert result is None + table.update.assert_not_awaited() diff --git a/tests/unit/proxy/_experimental/mcp_server/test_mcp_server.py b/tests/unit/proxy/_experimental/mcp_server/test_mcp_server.py index f8bf72428aa..d3679506a2f 100644 --- a/tests/unit/proxy/_experimental/mcp_server/test_mcp_server.py +++ b/tests/unit/proxy/_experimental/mcp_server/test_mcp_server.py @@ -71,7 +71,7 @@ async def test_mcp_server_manager_https_server(): return mock_client with patch( - "litellm.proxy._experimental.mcp_server.mcp_server_manager.MCPClient", + "litellm.proxy._experimental.mcp_server.upstream.MCPClient", mock_client_constructor, ): await mcp_server_manager.load_servers_from_config( @@ -179,7 +179,7 @@ async def test_mcp_http_transport_list_tools_mock(): return mock_client with patch( - "litellm.proxy._experimental.mcp_server.mcp_server_manager.MCPClient", + "litellm.proxy._experimental.mcp_server.upstream.MCPClient", mock_client_constructor, ): # Load server config with HTTP transport @@ -256,7 +256,7 @@ async def test_mcp_http_transport_call_tool_mock(): return mock_client with patch( - "litellm.proxy._experimental.mcp_server.mcp_server_manager.MCPClient", + "litellm.proxy._experimental.mcp_server.upstream.MCPClient", mock_client_constructor, ): # Load server config with HTTP transport @@ -322,7 +322,7 @@ async def test_mcp_http_transport_call_tool_error_mock(): return mock_client with patch( - "litellm.proxy._experimental.mcp_server.mcp_server_manager.MCPClient", + "litellm.proxy._experimental.mcp_server.upstream.MCPClient", mock_client_constructor, ): # Load server config with HTTP transport @@ -1093,7 +1093,7 @@ async def test_list_tools_only_returns_allowed_servers(monkeypatch): return mock_client with patch( - "litellm.proxy._experimental.mcp_server.mcp_server_manager.MCPClient", + "litellm.proxy._experimental.mcp_server.upstream.MCPClient", mock_client_constructor, ): # Call list_tools @@ -1390,7 +1390,7 @@ async def test_mcp_server_manager_alias_tool_prefixing(): return mock_client with patch( - "litellm.proxy._experimental.mcp_server.mcp_server_manager.MCPClient", + "litellm.proxy._experimental.mcp_server.upstream.MCPClient", mock_client_constructor, ): # Get tools from server @@ -1450,7 +1450,7 @@ async def test_mcp_server_manager_server_name_tool_prefixing(): return mock_client with patch( - "litellm.proxy._experimental.mcp_server.mcp_server_manager.MCPClient", + "litellm.proxy._experimental.mcp_server.upstream.MCPClient", mock_client_constructor, ): # Get tools from server @@ -1510,7 +1510,7 @@ async def test_mcp_server_manager_server_id_tool_prefixing(): return mock_client with patch( - "litellm.proxy._experimental.mcp_server.mcp_server_manager.MCPClient", + "litellm.proxy._experimental.mcp_server.upstream.MCPClient", mock_client_constructor, ): # Get tools from server @@ -2506,7 +2506,7 @@ async def test_filter_tools_by_allowed_tools_integration(): # Mock the MCPClient constructor with patch( - "litellm.proxy._experimental.mcp_server.mcp_server_manager.MCPClient", + "litellm.proxy._experimental.mcp_server.upstream.MCPClient", mock_client_constructor, ): # Call _get_tools_from_mcp_servers which should apply the filtering @@ -2620,7 +2620,7 @@ async def test_filter_tools_by_disallowed_tools_integration(): # Mock the MCPClient constructor with patch( - "litellm.proxy._experimental.mcp_server.mcp_server_manager.MCPClient", + "litellm.proxy._experimental.mcp_server.upstream.MCPClient", mock_client_constructor, ): # Call _get_tools_from_mcp_servers which should apply the filtering @@ -2722,7 +2722,7 @@ async def test_filter_tools_no_restrictions_integration(): # Mock the MCPClient constructor with patch( - "litellm.proxy._experimental.mcp_server.mcp_server_manager.MCPClient", + "litellm.proxy._experimental.mcp_server.upstream.MCPClient", mock_client_constructor, ): # Call _get_tools_from_mcp_servers which should apply the filtering diff --git a/tests/unit/proxy/_experimental/mcp_server/test_mcp_server_manager.py b/tests/unit/proxy/_experimental/mcp_server/test_mcp_server_manager.py index a900ad50dfb..4b460fc67ae 100644 --- a/tests/unit/proxy/_experimental/mcp_server/test_mcp_server_manager.py +++ b/tests/unit/proxy/_experimental/mcp_server/test_mcp_server_manager.py @@ -1,3 +1,4 @@ +from litellm.proxy._experimental.mcp_server.upstream import resolve_upstream_auth import importlib import asyncio import functools @@ -94,7 +95,7 @@ async def test_manager_sampling_preserves_explicit_headers_without_ambient_conte client.call_tool = AsyncMock(return_value=CallToolResult(content=[])) assert legacy_server.get_active_auth_context() is None with ( - patch("litellm.proxy._experimental.mcp_server.mcp_server_manager.MCPClient", return_value=client) as factory, + patch("litellm.proxy._experimental.mcp_server.upstream.MCPClient", return_value=client) as factory, patch("litellm.proxy._experimental.mcp_server.sampling_handler.handle_sampling_create_message", sampling), ): await MCPServerManager()._call_regular_mcp_tool( @@ -314,8 +315,9 @@ class TestMCPServerManager: with patch.object(manager, "_get_general_settings", return_value={}): assert manager.get_mcp_server_by_id(server.server_id, client_ip="8.8.8.8") is None - async def test_create_mcp_client_stdio(self): + async def test_create_mcp_client_stdio(self, monkeypatch): """Test creating MCP client for stdio transport""" + monkeypatch.setenv("LITELLM_ENABLE_MCP_STDIO", "true") manager = MCPServerManager() stdio_server = MCPServer( @@ -458,11 +460,12 @@ class TestMCPServerManager: assert exc_info.value.status_code == 500 assert "oauth2_id_jag" in str(exc_info.value.detail) - async def test_create_mcp_client_stdio_injects_npm_config_cache(self): + async def test_create_mcp_client_stdio_injects_npm_config_cache(self, monkeypatch): """Test that _create_mcp_client injects NPM_CONFIG_CACHE when not already set, and preserves user-provided NPM_CONFIG_CACHE when present.""" from litellm.constants import MCP_NPM_CACHE_DIR + monkeypatch.setenv("LITELLM_ENABLE_MCP_STDIO", "true") manager = MCPServerManager() # Case 1: NPM_CONFIG_CACHE not set -> should be injected @@ -491,6 +494,173 @@ class TestMCPServerManager: client2 = await manager._create_mcp_client(server_with_cache) assert client2.stdio_config["env"]["NPM_CONFIG_CACHE"] == "/custom/cache" + async def test_create_mcp_client_refuses_to_start_a_stdio_server_while_stdio_is_not_enabled(self, monkeypatch): + monkeypatch.delenv("LITELLM_ENABLE_MCP_STDIO", raising=False) + manager = MCPServerManager() + server = MCPServer( + server_id="stdio-off", + name="stdio_off", + transport=MCPTransport.stdio, + command="python", + args=["server.py"], + ) + + with pytest.raises(HTTPException) as exc_info: + await manager._create_mcp_client(server) + + assert exc_info.value.status_code == 403 + assert "LITELLM_ENABLE_MCP_STDIO=true" in str(exc_info.value.detail) + + @pytest.mark.parametrize( + "listing", + [ + lambda manager, server: manager._get_tools_from_server(server), + lambda manager, server: manager.get_prompts_from_server(server, user_api_key_auth=None), + lambda manager, server: manager.get_resources_from_server(server, user_api_key_auth=None), + lambda manager, server: manager.get_resource_templates_from_server(server, user_api_key_auth=None), + ], + ids=["tools", "prompts", "resources", "resource_templates"], + ) + async def test_listing_skips_a_stdio_server_quietly_while_stdio_is_not_enabled(self, monkeypatch, caplog, listing): + monkeypatch.delenv("LITELLM_ENABLE_MCP_STDIO", raising=False) + manager = MCPServerManager() + server = MCPServer( + server_id="stdio-quiet", + name="stdio_quiet", + transport=MCPTransport.stdio, + command="python", + args=["server.py"], + ) + + with caplog.at_level(logging.DEBUG, logger="LiteLLM"): + items = await listing(manager, server) + + assert items == [] + assert any("stdio_quiet" in r.getMessage() for r in caplog.records if r.levelno == logging.DEBUG) + assert not [r for r in caplog.records if r.levelno >= logging.WARNING] + + async def test_calling_a_tool_on_a_stdio_server_names_the_flag_while_stdio_is_not_enabled(self, monkeypatch): + monkeypatch.delenv("LITELLM_ENABLE_MCP_STDIO", raising=False) + manager = MCPServerManager() + server = MCPServer( + server_id="stdio-call", + name="stdio_call", + alias="stdio_call", + transport=MCPTransport.stdio, + command="python", + args=["server.py"], + ) + manager.registry[server.server_id] = server + + with pytest.raises(HTTPException) as exc_info: + manager._resolve_mcp_server_for_tool_call(server_name="stdio_call", name="echo") + + assert exc_info.value.status_code == 403 + assert "LITELLM_ENABLE_MCP_STDIO=true" in str(exc_info.value.detail) + + async def test_calling_an_unknown_tool_on_an_enabled_stdio_server_is_still_not_found(self, monkeypatch): + monkeypatch.setenv("LITELLM_ENABLE_MCP_STDIO", "true") + manager = MCPServerManager() + server = MCPServer( + server_id="stdio-call", + name="stdio_call", + alias="stdio_call", + transport=MCPTransport.stdio, + command="python", + args=["server.py"], + ) + manager.registry[server.server_id] = server + + with pytest.raises(ValueError, match="Tool echo not found"): + manager._resolve_mcp_server_for_tool_call(server_name="stdio_call", name="echo") + + @pytest.mark.parametrize("flag, routed", [(None, True), ("true", False)]) + async def test_a_prefixed_tool_name_routes_to_its_blocked_stdio_server(self, monkeypatch, flag, routed): + if flag is None: + monkeypatch.delenv("LITELLM_ENABLE_MCP_STDIO", raising=False) + else: + monkeypatch.setenv("LITELLM_ENABLE_MCP_STDIO", flag) + manager = MCPServerManager() + server = MCPServer( + server_id="stdio-route", + name="stdio_route", + alias="stdio_route", + transport=MCPTransport.stdio, + command="python", + args=["server.py"], + ) + manager.registry[server.server_id] = server + + resolved = manager._get_mcp_server_from_tool_name("stdio_route-echo") + + assert (resolved is server) is routed + + async def test_health_check_reports_a_stdio_server_unhealthy_with_the_flag_to_set(self, monkeypatch): + monkeypatch.delenv("LITELLM_ENABLE_MCP_STDIO", raising=False) + manager = MCPServerManager() + server = MCPServer( + server_id="stdio-health", + name="stdio_health", + transport=MCPTransport.stdio, + command="python", + args=["server.py"], + ) + manager.registry[server.server_id] = server + + result = await manager.health_check_server(server.server_id) + + assert result.status == "unhealthy" + assert "LITELLM_ENABLE_MCP_STDIO=true" in (result.health_check_error or "") + + async def test_a_config_stdio_server_stays_registered_and_warns_while_stdio_is_not_enabled( + self, monkeypatch, config_only_mcp_manager_factory, caplog + ): + monkeypatch.delenv("LITELLM_ENABLE_MCP_STDIO", raising=False) + manager = config_only_mcp_manager_factory() + config = {"local_tools": {"transport": MCPTransport.stdio, "command": "python", "args": ["server.py"]}} + + with caplog.at_level(logging.WARNING, logger="LiteLLM"): + await manager.load_servers_from_config(config) + + assert [s.server_name for s in manager.config_mcp_servers.values()] == ["local_tools"] + warnings = [m for m in caplog.messages if "local_tools" in m] + assert len(warnings) == 1 + assert "LITELLM_ENABLE_MCP_STDIO=true" in warnings[0] + + async def test_a_config_stdio_server_loads_without_a_warning_once_stdio_is_enabled( + self, monkeypatch, config_only_mcp_manager_factory, caplog + ): + monkeypatch.setenv("LITELLM_ENABLE_MCP_STDIO", "true") + manager = config_only_mcp_manager_factory() + config = {"local_tools": {"transport": MCPTransport.stdio, "command": "python", "args": ["server.py"]}} + + with caplog.at_level(logging.WARNING, logger="LiteLLM"): + await manager.load_servers_from_config(config) + + assert [s.server_name for s in manager.config_mcp_servers.values()] == ["local_tools"] + assert not [m for m in caplog.messages if "LITELLM_ENABLE_MCP_STDIO" in m] + + async def test_a_db_stdio_server_stays_registered_and_warns_while_stdio_is_not_enabled(self, monkeypatch, caplog): + monkeypatch.delenv("LITELLM_ENABLE_MCP_STDIO", raising=False) + manager = MCPServerManager() + row = LiteLLM_MCPServerTable( + server_id="db-stdio", + alias="db_stdio", + transport=MCPTransport.stdio, + command="python", + args=["server.py"], + created_at=datetime.now(), + updated_at=datetime.now(), + ) + + with caplog.at_level(logging.WARNING, logger="LiteLLM"): + await manager.add_server(row) + await manager.update_server(row) + await manager.update_server(row) + + assert "db-stdio" in manager.registry + assert sum("db_stdio" in m and "LITELLM_ENABLE_MCP_STDIO=true" in m for m in caplog.messages) == 1 + def test_build_stdio_env_only_accepts_x_prefixed_placeholders(self): """Ensure only ${X-*} placeholders are substituted from headers.""" manager = MCPServerManager() @@ -1174,7 +1344,7 @@ class TestMCPServerManager: "ensure_oauth_metadata_discovered", new=ensure_oauth_metadata_discovered, ), - patch("litellm.proxy._experimental.mcp_server.mcp_server_manager.MCPClient"), + patch("litellm.proxy._experimental.mcp_server.upstream.MCPClient"), ): await manager._create_mcp_client(server) @@ -3731,10 +3901,10 @@ class TestMCPServerManager: ) with ( patch( - "litellm.proxy._experimental.mcp_server.mcp_server_manager.resolve_mcp_auth", + "litellm.proxy._experimental.mcp_server.upstream.resolve_mcp_auth", new_callable=AsyncMock, ) as mock_resolve, - patch("litellm.proxy._experimental.mcp_server.mcp_server_manager.MCPClient") as mock_client_cls, + patch("litellm.proxy._experimental.mcp_server.upstream.MCPClient") as mock_client_cls, ): await manager._create_mcp_client(server=server, extra_headers={"Authorization": "Bearer upstream-token"}) mock_resolve.assert_not_awaited() @@ -3784,10 +3954,10 @@ class TestMCPServerManager: ) with ( patch( - "litellm.proxy._experimental.mcp_server.mcp_server_manager.resolve_mcp_auth", + "litellm.proxy._experimental.mcp_server.upstream.resolve_mcp_auth", new_callable=AsyncMock, ) as mock_resolve, - patch("litellm.proxy._experimental.mcp_server.mcp_server_manager.MCPClient") as mock_client_cls, + patch("litellm.proxy._experimental.mcp_server.upstream.MCPClient") as mock_client_cls, ): await manager._create_mcp_client( server=server, @@ -3827,10 +3997,10 @@ class TestMCPServerManager: ) with ( patch( - "litellm.proxy._experimental.mcp_server.mcp_server_manager.resolve_mcp_auth", + "litellm.proxy._experimental.mcp_server.upstream.resolve_mcp_auth", new_callable=AsyncMock, ) as mock_resolve, - patch("litellm.proxy._experimental.mcp_server.mcp_server_manager.MCPClient") as mock_client_cls, + patch("litellm.proxy._experimental.mcp_server.upstream.MCPClient") as mock_client_cls, ): await manager._create_mcp_client( server=server, @@ -9876,7 +10046,8 @@ class TestCreateMcpClientV2Graft: assert exc.value.status_code == 500 assert "credential" in str(exc.value.detail) - async def test_stdio_migrated_auth_type_still_defers_to_v1(self): + async def test_stdio_migrated_auth_type_still_defers_to_v1(self, monkeypatch): + monkeypatch.setenv("LITELLM_ENABLE_MCP_STDIO", "true") client = await MCPServerManager()._create_mcp_client( MCPServer( server_id="stdio-graft", @@ -13537,7 +13708,8 @@ async def test_debug_resolution_matches_final_header_conflict_winner(_mcp_reques "none": NoneConfig(), }[config] try: - auth, remaining = await MCPServerManager()._resolve_v2_auth( + auth, remaining = await resolve_upstream_auth( + root_path="", server=MCPServer( server_id="s", name="s", @@ -13563,7 +13735,10 @@ async def test_debug_resolution_matches_final_header_conflict_winner(_mcp_reques @pytest.mark.asyncio @pytest.mark.parametrize("transport", ["http", "stdio"]) -async def test_debug_reports_legacy_signing_and_non_http_transport(_mcp_request_ctx, transport: Literal["http", "stdio"]) -> None: +async def test_debug_reports_legacy_signing_and_non_http_transport( + _mcp_request_ctx, monkeypatch, transport: Literal["http", "stdio"] +) -> None: + monkeypatch.setenv("LITELLM_ENABLE_MCP_STDIO", "true") from litellm.proxy._experimental.mcp_server.mcp_context import active_mcp_request_ctx_var from starlette.requests import Request @@ -14912,7 +15087,7 @@ async def test_client_sampling_does_not_fill_explicit_context_from_another_ambie try: legacy_server.set_auth_context(UserAPIKeyAuth(user_id="unrelated"), raw_headers={"authorization": "unrelated-credential"}, client_ip="192.0.2.99") with ( - patch("litellm.proxy._experimental.mcp_server.mcp_server_manager.MCPClient") as factory, + patch("litellm.proxy._experimental.mcp_server.upstream.MCPClient") as factory, patch("litellm.proxy._experimental.mcp_server.sampling_handler.handle_sampling_create_message", sampling), ): if legacy_factory: @@ -14991,6 +15166,53 @@ class TestSharedIdentifierPrefixWarning: assert "'shared'" in shared_warnings[0] +@pytest.mark.asyncio +@pytest.mark.parametrize( + "flag,transports,expected_warnings", + [ + (None, ["stdio", "stdio", "stdio"], 1), + (None, ["http", "stdio", "stdio"], 1), + ("true", ["stdio", "stdio", "stdio"], 0), + ], +) +async def test_reload_warns_once_about_a_blocked_stdio_row_that_is_rebuilt_every_time( + monkeypatch, caplog, flag, transports, expected_warnings +): + if flag is None: + monkeypatch.delenv("LITELLM_ENABLE_MCP_STDIO", raising=False) + else: + monkeypatch.setenv("LITELLM_ENABLE_MCP_STDIO", flag) + manager = MCPServerManager() + repository = MagicMock() + + async def build_from_table(table, **_kwargs): + return MCPServer(server_id=table.server_id, name=table.server_name, transport=table.transport) + + with ( + patch( + "litellm.proxy._experimental.mcp_server.mcp_server_manager.MCPServerRepository", + return_value=repository, + ), + patch( + "litellm.proxy.management_endpoints.mcp_management_endpoints.get_prisma_client_or_throw", + return_value=MagicMock(), + ), + patch.object(manager, "build_mcp_server_from_table", new=build_from_table), + patch.object(manager, "_maybe_register_openapi_tools", new=AsyncMock()), + patch.object(manager, "_prime_oauth_metadata_discovery_for_servers"), + caplog.at_level(logging.WARNING, logger="LiteLLM"), + ): + for transport in transports: + row = LiteLLM_MCPServerTable( + server_id="srv-null-ts", server_name="null_ts", transport=transport, command="python", updated_at=None + ) + repository.table.find_many = AsyncMock(return_value=[MagicMock(model_dump=row.model_dump)]) + await manager.reload_servers_from_database() + + assert manager.registry["srv-null-ts"].transport == transports[-1] + assert sum("'null_ts' will not start" in m for m in caplog.messages) == expected_warnings + + @pytest.mark.asyncio @pytest.mark.parametrize("revision", ["auto", "2024-11-05", "2025-03-26", "2025-06-18", "2025-11-25"]) async def test_configured_protocol_reaches_the_upstream_client(config_only_mcp_manager_factory, revision): @@ -15580,3 +15802,57 @@ class TestToolCatalogGuard: proxy_logging_obj=proxy_logging_obj, server=server, ) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("command,args", [(None, []), ("python", None), ("blocked-executable", [])]) +async def test_upstream_preparation_rejects_blocked_or_preserves_incomplete_stdio_config( + monkeypatch: pytest.MonkeyPatch, + command: str | None, + args: list[str] | None, +) -> None: + monkeypatch.setenv("LITELLM_ENABLE_MCP_STDIO", "true") + server: Final = MCPServer(server_id="stdio", name="stdio", transport=MCPTransport.stdio, command=command, args=args) + if command == "blocked-executable": + with pytest.raises(HTTPException) as error: + await MCPServerManager()._create_mcp_client(server) + assert error.value.status_code == 403 + assert "not in the allowlist" in error.value.detail + else: + client: Final = await MCPServerManager()._create_mcp_client(server) + assert client.stdio_config is None + + +@pytest.mark.asyncio +async def test_upstream_preparation_preserves_windows_command_and_caller_environment( + monkeypatch: pytest.MonkeyPatch, +) -> None: + from litellm.constants import MCP_NPM_CACHE_DIR + + monkeypatch.setenv("LITELLM_ENABLE_MCP_STDIO", "true") + environment: Final = {"PEER_USER": "alice"} + server: Final = MCPServer( + server_id="stdio", name="stdio", transport=MCPTransport.stdio, command="python.exe", args=[] + ) + client: Final = await MCPServerManager()._create_mcp_client(server, stdio_env=environment) + assert client.stdio_config == { + "command": "python.exe", + "args": [], + "env": {"PEER_USER": "alice", "NPM_CONFIG_CACHE": MCP_NPM_CACHE_DIR}, + } + assert environment == {"PEER_USER": "alice"} + + +@pytest.mark.asyncio +async def test_upstream_preparation_honors_case_sensitive_extra_command(monkeypatch: pytest.MonkeyPatch) -> None: + from litellm.proxy._experimental.mcp_server import upstream + + monkeypatch.setenv("LITELLM_ENABLE_MCP_STDIO", "true") + monkeypatch.setattr(upstream, "MCP_STDIO_ALLOWED_COMMANDS", frozenset({"CustomRunner"})) + server: Final = MCPServer( + server_id="custom-stdio", name="custom-stdio", transport=MCPTransport.stdio, + command="/opt/tools/CustomRunner", args=[], + ) + client: Final = await MCPServerManager()._create_mcp_client(server) + assert client.stdio_config is not None + assert client.stdio_config["command"] == "/opt/tools/CustomRunner" diff --git a/tests/unit/proxy/_experimental/mcp_server/test_openapi_tool_auth.py b/tests/unit/proxy/_experimental/mcp_server/test_openapi_tool_auth.py index 15d3b67e641..a8ec7be55f0 100644 --- a/tests/unit/proxy/_experimental/mcp_server/test_openapi_tool_auth.py +++ b/tests/unit/proxy/_experimental/mcp_server/test_openapi_tool_auth.py @@ -851,3 +851,28 @@ def test_the_openapi_arm_keeps_the_shared_client_when_no_guard_is_needed(resolve assert not client.client.event_hooks.get("request") finally: _request_resolved_auth_headers.reset(token) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("mode", [MCPAuth.true_passthrough, MCPAuth.oauth_delegate]) +@pytest.mark.parametrize("per_server", [None, "Bearer per-server"]) +async def test_openapi_passthrough_preparation_preserves_credential_precedence( + mode: MCPAuth, per_server: str | None, +) -> None: + from typing import Final + + from litellm.proxy._experimental.mcp_server.mcp_server_manager import MCPServerManager + + server: Final = MCPServer( + server_id="openapi-passthrough", name="openapi-passthrough", transport=MCPTransport.http, + url="https://upstream.example", spec_path="https://upstream.example/openapi.json", auth_type=mode, + ) + headers: Final = {"authorization": "Bearer forwarded", "X-Trace": "trace"} + resolved, remaining = await MCPServerManager().resolve_openapi_upstream_auth( + mcp_server=server, oauth2_headers=None, raw_headers=None, + mcp_auth_header=per_server, user_api_key_auth=UserAPIKeyAuth(user_id="alice"), + forwarded_headers=headers, + ) + assert resolved == {"Authorization": per_server or "Bearer forwarded"} + assert remaining == {"X-Trace": "trace"} + assert headers == {"authorization": "Bearer forwarded", "X-Trace": "trace"} diff --git a/tests/unit/proxy/_experimental/mcp_server/test_operations.py b/tests/unit/proxy/_experimental/mcp_server/test_operations.py index bb900de4f98..b16b27ac919 100644 --- a/tests/unit/proxy/_experimental/mcp_server/test_operations.py +++ b/tests/unit/proxy/_experimental/mcp_server/test_operations.py @@ -136,7 +136,7 @@ async def test_prompt_sampling_receives_explicit_operation_caller_headers_and_ip sampling = AsyncMock() with ( patch.object(operations, "_get_allowed_mcp_servers", AsyncMock(return_value=[upstream])), - patch("litellm.proxy._experimental.mcp_server.mcp_server_manager.MCPClient", return_value=client) as factory, + patch("litellm.proxy._experimental.mcp_server.upstream.MCPClient", return_value=client) as factory, patch("litellm.proxy._experimental.mcp_server.sampling_handler.handle_sampling_create_message", sampling), ): result = await GatewayOperations().execute( diff --git a/tests/unit/proxy/_experimental/mcp_server/test_rest_endpoints.py b/tests/unit/proxy/_experimental/mcp_server/test_rest_endpoints.py index 759014b54c5..680b84469d9 100644 --- a/tests/unit/proxy/_experimental/mcp_server/test_rest_endpoints.py +++ b/tests/unit/proxy/_experimental/mcp_server/test_rest_endpoints.py @@ -3730,6 +3730,10 @@ class TestGetToolsForSingleServer: class TestStdioCommandAllowlist: """Tests for MCP stdio command allowlist validation.""" + @pytest.fixture(autouse=True) + def _stdio_enabled(self, monkeypatch): + monkeypatch.setenv("LITELLM_ENABLE_MCP_STDIO", "true") + def test_allowed_command_passes_validation(self): """npx, uvx, python, etc. should be accepted.""" req = NewMCPServerRequest( diff --git a/tests/unit/proxy/_experimental/mcp_server/test_server_resolution.py b/tests/unit/proxy/_experimental/mcp_server/test_server_resolution.py index f88088a4fd8..853118b8dc2 100644 --- a/tests/unit/proxy/_experimental/mcp_server/test_server_resolution.py +++ b/tests/unit/proxy/_experimental/mcp_server/test_server_resolution.py @@ -10,7 +10,9 @@ from unittest.mock import Mock import pytest from fastapi import HTTPException +from litellm.proxy._experimental.mcp_server.contracts import TargetCatalog from litellm.proxy._experimental.mcp_server.server_resolution import ( + MCPServerTargetCatalog, ResolutionSource, ResolvedMCPServer, authorize_mcp_server, @@ -460,3 +462,94 @@ async def test_missing_alias_does_not_produce_a_resolution() -> None: manager: Final = _manager() assert await resolve_mcp_server("missing", manager=manager, match_name=True) is None manager.name_lookup_spy.assert_called_once_with("missing", None) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("source", ["db", "registry", "temp"]) +@pytest.mark.parametrize("allowed", [False, True]) +async def test_target_catalog_authorizes_canonical_identity(source: ResolutionSource, allowed: bool) -> None: + server: Final = _runtime_server() + manager: Final = _manager( + servers_by_name={"requested-alias": server}, + allowed_server_ids=(server.server_id,) if allowed else ("requested-alias",), + ) + + async def database_lookup(server_id: str) -> LiteLLM_MCPServerTable | None: + return _table_server(server.server_id) + + async def temporary_lookup(server_id: str) -> MCPServer | None: + return server + + catalog: Final[TargetCatalog] = MCPServerTargetCatalog( + manager=manager, + db_lookup=database_lookup if source == "db" else None, + temp_lookup=temporary_lookup if source == "temp" else None, + match_name=True, + ) + operation: Final = catalog.resolve( + "requested-alias", + _auth(), + is_admin_view=False, + not_found_detail={"error": "missing"}, + forbidden_detail={"error": "denied"}, + non_admin_missing="forbidden", + ) + if allowed and source != "temp": + result: Final = await operation + assert result.table.server_id == server.server_id + assert result.source == source + assert result.runtime is (None if source == "db" else server) + else: + with pytest.raises(HTTPException) as error: + await operation + assert (error.value.status_code, error.value.detail) == (403, {"error": "denied"}) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + "admin,missing,status_code", [(True, "forbidden", 404), (False, "forbidden", 403), (False, "not_found", 404)] +) +async def test_target_catalog_preserves_missing_target_policy( + admin: bool, + missing: Literal["forbidden", "not_found"], + status_code: int, +) -> None: + catalog: Final[TargetCatalog] = MCPServerTargetCatalog(manager=_manager()) + with pytest.raises(HTTPException) as error: + await catalog.resolve( + "missing", + _auth(), + is_admin_view=admin, + not_found_detail={"error": "missing"}, + forbidden_detail={"error": "denied"}, + non_admin_missing=missing, + ) + assert error.value.status_code == status_code + assert error.value.detail == {"error": "missing" if status_code == 404 else "denied"} + + +@pytest.mark.asyncio +async def test_target_catalog_does_not_reuse_admin_authorization_for_another_caller() -> None: + server: Final = _runtime_server() + manager: Final = _manager(servers_by_id={server.server_id: server}) + catalog: Final[TargetCatalog] = MCPServerTargetCatalog(manager=manager) + admin: Final = await catalog.resolve( + server.server_id, + UserAPIKeyAuth(user_id="admin"), + is_admin_view=True, + not_found_detail={"error": "missing"}, + forbidden_detail={"error": "denied"}, + non_admin_missing="forbidden", + ) + assert admin.runtime is server + with pytest.raises(HTTPException) as error: + await catalog.resolve( + server.server_id, + _auth(), + is_admin_view=False, + not_found_detail={"error": "missing"}, + forbidden_detail={"error": "denied"}, + non_admin_missing="forbidden", + ) + assert (error.value.status_code, error.value.detail) == (403, {"error": "denied"}) + manager.allowed_servers_spy.assert_called_once_with(_auth()) diff --git a/tests/unit/proxy/auth/test_auth_utils.py b/tests/unit/proxy/auth/test_auth_utils.py index 83ac56c4c85..bc4a6e0155d 100644 --- a/tests/unit/proxy/auth/test_auth_utils.py +++ b/tests/unit/proxy/auth/test_auth_utils.py @@ -8,7 +8,7 @@ from typing import Optional from unittest.mock import MagicMock, patch import pytest -from fastapi import Request +from fastapi import HTTPException, Request from litellm.proxy._types import UserAPIKeyAuth from litellm.proxy.auth.auth_utils import ( @@ -463,6 +463,30 @@ def test_get_model_from_request_no_request_extracts_model(): ) +@pytest.mark.parametrize("provider,model", [ + ("laya", "english"), ("laya", "multilingual"), ("laya", "typed-decisions"), + ("bespoke", "nimble-latest"), ("bespoke", "bespokelabs/Bespoke-Nimble-9B"), +]) +@pytest.mark.parametrize("suffix", ["", "/"]) +def test_oss_native_model_uses_the_classifier_permission_identity(provider: str, model: str, suffix: str) -> None: + assert get_model_from_request( + request_data={"model": model}, route=f"/{provider}/v1/systemone{suffix}" + ) == f"{provider}/{model}" + + +@pytest.mark.parametrize("provider", ["laya", "bespoke"]) +@pytest.mark.parametrize("model", [None, "", "auto", "laya/english", "bespoke/nimble-latest", "unknown", ["english"], 7]) +def test_oss_native_model_cannot_implicitly_select_an_unauthorized_checkpoint(provider: str, model: object) -> None: + with pytest.raises(HTTPException) as denied: + get_model_from_request(request_data={"model": model}, route=f"/{provider}/v1/systemone") + assert denied.value.status_code == 400 + + +def test_laya_model_normalization_does_not_change_other_provider_routes() -> None: + assert get_model_from_request(request_data={"model": "jev-latest"}, route="/typesafe/v1/systemone") == "jev-latest" + assert get_model_from_request(request_data={}, route="/laya/health") is None + + def _cache_prediction_router(): from litellm.router import Router diff --git a/tests/unit/proxy/auth/test_authorization.py b/tests/unit/proxy/auth/test_authorization.py new file mode 100644 index 00000000000..7d1548dd828 --- /dev/null +++ b/tests/unit/proxy/auth/test_authorization.py @@ -0,0 +1,29 @@ +from typing import Final + +import pytest + +from litellm.proxy._types import UserAPIKeyAuth +from litellm.proxy.auth.authorization import OwnedRows, resolve_owned_read_scope, resolve_trace_read_scope + + +@pytest.mark.asyncio +@pytest.mark.parametrize("token", (None, "key")) +async def test_team_membership_or_key_without_user_does_not_grant_log_access(token: str | None) -> None: + async def unexpected_lookup() -> tuple[str, ...]: + pytest.fail("Identity-less callers cannot consult team permissions") + + assert await resolve_trace_read_scope(UserAPIKeyAuth(team_id="team", token=token), unexpected_lookup) is None + + +@pytest.mark.asyncio +@pytest.mark.parametrize("token", (None, "key")) +@pytest.mark.parametrize("lookup_fails", (False, True)) +async def test_trace_reads_share_user_and_team_scope_regardless_of_key(token: str | None, lookup_fails: bool) -> None: + async def lookup() -> tuple[str, ...]: + if lookup_fails: + raise RuntimeError("team lookup failed") + return ("permitted",) + + expected: Final = OwnedRows("caller", () if lookup_fails else ("permitted",)) + assert await resolve_owned_read_scope("caller", lookup) == expected + assert await resolve_trace_read_scope(UserAPIKeyAuth(user_id="caller", token=token), lookup) == expected diff --git a/tests/unit/proxy/auth/test_route_checks.py b/tests/unit/proxy/auth/test_route_checks.py index 6503e5bf50e..6b1d8bc3a2c 100644 --- a/tests/unit/proxy/auth/test_route_checks.py +++ b/tests/unit/proxy/auth/test_route_checks.py @@ -4425,3 +4425,19 @@ def test_legacy_sse_respects_virtual_key_route_permissions(route: str, route_gro request_data={}, ) assert RouteChecks.is_virtual_key_allowed_to_call_route(route=route, valid_token=token, request=request) + + +@pytest.mark.parametrize( + "route", + ("/v1/traces", "/v1/traces/trace-id", "/v1/traces/trace-id/spans/span-id", + "/v1/traces/trace-id/spans/span-id/error"), +) +def test_non_admin_trace_reads_reach_endpoint_visibility_checks(route: str) -> None: + user_role: Final = LitellmUserRoles.INTERNAL_USER + user: Final = LiteLLM_UserTable(user_id="reader", user_role=user_role.value) + auth: Final = UserAPIKeyAuth(user_id="reader", user_role=user_role) + request: Final = Request({"type": "http", "method": "GET", "query_string": b""}) + assert RouteChecks.is_llm_api_route(route) + RouteChecks.non_proxy_admin_allowed_routes_check( + user_obj=user, _user_role=user_role.value, route=route, request=request, valid_token=auth, request_data={} + ) diff --git a/tests/unit/proxy/auth/test_user_api_key_auth_request_flow.py b/tests/unit/proxy/auth/test_user_api_key_auth_request_flow.py index 781d0a13bfd..a7219ac059b 100644 --- a/tests/unit/proxy/auth/test_user_api_key_auth_request_flow.py +++ b/tests/unit/proxy/auth/test_user_api_key_auth_request_flow.py @@ -2020,6 +2020,454 @@ async def test_auto_register_binds_api_key_to_token_hash(): assert result.end_user_id == "validated-end-user" +def _auto_register_patches(*, plaintext_key: str | None = "sk-minted-plaintext"): + from litellm.proxy.auth.auth_method import AuthMethod + from litellm.proxy.auth.resolvers.models import CredentialRef + from litellm.proxy.auth.resolvers.store import IdentityStore + from litellm.proxy.proxy_server import hash_token + + resolved_key = UserAPIKeyAuth( + token="existing-hash" if plaintext_key is None else hash_token(plaintext_key), + user_id="validated-user", + team_id="validated-team", + org_id="key-own-org", + ) + principal = IdentityStore._principal_from_key( + resolved_key, + auth_method=AuthMethod.API_KEY, + credential_ref=CredentialRef(token_id=resolved_key.token), + ) + return ( + patch( + "litellm.proxy.management_endpoints.key_management_endpoints.generate_key_helper_fn", + new_callable=AsyncMock, + return_value={"token": plaintext_key}, + ), + patch( + "litellm.proxy.auth.resolvers.store.IdentityStore.resolve", + new_callable=AsyncMock, + return_value=principal, + ), + ) + + +def _auto_register_kwargs(prisma_client, user_api_key_cache, jwt_handler, **over): + kwargs = { + "virtual_key_claim_field": "sub", + "claim_value": "validated-user", + "jwt_handler": jwt_handler, + "prisma_client": prisma_client, + "user_api_key_cache": user_api_key_cache, + "parent_otel_span": None, + "proxy_logging_obj": MagicMock(), + "cache_key": "jwt_key_mapping:sub:validated-user", + "team_id": "validated-team", + "user_id": "validated-user", + "org_id": "jwt-org", + "end_user_id": "validated-end-user", + } + kwargs.update(over) + return kwargs + + +@pytest.mark.asyncio +async def test_auto_register_map_existing_key_reuses_users_key_but_never_an_auto_registered_one(): + from litellm.proxy.auth.user_api_key_auth import _auto_register_jwt_mapping + + prisma_client = MagicMock() + prisma_client.db.litellm_verificationtoken.find_many = AsyncMock( + return_value=[ + {"token": "auto-registered-hash", "metadata": {"auto_registered": True}}, + {"token": "existing-hash", "metadata": {}}, + ] + ) + prisma_client.db.litellm_jwtkeymapping.create = AsyncMock() + + user_api_key_cache = MagicMock() + user_api_key_cache.async_set_cache = AsyncMock() + + jwt_handler = MagicMock() + jwt_handler.litellm_jwtauth = LiteLLM_JWTAuth( + user_id_jwt_field="sub", + auto_register_map_existing_key=True, + virtual_key_mapping_cache_ttl=300, + ) + + generate_patch, resolve_patch = _auto_register_patches(plaintext_key=None) + with generate_patch as generate_key, resolve_patch: + result = await _auto_register_jwt_mapping( + **_auto_register_kwargs(prisma_client, user_api_key_cache, jwt_handler) + ) + + generate_key.assert_not_awaited() + + create_data = prisma_client.db.litellm_jwtkeymapping.create.await_args.kwargs["data"] + assert create_data["token"] == "existing-hash" + assert create_data["created_by"] == "auto_register" + assert user_api_key_cache.async_set_cache.await_args.kwargs["value"] == "existing-hash" + assert result is not None + assert result.token == "existing-hash" + assert result.api_key == "existing-hash" + assert result.org_id == "key-own-org" + + +@pytest.mark.asyncio +async def test_auto_register_map_existing_key_mints_when_user_has_no_key(): + from litellm.proxy.auth.user_api_key_auth import _auto_register_jwt_mapping + from litellm.proxy.proxy_server import hash_token + + prisma_client = MagicMock() + prisma_client.db.litellm_verificationtoken.find_many = AsyncMock(return_value=[]) + prisma_client.db.litellm_jwtkeymapping.create = AsyncMock() + + user_api_key_cache = MagicMock() + user_api_key_cache.async_set_cache = AsyncMock() + + jwt_handler = MagicMock() + jwt_handler.litellm_jwtauth = LiteLLM_JWTAuth( + user_id_jwt_field="sub", + auto_register_map_existing_key=True, + virtual_key_mapping_cache_ttl=300, + ) + + generate_patch, resolve_patch = _auto_register_patches() + with generate_patch as generate_key, resolve_patch: + result = await _auto_register_jwt_mapping( + **_auto_register_kwargs(prisma_client, user_api_key_cache, jwt_handler) + ) + + generate_key.assert_awaited_once() + create_data = prisma_client.db.litellm_jwtkeymapping.create.await_args.kwargs["data"] + assert create_data["token"] == hash_token("sk-minted-plaintext") + assert result is not None + assert result.token == hash_token("sk-minted-plaintext") + + +@pytest.mark.asyncio +async def test_auto_register_default_never_looks_up_existing_keys(): + from litellm.proxy.auth.user_api_key_auth import _auto_register_jwt_mapping + + prisma_client = MagicMock() + prisma_client.db.litellm_verificationtoken.find_many = AsyncMock( + return_value=[{"token": "existing-hash", "metadata": {}}] + ) + prisma_client.db.litellm_jwtkeymapping.create = AsyncMock() + + user_api_key_cache = MagicMock() + user_api_key_cache.async_set_cache = AsyncMock() + + jwt_handler = MagicMock() + jwt_handler.litellm_jwtauth = LiteLLM_JWTAuth(user_id_jwt_field="sub", virtual_key_mapping_cache_ttl=300) + + generate_patch, resolve_patch = _auto_register_patches() + with generate_patch as generate_key, resolve_patch: + await _auto_register_jwt_mapping(**_auto_register_kwargs(prisma_client, user_api_key_cache, jwt_handler)) + + prisma_client.db.litellm_verificationtoken.find_many.assert_not_awaited() + generate_key.assert_awaited_once() + + +@pytest.mark.asyncio +async def test_auto_register_map_existing_key_race_loser_keeps_reused_key(): + from litellm.proxy.auth.user_api_key_auth import _auto_register_jwt_mapping + + prisma_client = MagicMock() + prisma_client.db.litellm_verificationtoken.find_many = AsyncMock( + return_value=[{"token": "existing-hash", "metadata": {}}] + ) + prisma_client.db.litellm_verificationtoken.delete = AsyncMock() + prisma_client.db.litellm_jwtkeymapping.create = AsyncMock(side_effect=Exception("Unique constraint failed (P2002)")) + + user_api_key_cache = MagicMock() + user_api_key_cache.async_set_cache = AsyncMock() + + jwt_handler = MagicMock() + jwt_handler.litellm_jwtauth = LiteLLM_JWTAuth( + user_id_jwt_field="sub", + auto_register_map_existing_key=True, + virtual_key_mapping_cache_ttl=300, + ) + + generate_patch, resolve_patch = _auto_register_patches(plaintext_key=None) + with ( + generate_patch, + resolve_patch, + patch( + "litellm.proxy.auth.user_api_key_auth.get_jwt_key_mapping_object", + new_callable=AsyncMock, + return_value="winner-hash", + ), + ): + result = await _auto_register_jwt_mapping( + **_auto_register_kwargs(prisma_client, user_api_key_cache, jwt_handler) + ) + + assert result is not None + assert result.org_id == "key-own-org" + prisma_client.db.litellm_verificationtoken.delete.assert_not_awaited() + assert user_api_key_cache.async_set_cache.await_args.kwargs["value"] == "winner-hash" + + +@pytest.mark.asyncio +async def test_auto_register_map_existing_key_user_id_none_mints(): + from litellm.proxy.auth.user_api_key_auth import _auto_register_jwt_mapping + + prisma_client = MagicMock() + prisma_client.db.litellm_verificationtoken.find_many = AsyncMock( + return_value=[{"token": "existing-hash", "metadata": {}}] + ) + prisma_client.db.litellm_jwtkeymapping.create = AsyncMock() + + user_api_key_cache = MagicMock() + user_api_key_cache.async_set_cache = AsyncMock() + + jwt_handler = MagicMock() + jwt_handler.litellm_jwtauth = LiteLLM_JWTAuth( + user_id_jwt_field="sub", + auto_register_map_existing_key=True, + virtual_key_mapping_cache_ttl=300, + ) + + generate_patch, resolve_patch = _auto_register_patches() + with generate_patch as generate_key, resolve_patch: + await _auto_register_jwt_mapping( + **_auto_register_kwargs(prisma_client, user_api_key_cache, jwt_handler, user_id=None) + ) + + prisma_client.db.litellm_verificationtoken.find_many.assert_not_awaited() + generate_key.assert_awaited_once() + + +@pytest.mark.asyncio +async def test_auto_register_map_existing_key_reuses_when_the_user_was_matched_by_a_fallback_lookup(): + from litellm.proxy.auth.user_api_key_auth import _auto_register_jwt_mapping + + prisma_client = MagicMock() + prisma_client.db.litellm_verificationtoken.find_many = AsyncMock( + return_value=[{"token": "existing-hash", "metadata": {}}] + ) + prisma_client.db.litellm_jwtkeymapping.create = AsyncMock() + + user_api_key_cache = MagicMock() + user_api_key_cache.async_set_cache = AsyncMock() + + jwt_handler = MagicMock() + jwt_handler.litellm_jwtauth = LiteLLM_JWTAuth( + user_id_jwt_field="sub", + user_email_jwt_field="email", + auto_register_map_existing_key=True, + virtual_key_mapping_cache_ttl=300, + ) + + generate_patch, resolve_patch = _auto_register_patches(plaintext_key=None) + with generate_patch as generate_key, resolve_patch: + await _auto_register_jwt_mapping( + **_auto_register_kwargs( + prisma_client, + user_api_key_cache, + jwt_handler, + claim_value="idp-subject-not-the-db-user-id", + cache_key="jwt_key_mapping:sub:idp-subject-not-the-db-user-id", + ) + ) + + generate_key.assert_not_awaited() + assert prisma_client.db.litellm_jwtkeymapping.create.await_args.kwargs["data"]["token"] == "existing-hash" + + +@pytest.mark.asyncio +async def test_auto_register_map_existing_key_mints_when_the_claim_is_not_a_user_identity_field(): + from litellm.proxy.auth.user_api_key_auth import _auto_register_jwt_mapping + from litellm.proxy.proxy_server import hash_token + + prisma_client = MagicMock() + prisma_client.db.litellm_verificationtoken.find_many = AsyncMock( + return_value=[{"token": "existing-hash", "metadata": {}}] + ) + prisma_client.db.litellm_jwtkeymapping.create = AsyncMock() + + user_api_key_cache = MagicMock() + user_api_key_cache.async_set_cache = AsyncMock() + + jwt_handler = MagicMock() + jwt_handler.litellm_jwtauth = LiteLLM_JWTAuth( + user_id_jwt_field="sub", + auto_register_map_existing_key=True, + virtual_key_mapping_cache_ttl=300, + ) + + generate_patch, resolve_patch = _auto_register_patches() + with generate_patch as generate_key, resolve_patch: + await _auto_register_jwt_mapping( + **_auto_register_kwargs( + prisma_client, + user_api_key_cache, + jwt_handler, + virtual_key_claim_field="azp", + claim_value="shared-client-app", + cache_key="jwt_key_mapping:azp:shared-client-app", + ) + ) + + prisma_client.db.litellm_verificationtoken.find_many.assert_not_awaited() + generate_key.assert_awaited_once() + assert prisma_client.db.litellm_jwtkeymapping.create.await_args.kwargs["data"]["token"] == hash_token( + "sk-minted-plaintext" + ) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("issuer_user_id_field", "expect_reuse"), + [("uid", False), (None, True)], +) +async def test_auto_register_map_existing_key_uses_the_issuers_own_user_field_over_the_global_one( + issuer_user_id_field, expect_reuse +): + from litellm.proxy._types import JWTIssuerConfig + from litellm.proxy.auth.user_api_key_auth import _auto_register_jwt_mapping + from litellm.proxy.proxy_server import hash_token + + prisma_client = MagicMock() + prisma_client.db.litellm_verificationtoken.find_many = AsyncMock( + return_value=[{"token": "existing-hash", "metadata": {}}] + ) + prisma_client.db.litellm_jwtkeymapping.create = AsyncMock() + + user_api_key_cache = MagicMock() + user_api_key_cache.async_set_cache = AsyncMock() + + jwt_handler = MagicMock() + jwt_handler.litellm_jwtauth = LiteLLM_JWTAuth( + user_id_jwt_field="sub", + auto_register_map_existing_key=True, + virtual_key_mapping_cache_ttl=300, + issuers=[ + JWTIssuerConfig( + issuer="https://idp.example.com", audience="litellm", user_id_jwt_field=issuer_user_id_field + ) + ], + ) + + generate_patch, resolve_patch = _auto_register_patches() + with generate_patch as generate_key, resolve_patch: + await _auto_register_jwt_mapping( + **_auto_register_kwargs( + prisma_client, user_api_key_cache, jwt_handler, jwt_issuer="https://idp.example.com" + ) + ) + + mapped_token = prisma_client.db.litellm_jwtkeymapping.create.await_args.kwargs["data"]["token"] + assert mapped_token == ("existing-hash" if expect_reuse else hash_token("sk-minted-plaintext")) + assert generate_key.await_count == (0 if expect_reuse else 1) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("map_existing_key", "master_key", "reused_key_models", "expect_denied"), + [ + (True, "sk-master", ["some-other-model"], True), + (True, "sk-master", [], False), + (False, "sk-master", ["some-other-model"], False), + (True, None, ["some-other-model"], False), + ], +) +async def test_auto_register_map_existing_key_first_request_runs_key_checks( + map_existing_key: bool, master_key: str | None, reused_key_models: list[str], expect_denied: bool +) -> None: + jwt_token = "eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ1c2VyMSJ9.signature" + user_api_key_cache = DualCache() + prisma_client = MagicMock() + jwt_handler = MagicMock() + jwt_handler.is_jwt.return_value = True + jwt_handler.auth_jwt = AsyncMock(return_value={"sub": "user1"}) + jwt_handler.litellm_jwtauth = LiteLLM_JWTAuth( + virtual_key_claim_field="sub", + virtual_key_mapping_cache_ttl=300, + auto_register_map_existing_key=map_existing_key, + ) + reused_key = UserAPIKeyAuth( + token="hashed-existing-key", + api_key="hashed-existing-key", + user_id="validated-user", + team_id="validated-team", + models=reused_key_models, + ) + mock_jwt_result = { + "is_proxy_admin": False, + "team_object": None, + "user_object": LiteLLM_UserTable(user_id="validated-user", user_role="internal_user"), + "end_user_object": None, + "org_object": None, + "token": jwt_token, + "team_id": "validated-team", + "user_id": "validated-user", + "user_email": None, + "end_user_id": None, + "org_id": None, + "team_membership": None, + "jwt_claims": {"sub": "user1"}, + } + + mock_request = MagicMock() + mock_request.url.path = "/v1/chat/completions" + mock_request.method = "POST" + mock_request.headers = {"authorization": f"Bearer {jwt_token}"} + mock_request.query_params = {} + mock_request.state = SimpleNamespace() + + with ( + patch("litellm.proxy.proxy_server.general_settings", {"enable_jwt_auth": True}), + patch("litellm.proxy.proxy_server.premium_user", True), + patch("litellm.proxy.proxy_server.master_key", master_key), + patch("litellm.proxy.proxy_server.prisma_client", prisma_client), + patch("litellm.proxy.proxy_server.user_api_key_cache", user_api_key_cache), + patch( + "litellm.proxy.proxy_server.proxy_logging_obj", + MagicMock(post_call_failure_hook=AsyncMock(return_value=None)), + ), + patch("litellm.proxy.proxy_server.jwt_handler", jwt_handler), + patch( + "litellm.proxy.auth.user_api_key_auth._resolve_jwt_to_virtual_key", + new_callable=AsyncMock, + return_value=_PendingAutoRegister( + claim_field="sub", + claim_value="user1", + cache_key="jwt_key_mapping:sub:user1", + ), + ), + patch( + "litellm.proxy.auth.user_api_key_auth.JWTAuthManager.auth_builder", + new_callable=AsyncMock, + return_value=mock_jwt_result, + ), + patch( + "litellm.proxy.auth.user_api_key_auth._auto_register_jwt_mapping", + new_callable=AsyncMock, + return_value=reused_key, + ), + ): + call = _user_api_key_auth_builder( + request=mock_request, + api_key=jwt_token, + azure_api_key_header="", + anthropic_api_key_header=None, + google_ai_studio_api_key_header=None, + azure_apim_header=None, + request_data={"model": "gpt-4o-mini"}, + ) + if expect_denied: + with pytest.raises(ProxyException, match="not available for this API key"): + await call + return + result = await call + + assert result.api_key == "hashed-existing-key" + assert result.user_id == "validated-user" + assert result.team_id == "validated-team" + assert result.models == reused_key_models + + @pytest.mark.asyncio @pytest.mark.parametrize("active", [True, False]) async def test_auto_register_first_request_propagates_user_email(active: bool) -> None: diff --git a/tests/unit/proxy/common_utils/test_http_parsing_utils.py b/tests/unit/proxy/common_utils/test_http_parsing_utils.py index fd747d5a6f2..f00be0f8a65 100644 --- a/tests/unit/proxy/common_utils/test_http_parsing_utils.py +++ b/tests/unit/proxy/common_utils/test_http_parsing_utils.py @@ -1354,7 +1354,7 @@ async def test_auth_body_read_and_trace_handler_leave_stream_for_receiver_limit( store: Final = MagicMock() store.insert_spans = AsyncMock() context: Final = await tracing_endpoints.provide_trace_access( - auth=UserAPIKeyAuth(token="key", team_id="team"), tracing=TraceReceiver(store) + auth=UserAPIKeyAuth(token="key", team_id="team"), tracing=TraceReceiver(store), log_team_lookup=AsyncMock() ) parsed, parse_error = await _read_request_body_deferring_parse_failure(request) diff --git a/tests/unit/proxy/common_utils/test_registry_read_through.py b/tests/unit/proxy/common_utils/test_registry_read_through.py index 9e20386bf3d..5713f7dfaa7 100644 --- a/tests/unit/proxy/common_utils/test_registry_read_through.py +++ b/tests/unit/proxy/common_utils/test_registry_read_through.py @@ -1,10 +1,17 @@ import asyncio -from typing import Final +from typing import TYPE_CHECKING, Final import pytest from litellm.proxy.common_utils.registry_read_through import RegistryReadThrough +if TYPE_CHECKING: + from litellm.proxy.agent_endpoints.agent_registry import AgentRegistry + + +def nothing_loaded(_key: str) -> bool: + return False + class ResyncSpy: def __init__(self, found: bool = True, error: Exception | None = None) -> None: @@ -22,7 +29,7 @@ class ResyncSpy: @pytest.mark.asyncio async def test_attempt_returns_true_when_resync_finds_object(): spy: Final = ResyncSpy(found=True) - read_through: Final = RegistryReadThrough(resync=spy) + read_through: Final = RegistryReadThrough(resync=spy, is_loaded=nothing_loaded) assert await read_through.attempt("new-model") is True assert spy.calls == ["new-model"] @@ -31,7 +38,7 @@ async def test_attempt_returns_true_when_resync_finds_object(): @pytest.mark.asyncio async def test_attempt_found_key_is_not_negative_cached(): spy: Final = ResyncSpy(found=True) - read_through: Final = RegistryReadThrough(resync=spy) + read_through: Final = RegistryReadThrough(resync=spy, is_loaded=nothing_loaded) assert await read_through.attempt("new-model") is True assert await read_through.attempt("new-model") is True @@ -41,7 +48,7 @@ async def test_attempt_found_key_is_not_negative_cached(): @pytest.mark.asyncio async def test_missing_key_is_negative_cached_within_ttl(): spy: Final = ResyncSpy(found=False) - read_through: Final = RegistryReadThrough(resync=spy, miss_ttl_seconds=60.0) + read_through: Final = RegistryReadThrough(resync=spy, is_loaded=nothing_loaded, miss_ttl_seconds=60.0) assert await read_through.attempt("ghost-model") is False assert await read_through.attempt("ghost-model") is False @@ -51,7 +58,7 @@ async def test_missing_key_is_negative_cached_within_ttl(): @pytest.mark.asyncio async def test_negative_cache_expires_and_resync_runs_again(): spy: Final = ResyncSpy(found=False) - read_through: Final = RegistryReadThrough(resync=spy, miss_ttl_seconds=0.05) + read_through: Final = RegistryReadThrough(resync=spy, is_loaded=nothing_loaded, miss_ttl_seconds=0.05) assert await read_through.attempt("ghost-model") is False await asyncio.sleep(0.1) @@ -62,7 +69,7 @@ async def test_negative_cache_expires_and_resync_runs_again(): @pytest.mark.asyncio async def test_resync_exception_returns_false_without_negative_caching(): spy: Final = ResyncSpy(error=RuntimeError("db down")) - read_through: Final = RegistryReadThrough(resync=spy) + read_through: Final = RegistryReadThrough(resync=spy, is_loaded=nothing_loaded) assert await read_through.attempt("new-model") is False assert await read_through.attempt("new-model") is False @@ -77,7 +84,7 @@ async def test_concurrent_attempts_for_missing_key_resync_once(): return await super().__call__(key) spy: Final = SlowResyncSpy(found=False) - read_through: Final = RegistryReadThrough(resync=spy, miss_ttl_seconds=60.0) + read_through: Final = RegistryReadThrough(resync=spy, is_loaded=nothing_loaded, miss_ttl_seconds=60.0) results: Final = await asyncio.gather(*(read_through.attempt("ghost-model") for _ in range(5))) assert results == [False] * 5 @@ -87,7 +94,7 @@ async def test_concurrent_attempts_for_missing_key_resync_once(): @pytest.mark.asyncio async def test_distinct_keys_do_not_share_negative_cache(): spy: Final = ResyncSpy(found=False) - read_through: Final = RegistryReadThrough(resync=spy, miss_ttl_seconds=60.0) + read_through: Final = RegistryReadThrough(resync=spy, is_loaded=nothing_loaded, miss_ttl_seconds=60.0) assert await read_through.attempt("ghost-a") is False assert await read_through.attempt("ghost-b") is False @@ -98,7 +105,11 @@ async def test_distinct_keys_do_not_share_negative_cache(): async def test_resync_budget_exhausted_blocks_resync_without_negative_caching(): spy: Final = ResyncSpy(found=False) read_through: Final = RegistryReadThrough( - resync=spy, miss_ttl_seconds=60.0, max_resyncs_per_window=2, resync_window_seconds=60.0 + resync=spy, + is_loaded=nothing_loaded, + miss_ttl_seconds=60.0, + max_resyncs_per_window=2, + resync_window_seconds=60.0, ) assert await read_through.attempt("ghost-a") is False @@ -108,10 +119,48 @@ async def test_resync_budget_exhausted_blocks_resync_without_negative_caching(): assert read_through._recent_misses.get_cache("ghost-c") is None +@pytest.mark.asyncio +async def test_requests_queued_behind_a_successful_resync_spend_no_budget(): + from unittest.mock import AsyncMock, call + + entered: Final = asyncio.Event() + release: Final = asyncio.Event() + new_model_loaded: Final = asyncio.Event() + + async def gated_load(key: str) -> bool: + entered.set() + await release.wait() + if key == "new-model": + new_model_loaded.set() + return True + + def is_loaded(key: str) -> bool: + return key == "new-model" and new_model_loaded.is_set() + + resync: Final = AsyncMock(side_effect=gated_load) + read_through: Final = RegistryReadThrough( + resync=resync, + is_loaded=is_loaded, + max_resyncs_per_window=2, + resync_window_seconds=60.0, + ) + + burst: Final = asyncio.gather(*(read_through.attempt("new-model") for _ in range(25))) + await entered.wait() + release.set() + + assert await burst == [True] * 25 + assert resync.await_args_list == [call("new-model")] + assert await read_through.attempt("other-model") is True + assert resync.await_args_list == [call("new-model"), call("other-model")] + + @pytest.mark.asyncio async def test_resync_budget_replenishes_after_window(): spy: Final = ResyncSpy(found=True) - read_through: Final = RegistryReadThrough(resync=spy, max_resyncs_per_window=1, resync_window_seconds=0.05) + read_through: Final = RegistryReadThrough( + resync=spy, is_loaded=nothing_loaded, max_resyncs_per_window=1, resync_window_seconds=0.05 + ) assert await read_through.attempt("model-a") is True assert await read_through.attempt("model-b") is False @@ -551,3 +600,100 @@ async def test_agent_read_through_hydrates_identity_binding(lookup, clean_agent_ assert agent.identity is not None assert agent.identity.model_dump(include=set(binding)) == binding assert clean_agent_registry.get_agent_by_id(agent_id="agent-id").identity == agent.identity + + +def test_model_is_loaded_matches_router_model_names_and_deployment_ids(monkeypatch: pytest.MonkeyPatch): + import litellm.proxy.proxy_server as proxy_server + from litellm import Router + from litellm.proxy.common_utils.registry_read_through import _model_is_loaded + + router: Final = Router( + model_list=[ + { + "model_name": "loaded-model", + "litellm_params": {"model": "openai/gpt-4o-mini", "api_key": "sk-test"}, + "model_info": {"id": "loaded-deployment-id"}, + } + ] + ) + monkeypatch.setattr(proxy_server, "llm_router", router) + + assert _model_is_loaded("loaded-model") is True + assert _model_is_loaded("loaded-deployment-id") is True + assert _model_is_loaded("model-created-on-a-sibling") is False + + monkeypatch.setattr(proxy_server, "llm_router", None) + assert _model_is_loaded("loaded-model") is False + + +@pytest.mark.asyncio +async def test_model_read_through_answers_a_loaded_model_without_reading_the_db(monkeypatch: pytest.MonkeyPatch): + from unittest.mock import AsyncMock, MagicMock + + import litellm.proxy.proxy_server as proxy_server + from litellm import Router + from litellm.proxy.common_utils.registry_read_through import model_registry_read_through + + prisma_client: Final = MagicMock() + prisma_client.db.litellm_proxymodeltable.find_many = AsyncMock(side_effect=AssertionError("db read")) + router: Final = Router( + model_list=[ + { + "model_name": "wired-loaded-model", + "litellm_params": {"model": "openai/gpt-4o-mini", "api_key": "sk-test"}, + } + ] + ) + monkeypatch.setattr(proxy_server, "prisma_client", prisma_client) + monkeypatch.setattr(proxy_server, "store_model_in_db", True) + monkeypatch.setattr(proxy_server, "llm_router", router) + + assert await model_registry_read_through.attempt("wired-loaded-model") is True + prisma_client.db.litellm_proxymodeltable.find_many.assert_not_awaited() + + +@pytest.mark.asyncio +async def test_guardrail_read_through_answers_a_loaded_guardrail_without_reading_the_db( + monkeypatch: pytest.MonkeyPatch, +): + from unittest.mock import AsyncMock, MagicMock + + import litellm.proxy.proxy_server as proxy_server + from litellm.proxy.common_utils.registry_read_through import guardrail_registry_read_through + from litellm.proxy.guardrails.guardrail_registry import IN_MEMORY_GUARDRAIL_HANDLER + from litellm.types.guardrails import Guardrail + + guardrail_id: Final = "wired-loaded-guardrail-id" + guardrail_name: Final = "wired-loaded-guardrail" + prisma_client: Final = MagicMock() + prisma_client.db.litellm_guardrailstable.find_first = AsyncMock(side_effect=AssertionError("db read")) + monkeypatch.setattr(proxy_server, "prisma_client", prisma_client) + monkeypatch.setattr(proxy_server, "store_model_in_db", True) + + IN_MEMORY_GUARDRAIL_HANDLER.sync_guardrail_from_db( + guardrail=Guardrail(**dict(FakeGuardrailRow(guardrail_id, guardrail_name))) + ) + try: + assert await guardrail_registry_read_through.attempt(guardrail_name) is True + prisma_client.db.litellm_guardrailstable.find_first.assert_not_awaited() + finally: + IN_MEMORY_GUARDRAIL_HANDLER.delete_in_memory_guardrail(guardrail_id) + + +@pytest.mark.asyncio +async def test_agent_read_through_answers_a_loaded_agent_without_reading_the_db( + clean_agent_registry: "AgentRegistry", monkeypatch: pytest.MonkeyPatch +): + import litellm.proxy.proxy_server as proxy_server + from litellm.proxy.common_utils.registry_read_through import agent_registry_read_through + from litellm.types.agents import AgentResponse + + monkeypatch.setattr(proxy_server, "store_model_in_db", False) + clean_agent_registry.register_agent( + agent_config=AgentResponse.model_validate( + FakeAgentRow("wired-loaded-agent-id", "wired-loaded-agent").model_dump() + ) + ) + + assert await agent_registry_read_through.attempt("wired-loaded-agent-id") is True + assert await agent_registry_read_through.attempt("wired-loaded-agent") is True diff --git a/tests/unit/proxy/conftest.py b/tests/unit/proxy/conftest.py index 50c89387d80..6dec588b763 100644 --- a/tests/unit/proxy/conftest.py +++ b/tests/unit/proxy/conftest.py @@ -411,6 +411,8 @@ def create_proxy_test_client( def fresh_agent_read_through(monkeypatch): from litellm.proxy.common_utils import registry_read_through - read_through = registry_read_through.RegistryReadThrough(resync=registry_read_through._resync_agents) + read_through = registry_read_through.RegistryReadThrough( + resync=registry_read_through._resync_agents, is_loaded=registry_read_through._agent_is_loaded + ) monkeypatch.setattr(registry_read_through, "agent_registry_read_through", read_through) return read_through diff --git a/tests/unit/proxy/db/db_transaction_queue/test_e2e_pod_lock_manager.py b/tests/unit/proxy/db/db_transaction_queue/test_e2e_pod_lock_manager.py index 6fac731a60d..e90184ce45b 100644 --- a/tests/unit/proxy/db/db_transaction_queue/test_e2e_pod_lock_manager.py +++ b/tests/unit/proxy/db/db_transaction_queue/test_e2e_pod_lock_manager.py @@ -19,12 +19,10 @@ import fakeredis # this file is to test litellm/proxy import asyncio -import logging import pytest from litellm.proxy.db.db_transaction_queue.pod_lock_manager import PodLockManager import litellm -from litellm._logging import verbose_proxy_logger from litellm.proxy.management_endpoints.internal_user_endpoints import ( new_user, user_info, @@ -66,7 +64,6 @@ from litellm.proxy.spend_tracking.spend_management_endpoints import ( ) from litellm.proxy.utils import PrismaClient, ProxyLogging, hash_token, update_spend -verbose_proxy_logger.setLevel(level=logging.DEBUG) from starlette.datastructures import URL diff --git a/tests/unit/proxy/db/test_model_usage_rollup.py b/tests/unit/proxy/db/test_model_usage_rollup.py index f54856129dc..b9b806f4c54 100644 --- a/tests/unit/proxy/db/test_model_usage_rollup.py +++ b/tests/unit/proxy/db/test_model_usage_rollup.py @@ -1,9 +1,68 @@ +import asyncio from datetime import datetime, timezone -from unittest.mock import AsyncMock, MagicMock +from typing import Any +from unittest.mock import MagicMock +import httpx import pytest -from litellm.proxy.db.model_usage_rollup import increment_daily_model_usage, model_usage_task_type +from litellm.proxy.db.db_spend_update_writer import DBSpendUpdateWriter +from litellm.proxy.db.model_usage_rollup import ( + ModelUsageKey, + ModelUsageTransaction, + build_model_usage_transaction, + flush_model_usage_transactions, + model_usage_task_type, +) + + +class _FakeBatcher: + def __init__(self) -> None: + self.litellm_dailymodelusage = MagicMock() + + async def __aenter__(self) -> "_FakeBatcher": + return self + + async def __aexit__(self, *args: Any) -> None: + return None + + +def _prisma(batch_: MagicMock) -> MagicMock: + prisma = MagicMock() + prisma.db.batch_ = batch_ + return prisma + + +def _payload(**overrides: Any) -> dict[str, Any]: + return { + "spend": 0.25, + "prompt_tokens": 10, + "completion_tokens": 20, + "startTime": datetime(2026, 9, 28, 13, tzinfo=timezone.utc), + "model": "openai/gpt-5.4-mini", + "model_group": "fast-chat", + "metadata": "{}", + "request_tags": "[]", + "custom_llm_provider": "openai", + "status": "success", + **overrides, + } + + +def _transaction(model: str, spend: float, successful: bool = True) -> ModelUsageTransaction: + return ModelUsageTransaction( + key=ModelUsageKey( + date="2026-09-28", model_group=model, model=model, custom_llm_provider="openai", task_type="debugging" + ), + spend=spend, + prompt_tokens=10, + completion_tokens=5, + successful=successful, + ) + + +async def _no_sleep(seconds: float) -> None: + return None def test_model_usage_task_type_reads_task_tag_or_defaults() -> None: @@ -14,76 +73,131 @@ def test_model_usage_task_type_reads_task_tag_or_defaults() -> None: assert model_usage_task_type("not json") == "uncategorized" -@pytest.mark.asyncio -async def test_increment_daily_model_usage_uses_atomic_prisma_upsert() -> None: - table = MagicMock() - table.upsert = AsyncMock() - prisma_client = MagicMock() - prisma_client.db.litellm_dailymodelusage = table - payload = { - "request_id": "request-1", - "call_type": "acompletion", - "api_key": "key", - "spend": 0.25, - "total_tokens": 30, - "prompt_tokens": 10, - "completion_tokens": 20, - "startTime": datetime(2026, 9, 28, tzinfo=timezone.utc), - "endTime": datetime(2026, 9, 28, tzinfo=timezone.utc), - "completionStartTime": None, - "model": "openai/gpt-5.4-mini", - "model_id": None, - "model_group": "fast-chat", - "mcp_namespaced_tool_name": None, - "agent_id": None, - "api_base": "", - "user": "user", - "metadata": "{}", - "cache_hit": "False", - "cache_key": "", - "request_tags": "[]", - "team_id": None, - "organization_id": None, - "end_user": None, - "requester_ip_address": None, - "custom_llm_provider": "openai", - "messages": None, - "response": None, - "proxy_server_request": None, - "session_id": None, - "request_duration_ms": 20, - "status": "success", - "litellm_call_id": None, - } +def test_build_model_usage_transaction_keys_on_day_model_and_task() -> None: + transaction = build_model_usage_transaction(_payload(request_tags='["task:debugging"]', status="failure")) - await increment_daily_model_usage(prisma_client, payload) + assert transaction == ModelUsageTransaction( + key=ModelUsageKey( + date="2026-09-28", + model_group="fast-chat", + model="openai/gpt-5.4-mini", + custom_llm_provider="openai", + task_type="debugging", + ), + spend=0.25, + prompt_tokens=10, + completion_tokens=20, + successful=False, + ) - call = table.upsert.await_args.kwargs - assert call["data"]["create"]["request_count"] == 1 - assert call["data"]["update"]["completion_tokens"] == {"increment": 20} - assert call["data"]["create"]["task_type"] == "uncategorized" + +def test_build_model_usage_transaction_falls_back_for_missing_model_fields() -> None: + transaction = build_model_usage_transaction( + _payload(model="", model_group=None, custom_llm_provider=None, startTime="2026-09-28T01:02:03Z") + ) + + assert transaction is not None + assert transaction.key == ModelUsageKey( + date="2026-09-28", + model_group="unknown", + model="unknown", + custom_llm_provider="unknown", + task_type="uncategorized", + ) + + +@pytest.mark.parametrize( + "overrides", + [{"metadata": '{"internal_call_origin": "health_check"}'}, {"startTime": "bad"}], +) +def test_build_model_usage_transaction_skips_internal_calls_and_bad_dates(overrides: dict[str, Any]) -> None: + assert build_model_usage_transaction(_payload(**overrides)) is None @pytest.mark.asyncio -async def test_increment_daily_model_usage_records_task_from_request_tags() -> None: - table = MagicMock() - table.upsert = AsyncMock() - prisma_client = MagicMock() - prisma_client.db.litellm_dailymodelusage = table - payload = { - "call_type": "acompletion", - "spend": 0.1, - "prompt_tokens": 1, - "completion_tokens": 2, - "startTime": datetime(2026, 9, 28, tzinfo=timezone.utc), - "model": "gpt-5", - "model_group": "gpt-5", - "metadata": "{}", - "request_tags": '["task:debugging"]', - "custom_llm_provider": "openai", - "status": "success", +async def test_flush_aggregates_each_rollup_row_into_one_upsert() -> None: + batcher = _FakeBatcher() + prisma = _prisma(MagicMock(return_value=batcher)) + + await flush_model_usage_transactions( + prisma_client=prisma, + transactions=[ + _transaction("gpt-5", 0.5), + _transaction("claude", 1.0), + _transaction("gpt-5", 0.25, successful=False), + _transaction("gpt-5", 0.25), + ], + ) + + upserts = { + call.kwargs["where"]["date_model_group_model_custom_llm_provider_task_type"]["model"]: call.kwargs["data"] + for call in batcher.litellm_dailymodelusage.upsert.call_args_list } + assert list(upserts) == ["claude", "gpt-5"] + gpt = upserts["gpt-5"] + assert gpt["create"]["spend"] == 1.0 + assert gpt["create"]["prompt_tokens"] == 30 + assert gpt["create"]["completion_tokens"] == 15 + assert gpt["create"]["request_count"] == 3 + assert gpt["create"]["successful_requests"] == 2 + assert gpt["create"]["failed_requests"] == 1 + assert gpt["update"] == { + "spend": {"increment": 1.0}, + "prompt_tokens": {"increment": 30}, + "completion_tokens": {"increment": 15}, + "request_count": {"increment": 3}, + "successful_requests": {"increment": 2}, + "failed_requests": {"increment": 1}, + } + assert upserts["claude"]["create"]["request_count"] == 1 - await increment_daily_model_usage(prisma_client, payload) - assert table.upsert.await_args.kwargs["data"]["create"]["task_type"] == "debugging" +@pytest.mark.asyncio +async def test_flush_with_no_transactions_touches_nothing() -> None: + prisma = _prisma(MagicMock()) + await flush_model_usage_transactions(prisma_client=prisma, transactions=[]) + prisma.db.batch_.assert_not_called() + + +@pytest.mark.asyncio +async def test_flush_retries_connection_errors(monkeypatch: pytest.MonkeyPatch) -> None: + batcher = _FakeBatcher() + prisma = _prisma(MagicMock(side_effect=[httpx.ConnectError("down"), batcher])) + monkeypatch.setattr("litellm.proxy.db.model_usage_rollup.asyncio.sleep", _no_sleep) + + await flush_model_usage_transactions(prisma_client=prisma, transactions=[_transaction("gpt-5", 0.1)]) + + assert prisma.db.batch_.call_count == 2 + batcher.litellm_dailymodelusage.upsert.assert_called_once() + + +@pytest.mark.asyncio +async def test_flush_does_not_retry_ambiguous_errors() -> None: + prisma = _prisma(MagicMock(side_effect=httpx.ReadTimeout("ambiguous"))) + + with pytest.raises(httpx.ReadTimeout): + await flush_model_usage_transactions(prisma_client=prisma, transactions=[_transaction("gpt-5", 0.1)]) + + prisma.db.batch_.assert_called_once() + + +@pytest.mark.asyncio +async def test_request_time_path_queues_usage_instead_of_writing_to_the_db() -> None: + prisma = MagicMock() + prisma.model_usage_transactions = [] + prisma._model_usage_transactions_lock = asyncio.Lock() + + await DBSpendUpdateWriter()._batch_database_updates( + response_cost=0.25, + user_id="u1", + hashed_token="t1", + team_id=None, + org_id=None, + end_user_id=None, + prisma_client=prisma, + litellm_proxy_budget_name=None, + payload=_payload(request_id="req-1"), + ) + + assert [transaction.key.model for transaction in prisma.model_usage_transactions] == ["openai/gpt-5.4-mini"] + prisma.db.litellm_dailymodelusage.upsert.assert_not_called() diff --git a/tests/unit/proxy/discovery_endpoints/test_ui_discovery_endpoints.py b/tests/unit/proxy/discovery_endpoints/test_ui_discovery_endpoints.py index 64a2eb69325..34a7f78659b 100644 --- a/tests/unit/proxy/discovery_endpoints/test_ui_discovery_endpoints.py +++ b/tests/unit/proxy/discovery_endpoints/test_ui_discovery_endpoints.py @@ -460,3 +460,23 @@ def test_ui_discovery_endpoints_is_control_plane_false_when_no_workers(): data = response.json() assert data["is_control_plane"] is False assert data["workers"] == [] + + +@pytest.mark.parametrize(("flag", "expected"), [(None, False), ("false", False), ("true", True)]) +def test_ui_config_tells_the_dashboard_whether_stdio_mcp_servers_are_enabled(monkeypatch, flag, expected): + if flag is None: + monkeypatch.delenv("LITELLM_ENABLE_MCP_STDIO", raising=False) + else: + monkeypatch.setenv("LITELLM_ENABLE_MCP_STDIO", flag) + app = FastAPI() + app.include_router(router) + + with ( + patch("litellm.proxy.utils.get_server_root_path", return_value="/"), + patch("litellm.proxy.utils.get_proxy_base_url", return_value=None), + patch("litellm.proxy.auth.auth_utils.has_user_setup_sso", return_value=False), + ): + response = TestClient(app).get("/.well-known/litellm-ui-config") + + assert response.status_code == 200 + assert response.json()["mcp_stdio_enabled"] is expected diff --git a/tests/unit/proxy/lens/test_endpoints.py b/tests/unit/proxy/lens/test_endpoints.py index ca19277da08..a1441b34ffa 100644 --- a/tests/unit/proxy/lens/test_endpoints.py +++ b/tests/unit/proxy/lens/test_endpoints.py @@ -3,8 +3,105 @@ from typing import Final import pytest from fastapi import HTTPException +import litellm from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth -from litellm.proxy.lens.endpoints import list_agents, user_scope +from litellm import Router +from litellm.proxy.lens.endpoints import list_agents, user_scope, validate_model, worker_supports_model +from litellm.proxy.lens.models import LensSettings + + +@pytest.fixture +def analysis_router(monkeypatch: pytest.MonkeyPatch) -> Router: + from litellm.proxy import proxy_server + + monkeypatch.setattr(litellm, "model_cost", {**litellm.model_cost}) + router: Final = Router( + model_list=[ + { + "model_name": "openai/*", + "litellm_params": { + "model": "openai/*", + "api_key": "test-key", + "input_cost_per_token": 0.001, + "output_cost_per_token": 0.002, + }, + }, + { + "model_name": "analysis", + "litellm_params": { + "model": "openai/test-analysis", + "api_key": "test-key", + "input_cost_per_token": 0.001, + "output_cost_per_token": 0.002, + }, + }, + {"model_name": "unpriced/*", "litellm_params": {"model": "openai/*", "api_key": "test-key"}}, + ], + model_group_alias={"analysis-alias": "analysis"}, + ) + monkeypatch.setattr(proxy_server, "llm_router", router) + return router + + +@pytest.mark.parametrize("model", ("openai/test-analysis", "analysis", "analysis-alias")) +@pytest.mark.asyncio +async def test_analysis_accepts_models_served_by_configured_routes(analysis_router: Router, model: str) -> None: + settings: Final = LensSettings(name="Research", model=model, context="Answer using cited sources") + auth: Final = UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN) + assert analysis_router.get_model_list(model_name=model) + await validate_model(settings, auth) + + +@pytest.mark.parametrize("model", ("unconfigured", "anthropic/test-analysis")) +@pytest.mark.asyncio +async def test_analysis_rejects_models_without_a_configured_route(analysis_router: Router, model: str) -> None: + settings: Final = LensSettings(name="Research", model=model, context="Answer using cited sources") + auth: Final = UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN) + assert not analysis_router.get_model_list(model_name=model) + with pytest.raises(HTTPException) as error: + await validate_model(settings, auth) + assert error.value.status_code == 400 + + +@pytest.mark.asyncio +async def test_analysis_route_resolution_preserves_key_model_restrictions(analysis_router: Router) -> None: + settings: Final = LensSettings(name="Research", model="openai/test-analysis", context="Answer using cited sources") + auth: Final = UserAPIKeyAuth(user_role=LitellmUserRoles.INTERNAL_USER, models=["analysis"]) + assert analysis_router.get_model_list(model_name=settings.model) + with pytest.raises(HTTPException) as error: + await validate_model(settings, auth) + assert error.value.status_code == 403 + + +@pytest.mark.asyncio +async def test_analysis_rejects_unpriced_wildcard_before_creating_a_run(analysis_router: Router) -> None: + settings: Final = LensSettings(name="Research", model="unpriced/lens-unpriced-test", context="Answer questions") + auth: Final = UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN) + assert analysis_router.get_model_list(model_name=settings.model) + with pytest.raises(HTTPException) as error: + await validate_model(settings, auth) + assert error.value.status_code == 400 + assert "Pricing is not configured" in error.value.detail + + +@pytest.mark.parametrize("model,allowed", (("openai/test-analysis", "openai/*"), ("analysis-alias", "analysis"))) +@pytest.mark.asyncio +async def test_analysis_key_accepts_wildcard_and_alias_access( + analysis_router: Router, model: str, allowed: str +) -> None: + auth: Final = UserAPIKeyAuth(user_role=LitellmUserRoles.INTERNAL_USER, models=[allowed]) + assert analysis_router.get_model_list(model_name=model) + await validate_model(LensSettings(name="Research", model=model, context="Answer questions"), auth) + + +@pytest.mark.parametrize("revoked,key_id", ((True, "a" * 64), (False, None))) +@pytest.mark.asyncio +async def test_worker_without_active_billing_cannot_take_work(revoked: bool, key_id: str | None) -> None: + from tests.unit.proxy.lens.test_state import worker + + inactive: Final = worker().model_copy(update={"revoked": revoked, "analysis_key_id": key_id}) + settings: Final = LensSettings(name="Research", model="analysis", context="Answer questions") + assert not await worker_supports_model(inactive, settings) @pytest.mark.parametrize("role", (LitellmUserRoles.PROXY_ADMIN, LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY)) diff --git a/tests/unit/proxy/lens/test_inference.py b/tests/unit/proxy/lens/test_inference.py index 2243759b773..3b69d624a7e 100644 --- a/tests/unit/proxy/lens/test_inference.py +++ b/tests/unit/proxy/lens/test_inference.py @@ -1,11 +1,47 @@ from typing import Final import pytest +from fastapi import HTTPException +import litellm from litellm.proxy.lens.inference import Deployment, DeploymentParams, completion_charge, quote from litellm.types.utils import ModelResponse +def test_missing_optional_price_tiers_use_base_rates(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(litellm, "model_cost", {**litellm.model_cost}) + litellm.register_model( + model_cost={ + "openai/lens-base-rate-test": { + "litellm_provider": "openai", + "mode": "chat", + "input_cost_per_token": 0.001, + "output_cost_per_token": 0.002, + "input_cost_per_token_above_200k_tokens": None, + "output_cost_per_token_above_200k_tokens": None, + "input_cost_per_token_above_128k_tokens": None, + "output_cost_per_token_above_128k_tokens": None, + } + } + ) + deployment: Final = Deployment(litellm_params=DeploymentParams(model="openai/lens-base-rate-test")) + explicit: Final = Deployment( + litellm_params=DeploymentParams( + model="openai/lens-base-rate-test", input_cost_per_token=0.001, output_cost_per_token=0.002 + ) + ) + assert quote((deployment,), "Answer the question") == quote((explicit,), "Answer the question") + + +def test_unpriced_model_requires_explicit_rates() -> None: + deployment: Final = Deployment(litellm_params=DeploymentParams(model="openai/lens-unpriced-test")) + with pytest.raises(HTTPException) as error: + quote((deployment,), "Answer the question") + assert error.value.status_code == 400 + assert "input_cost_per_token" in error.value.detail + assert "output_cost_per_token" in error.value.detail + + def test_custom_priced_model_charges_reported_tokens() -> None: deployment: Final = Deployment( litellm_params=DeploymentParams( diff --git a/tests/unit/proxy/lens/test_sources.py b/tests/unit/proxy/lens/test_sources.py index f063f496314..38af8477cf9 100644 --- a/tests/unit/proxy/lens/test_sources.py +++ b/tests/unit/proxy/lens/test_sources.py @@ -4,12 +4,11 @@ from typing import Final import pytest -from litellm.proxy.lens.models import Scope, MetadataFilter -from litellm.proxy.lens.sources import SourceReader +from litellm.proxy.lens.models import MetadataFilter, Scope +from litellm.proxy.lens.sources import SourceReader, execution_id, parse_execution +from litellm.rust_bridge.trace_queries import ActivityAvailability, AgentRow, ExecutionRow from tests.unit.proxy.lens.test_state import lens -from litellm.proxy.lens.sources import execution_id, parse_execution - def test_same_trace_id_from_different_keys_is_a_distinct_execution() -> None: assert execution_id("traces", "team", "trace", "key-one-ref") != execution_id( @@ -36,24 +35,24 @@ def test_previous_saved_findings_keep_their_execution_links() -> None: async def test_sample_never_returns_authentication_attributes() -> None: class StorageResponse: async def lens_sample(self, parameters): - assert parameters["team"] == "alpha" + assert parameters.team == "alpha" return [ - { - "source": "traces", - "trace_id": "trace", - "team_id": "alpha", - "name": "run", - "start_time": "", - "span_count": 1, - "root_seen": 1, - "eligible": 1, - "attributes": [ - ["litellm.api_key_hash", "opaque-oauth-bearer"], - ["environment", "production"], - ["", "invalid"], - ["oversized", "x" * 501], - ], - } + ExecutionRow( + source="traces", + trace_id="trace", + team_id="alpha", + name="run", + start_time="", + span_count=1, + root_seen=1, + eligible=1, + attributes=( + ("litellm.api_key_hash", "opaque-oauth-bearer"), + ("environment", "production"), + ("", "invalid"), + ("oversized", "x" * 501), + ), + ) ] reader: Final = SourceReader(StorageResponse()) @@ -67,10 +66,10 @@ async def test_sample_never_returns_authentication_attributes() -> None: async def test_agents_use_the_same_team_and_key_scope_as_samples() -> None: class AgentStorage: async def lens_agents(self, parameters): - assert parameters["all_teams"] == 0 - assert parameters["team"] == "alpha" - assert parameters["key_hash"] == "key-hash" - return [{"agent_name": "research_agent"}, {"agent_name": "support_agent"}] + assert parameters.all_teams == 0 + assert parameters.team == "alpha" + assert parameters.key_hash == "key-hash" + return (AgentRow(agent_name="research_agent"), AgentRow(agent_name="support_agent")) names: Final = await SourceReader(AgentStorage()).agents(Scope(team_id="alpha", api_key_hash="key-hash")) assert names == ("research_agent", "support_agent") @@ -80,8 +79,8 @@ async def test_agents_use_the_same_team_and_key_scope_as_samples() -> None: async def test_request_only_storage_is_available_for_investigation() -> None: class RequestStorage: async def lens_availability(self, parameters): - assert parameters["team"] == "alpha" - return [{"traces": 0, "requests": 1}] + assert parameters.team == "alpha" + return (ActivityAvailability(traces=False, requests=True),) available: Final = await SourceReader(RequestStorage()).availability(Scope(team_id="alpha")) assert available.requests @@ -92,10 +91,10 @@ async def test_request_only_storage_is_available_for_investigation() -> None: async def test_agent_filter_is_independent_of_service_and_metadata() -> None: class SampleStorage: async def lens_sample(self, parameters): - assert parameters["agent_name"] == "research_agent" - assert parameters["service"] == "shared-app" - assert parameters["filter_keys"] == ("enduser.id",) - assert parameters["filter_values"] == ("user-42",) + assert parameters.agent_name == "research_agent" + assert parameters.service == "shared-app" + assert parameters.filter_keys == ("enduser.id",) + assert parameters.filter_values == ("user-42",) return [] settings: Final = lens().settings.model_copy( diff --git a/tests/unit/proxy/lens/test_worker.py b/tests/unit/proxy/lens/test_worker.py index dce3fc04d45..0e64b0f10c8 100644 --- a/tests/unit/proxy/lens/test_worker.py +++ b/tests/unit/proxy/lens/test_worker.py @@ -3,6 +3,7 @@ from typing import Final import httpx import pytest +from pydantic import ValidationError from litellm.proxy.lens.models import ( Claim, @@ -81,6 +82,45 @@ async def test_idle_worker_does_not_start_an_analysis() -> None: assert await LensWorker(client).run_once() is False +@pytest.mark.asyncio +@pytest.mark.parametrize("result_status", (200, 409)) +async def test_incompatible_claim_reports_failure_instead_of_leaving_the_investigation_running( + result_status: int, +) -> None: + claim: Final = Claim(lens_id="lens", job=queue_job(lens(), NOW, "job").jobs[0], findings=()) + payload: Final = claim.model_dump(mode="json") | { + "job": claim.job.model_dump(mode="json") | { + "settings": claim.job.settings.model_dump() | {"future_setting": "private content"}, + }, + } + saved: Final = SimpleQueue[Result]() + + def handle(request: httpx.Request) -> httpx.Response: + if request.url.path == "/lens/worker/claim": + return httpx.Response(200, json=payload) + assert request.url.path == "/lens/worker/lens/job/result" + saved.put(Result.model_validate_json(request.content)) + return httpx.Response(result_status, json=True) + + async with httpx.AsyncClient(base_url="https://proxy.test", transport=httpx.MockTransport(handle)) as client: + assert await LensWorker(client).run_once() is True + assert saved.get_nowait().error == ( + "The worker could not read this investigation. Update the worker to match the gateway, then retry." + ) + assert saved.empty() + + +@pytest.mark.asyncio +async def test_claim_without_an_identity_does_not_report_failure_for_another_investigation() -> None: + def handle(request: httpx.Request) -> httpx.Response: + assert request.url.path == "/lens/worker/claim" + return httpx.Response(200, json={"job": {"settings": {"future_setting": True}}}) + + async with httpx.AsyncClient(base_url="https://proxy.test", transport=httpx.MockTransport(handle)) as client: + with pytest.raises(ValidationError): + await LensWorker(client).run_once() + + @pytest.mark.asyncio @pytest.mark.parametrize("model_status", (200, 402, 503)) async def test_worker_reads_claimed_activity_and_reports_analysis_or_failure(model_status: int) -> None: diff --git a/tests/unit/proxy/management_endpoints/management_v1/test_spend_logs.py b/tests/unit/proxy/management_endpoints/management_v1/test_spend_logs.py index b6867d338c5..7523c864985 100644 --- a/tests/unit/proxy/management_endpoints/management_v1/test_spend_logs.py +++ b/tests/unit/proxy/management_endpoints/management_v1/test_spend_logs.py @@ -1,5 +1,5 @@ from datetime import datetime, timezone -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, MagicMock import pytest from fastapi import FastAPI, Request @@ -7,6 +7,7 @@ from fastapi.exceptions import RequestValidationError from fastapi.testclient import TestClient from litellm.proxy._types import LiteLLMRoutes, LitellmUserRoles +from litellm.proxy.auth.authorization_dependencies import get_log_team_lookup from litellm.proxy.auth.user_api_key_auth import UserAPIKeyAuth, user_api_key_auth from litellm.proxy.list_api.common import ( PROBLEM_TYPE_BASE, @@ -51,7 +52,7 @@ WINDOW = "filter[startTime][gte]=2026-07-23T00:00:00Z&filter[startTime][lte]=202 @pytest.fixture def mock_prisma_client(monkeypatch): prisma_client = MagicMock() - prisma_client.db.query_raw = AsyncMock(return_value=[]) + prisma_client.db.query_raw = AsyncMock(return_value=()) monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", prisma_client) return prisma_client @@ -71,9 +72,10 @@ def _mock_rows(mock_prisma_client, end_users: list[str]) -> AsyncMock: return query_raw -def _as_role(role: LitellmUserRoles, user_id): +def _as_role(role: LitellmUserRoles, user_id, log_team_lookup): original = app.dependency_overrides.copy() app.dependency_overrides[user_api_key_auth] = lambda: UserAPIKeyAuth(user_id=user_id, user_role=role) + app.dependency_overrides[get_log_team_lookup] = lambda: log_team_lookup return original @@ -283,13 +285,9 @@ def test_applies_no_scope_for_a_proxy_admin(mock_prisma_client, as_proxy_admin): def test_scopes_a_team_admin_to_their_own_rows_and_teams(mock_prisma_client, role): """A team admin must not see end users belonging to teams they cannot read.""" query_raw = _mock_rows(mock_prisma_client, ["cust-a"]) - original = _as_role(role, user_id="team-admin-1") + original = _as_role(role, user_id="team-admin-1", log_team_lookup=AsyncMock(return_value=("team-a", "team-b"))) try: - with patch( - "litellm.proxy.spend_tracking.spend_management_endpoints._get_permitted_team_ids_for_spend_logs", - new=AsyncMock(return_value=["team-a", "team-b"]), - ): - response = _get() + response = _get() finally: app.dependency_overrides = original @@ -297,24 +295,20 @@ def test_scopes_a_team_admin_to_their_own_rows_and_teams(mock_prisma_client, rol # Same clause shape ui_view_spend_logs builds, so the two cannot diverge. assert '("user" = $3 OR team_id = ANY($4::text[]))' in query_raw.call_args.args[0] assert query_raw.call_args.args[3] == "team-admin-1" - assert query_raw.call_args.args[4] == ["team-a", "team-b"] + assert query_raw.call_args.args[4] == ("team-a", "team-b") def test_scopes_a_teamless_user_to_their_own_rows(mock_prisma_client): query_raw = _mock_rows(mock_prisma_client, []) - original = _as_role(LitellmUserRoles.INTERNAL_USER, user_id="solo") + original = _as_role(LitellmUserRoles.INTERNAL_USER, user_id="solo", log_team_lookup=AsyncMock(return_value=())) try: - with patch( - "litellm.proxy.spend_tracking.spend_management_endpoints._get_permitted_team_ids_for_spend_logs", - new=AsyncMock(return_value=[]), - ): - response = _get() + response = _get() finally: app.dependency_overrides = original assert response.status_code == 200 sql = query_raw.call_args.args[0] - assert '("user" = $3)' in sql + assert '"user" = $3' in sql assert "team_id" not in sql assert query_raw.call_args.args[3] == "solo" @@ -322,13 +316,9 @@ def test_scopes_a_teamless_user_to_their_own_rows(mock_prisma_client): def test_returns_nothing_when_the_caller_owns_no_scope(mock_prisma_client): """Unidentifiable caller must match no rows, never fall through to unscoped.""" query_raw = _mock_rows(mock_prisma_client, []) - original = _as_role(LitellmUserRoles.INTERNAL_USER, user_id=None) + original = _as_role(LitellmUserRoles.INTERNAL_USER, user_id=None, log_team_lookup=AsyncMock(return_value=())) try: - with patch( - "litellm.proxy.spend_tracking.spend_management_endpoints._get_permitted_team_ids_for_spend_logs", - new=AsyncMock(return_value=[]), - ): - response = _get() + response = _get() finally: app.dependency_overrides = original @@ -339,19 +329,17 @@ def test_returns_nothing_when_the_caller_owns_no_scope(mock_prisma_client): def test_scopes_when_the_permitted_team_lookup_fails(mock_prisma_client): """A failed team lookup must degrade to own-rows-only, never to unscoped.""" query_raw = _mock_rows(mock_prisma_client, []) - original = _as_role(LitellmUserRoles.INTERNAL_USER, user_id="solo") + original = _as_role( + LitellmUserRoles.INTERNAL_USER, user_id="solo", log_team_lookup=AsyncMock(side_effect=RuntimeError("db down")) + ) try: - with patch( - "litellm.proxy.spend_tracking.spend_management_endpoints._get_permitted_team_ids_for_spend_logs", - new=AsyncMock(side_effect=RuntimeError("db down")), - ): - response = _get() + response = _get() finally: app.dependency_overrides = original assert response.status_code == 200 sql = query_raw.call_args.args[0] - assert '("user" = $3)' in sql + assert '"user" = $3' in sql assert "team_id" not in sql @@ -422,24 +410,22 @@ def test_user_facet_reads_internal_users_from_spend_logs(mock_prisma_client, as_ def test_user_facet_uses_the_same_team_scope_as_request_logs(mock_prisma_client): query_raw = AsyncMock(return_value=[{"user": "member@example.com"}]) mock_prisma_client.db.query_raw = query_raw - original = _as_role(LitellmUserRoles.INTERNAL_USER, user_id="team-admin-1") + original = _as_role( + LitellmUserRoles.INTERNAL_USER, user_id="team-admin-1", log_team_lookup=AsyncMock(return_value=("team-a",)) + ) try: - with patch( - "litellm.proxy.spend_tracking.spend_management_endpoints._get_permitted_team_ids_for_spend_logs", - new=AsyncMock(return_value=["team-a"]), - ): - response = _get_users() + response = _get_users() finally: app.dependency_overrides = original assert response.status_code == 200 assert '("user" = $3 OR team_id = ANY($4::text[]))' in query_raw.call_args.args[0] assert query_raw.call_args.args[3] == "team-admin-1" - assert query_raw.call_args.args[4] == ["team-a"] + assert query_raw.call_args.args[4] == ("team-a",) def test_user_facet_searches_the_internal_user_value(mock_prisma_client, as_proxy_admin): - query_raw = AsyncMock(return_value=[]) + query_raw = AsyncMock(return_value=()) mock_prisma_client.db.query_raw = query_raw _get_users(f"{WINDOW}&q=alice%40example.com") diff --git a/tests/unit/proxy/management_endpoints/scim/test_scim_v2_endpoints.py b/tests/unit/proxy/management_endpoints/scim/test_scim_v2_endpoints.py index dbcf622bbb1..62d77a00f25 100644 --- a/tests/unit/proxy/management_endpoints/scim/test_scim_v2_endpoints.py +++ b/tests/unit/proxy/management_endpoints/scim/test_scim_v2_endpoints.py @@ -6167,3 +6167,203 @@ async def test_merge_placeholder_refuses_rows_that_are_not_a_lone_placeholder( assert reason in str(exc_info.value.message) team_member_add_mock.assert_not_awaited() prisma_client.db.litellm_usertable.delete.assert_not_awaited() + + +class _PatchedTeamRow: + def __init__(self, team: LiteLLM_TeamTable) -> None: + self.team = team + self.written: dict[str, object] = {} + + async def find_unique(self, *, where: dict[str, object]) -> LiteLLM_TeamTable: + return self.team + + async def update(self, *, where: dict[str, object], data: dict[str, object]) -> LiteLLM_TeamTable: + self.written = data + self.team = LiteLLM_TeamTable(**{**self.team.model_dump(), **data, "metadata": json.loads(str(data["metadata"]))}) + return self.team + + +@pytest.mark.asyncio +async def test_patch_group_pathless_replace_applies_attributes_and_drops_empty_key(mocker, monkeypatch): + """Okta Push Groups renames a group with a path-less ``replace`` whose value is a + partial Group resource. Each attribute must apply as if sent with its own path and + the resource must land in the ``scim_data`` snapshot, never whole under an empty + metadata key, and an empty key an earlier push left behind must be dropped so the + team saves from the Admin UI again.""" + from litellm.proxy import proxy_server + + group_id = "team-1" + existing_team = LiteLLM_TeamTable( + team_id=group_id, + team_alias="okta-push-group", + members=[], + members_with_roles=[Member(user_id="user1", role="user")], + metadata={ + "": {"id": group_id, "displayName": "okta-push-group-stale"}, + "scim_managed": True, + "scim_data": {"id": group_id, "displayName": "okta-push-group", "externalId": "ext-1"}, + }, + ) + patch_ops = SCIMPatchOp( + schemas=["urn:ietf:params:scim:api:messages:2.0:PatchOp"], + Operations=[ + SCIMPatchOperation( + op="replace", + value={"id": group_id, "displayName": "okta-push-group-renamed", "externalId": "ext-2"}, + ) + ], + ) + + team_rows = _PatchedTeamRow(existing_team) + mock_prisma_client = mocker.MagicMock() + mock_prisma_client.db = mocker.MagicMock() + mock_prisma_client.db.litellm_teamtable = team_rows + mock_prisma_client.db.litellm_usertable = mocker.MagicMock() + mock_prisma_client.db.litellm_usertable.find_unique = AsyncMock(return_value=mocker.MagicMock()) + mock_prisma_client.db.litellm_usertable.find_many = AsyncMock(return_value=()) + + monkeypatch.setattr(proxy_server, "prisma_client", mock_prisma_client) + mocker.patch("litellm.proxy.management_endpoints.scim.scim_v2.patch_team_membership", AsyncMock()) + mocker.patch("litellm.proxy.management_endpoints.scim.scim_v2._recompute_scim_member_roles", AsyncMock()) + + response = await patch_group(group_id=group_id, patch_ops=patch_ops) + + assert response.id == group_id + assert response.displayName == "okta-push-group-renamed" + written = team_rows.written + assert written["team_alias"] == "okta-push-group-renamed" + written_metadata = json.loads(written["metadata"]) + assert "" not in written_metadata + assert written_metadata["externalId"] == "ext-2" + assert written_metadata["scim_data"] == { + "id": group_id, + "displayName": "okta-push-group-renamed", + "externalId": "ext-2", + } + assert written_metadata["scim_managed"] is True + + +@pytest.mark.asyncio +async def test_process_group_patch_operations_pathless_replace_members_is_absolute(mocker, monkeypatch): + """A path-less ``replace`` carrying ``members`` declares the whole roster exactly like + ``replace`` with path ``members``, so it must be reported as the replace target, and the + read-only ``id`` it carries must never become a metadata key.""" + + async def mock_get_config(): + return {"litellm_settings": {"scim_upsert_user": True}} + + from litellm.proxy.proxy_server import proxy_config + + monkeypatch.setattr(proxy_config, "get_config", mock_get_config) + + existing_team = LiteLLM_TeamTable( + team_id="team-1", + team_alias="Team One", + members=[], + members_with_roles=[Member(user_id="old-user", role="user")], + ) + patch_ops = SCIMPatchOp( + schemas=["urn:ietf:params:scim:api:messages:2.0:PatchOp"], + Operations=[SCIMPatchOperation(op="replace", value={"id": "team-1", "members": [{"value": "new-user"}]})], + ) + + mock_prisma_client = mocker.MagicMock() + mock_prisma_client.db = mocker.MagicMock() + mock_prisma_client.db.litellm_usertable = mocker.MagicMock() + mock_prisma_client.db.litellm_usertable.find_many = AsyncMock(return_value=(mocker.MagicMock(user_id="new-user"),)) + + update_data, final_members, replace_target = await _process_group_patch_operations( + patch_ops=patch_ops, + existing_team=existing_team, + prisma_client=mock_prisma_client, + ) + + assert final_members == {"new-user"} + assert replace_target == {"new-user"} + assert "id" not in update_data["metadata"] + assert "" not in update_data["metadata"] + assert update_data["metadata"]["scim_data"] == {"id": "team-1"} + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("later_op", "expected_alias", "expected_external_id", "expected_snapshot"), + [ + ( + SCIMPatchOperation(op="replace", path="displayName", value="path-wins"), + "path-wins", + "ext-pathless", + {"id": "team-1", "displayName": "path-wins", "externalId": "ext-pathless"}, + ), + ( + SCIMPatchOperation(op="remove", path="displayName"), + None, + "ext-pathless", + {"id": "team-1", "externalId": "ext-pathless"}, + ), + ( + SCIMPatchOperation(op="replace", path="externalId", value="ext-path-wins"), + "pathless-name", + "ext-path-wins", + {"id": "team-1", "displayName": "pathless-name", "externalId": "ext-path-wins"}, + ), + ], +) +async def test_process_group_patch_operations_later_path_op_wins_over_pathless_snapshot( + mocker, later_op, expected_alias, expected_external_id, expected_snapshot +): + """Operations apply in order (RFC 7644 Section 3.5.2), so a path op after a path-less one + decides both the team's value and the ``scim_data`` snapshot; the snapshot must never keep + the path-less value the later op replaced or removed.""" + existing_team = LiteLLM_TeamTable( + team_id="team-1", + team_alias="Team One", + members=[], + members_with_roles=[], + metadata={"scim_managed": True, "scim_data": {"id": "team-1", "displayName": "Team One"}}, + ) + patch_ops = SCIMPatchOp( + schemas=["urn:ietf:params:scim:api:messages:2.0:PatchOp"], + Operations=[ + SCIMPatchOperation( + op="replace", + value={"id": "team-1", "displayName": "pathless-name", "externalId": "ext-pathless"}, + ), + later_op, + ], + ) + + update_data, _, _ = await _process_group_patch_operations( + patch_ops=patch_ops, + existing_team=existing_team, + prisma_client=mocker.MagicMock(), + ) + + assert update_data["team_alias"] == expected_alias + assert update_data["metadata"].get("externalId") == expected_external_id + assert update_data["metadata"]["scim_data"] == expected_snapshot + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("op", "value"), + [("remove", {"displayName": "okta-push-group"}), ("replace", "okta-push-group-renamed")], +) +async def test_process_group_patch_operations_rejects_pathless_op_it_cannot_apply(mocker, op, value): + """A path-less ``remove`` has no target and a path-less ``add``/``replace`` needs an + object value (RFC 7644 Section 3.5.2); neither may fall through to a metadata write + under an empty key.""" + existing_team = LiteLLM_TeamTable(team_id="team-1", team_alias="Team One", members=[], members_with_roles=[]) + patch_ops = SCIMPatchOp( + schemas=["urn:ietf:params:scim:api:messages:2.0:PatchOp"], + Operations=[SCIMPatchOperation(op=op, value=value)], + ) + + with pytest.raises(HTTPException) as exc: + await _process_group_patch_operations( + patch_ops=patch_ops, + existing_team=existing_team, + prisma_client=mocker.MagicMock(), + ) + + assert exc.value.status_code == 400 diff --git a/tests/unit/proxy/management_endpoints/test_auto_router_endpoints.py b/tests/unit/proxy/management_endpoints/test_auto_router_endpoints.py index 2cbba9da8b3..01e41e8b03f 100644 --- a/tests/unit/proxy/management_endpoints/test_auto_router_endpoints.py +++ b/tests/unit/proxy/management_endpoints/test_auto_router_endpoints.py @@ -619,6 +619,18 @@ def test_classifier_plugin_is_not_settable_over_http(): _request("what is 2+2", classifier_type="custom", classifier_plugin="my_module.instance") +def _benchmark_db(rows: Sequence[Mapping[str, object]], recorded: float | None = None) -> SimpleNamespace: + """The joined benchmark statement returns the rows as given; any other statement is the Overall total.""" + from litellm.proxy.db.autorouter_session_rollup import AUTOROUTER_BENCHMARKS_SQL + + total: Final = recorded if recorded is not None else sum(float(row.get("saved_spend") or 0.0) for row in rows) + + async def query_raw(sql: str, *params: object) -> Sequence[Mapping[str, object]]: + return rows if sql == AUTOROUTER_BENCHMARKS_SQL else ({"saved": total},) + + return SimpleNamespace(db=SimpleNamespace(query_raw=AsyncMock(side_effect=query_raw))) + + class TestAutoRouterBenchmarks: from litellm.proxy.management_endpoints.auto_router_endpoints import _SessionAggRow @@ -635,15 +647,12 @@ class TestAutoRouterBenchmarks: rows: Sequence[Mapping[str, object]], model_list: Sequence[object], api_key: str | None = None, + recorded: float | None = None, ) -> AutoRouterBenchmarksResponse: from litellm.proxy import proxy_server from litellm.proxy.management_endpoints.auto_router_endpoints import get_auto_router_benchmarks - class _DB: - async def query_raw(self, sql: str, *params: object): - return rows - - monkeypatch.setattr(proxy_server, "prisma_client", type("P", (), {"db": _DB()})()) + monkeypatch.setattr(proxy_server, "prisma_client", _benchmark_db(rows, recorded)) monkeypatch.setattr(proxy_server, "llm_router", type("R", (), {"model_list": model_list})()) return await get_auto_router_benchmarks( user_api_key_dict=ADMIN, @@ -657,6 +666,7 @@ class TestAutoRouterBenchmarks: router_type="complexity", tier_turns={}, sessions=4, + session_turns=40, turns=40, unordered_turns=1, covered_turns=38, @@ -703,7 +713,6 @@ class TestAutoRouterBenchmarks: assert totals.baseline_spend == 40.0 assert totals.saved_pct == 75.0 assert totals.savings_estimated_classifier_cost == 0.4 - assert totals.saved_per_session == 7.5 assert totals.cache.coverage_pct == 95.0 assert totals.cache.hit_rate_pct == pytest.approx(73.7) assert totals.cache.same_model.hit_rate_pct == 95.0 @@ -742,7 +751,6 @@ class TestAutoRouterBenchmarks: assert (totals.spend, totals.saved_spend, totals.baseline_spend, totals.saved_pct) == (10.0, 30.0, 40.0, 75.0) assert (totals.savings_estimated_turns, totals.savings_estimated_actual_spend) == (40, 10.0) assert totals.savings_estimated_classifier_cost == 0.4 - assert totals.saved_per_session == 7.5 @pytest.mark.asyncio @pytest.mark.parametrize("router_type, saved", [("adaptive", 0.0), ("quality", 0.0), ("quality", 2.0)]) @@ -772,9 +780,50 @@ class TestAutoRouterBenchmarks: totals: Final = response.totals assert (totals.turns, totals.spend) == (50, 13.0) assert (totals.savings_estimated_turns, totals.savings_estimated_actual_spend) == (40, 10.0) - assert (totals.saved_spend, totals.baseline_spend, totals.saved_pct) == (30.0, 40.0, 75.0) + assert totals.unattributed_saved_spend is None + assert (totals.saved_spend, totals.baseline_spend, totals.saved_pct) == ( + (30.0, 40.0, 75.0) if saved == 0.0 else (32.0, None, None) + ) assert totals.savings_estimated_classifier_cost == 0.4 + @pytest.mark.asyncio + @pytest.mark.parametrize("recorded, unattributed", [(30.0, None), (33.0, 3.0), (27.0, -3.0)]) + async def test_the_headline_is_the_overall_daily_total_and_untracked_savings_void_the_baseline( + self, recorded: float, unattributed: float | None, monkeypatch: pytest.MonkeyPatch + ) -> None: + response: Final = await self._benchmarks( + monkeypatch, rows=[self.ROW.model_dump()], model_list=[], recorded=recorded + ) + totals: Final = response.totals + assert (totals.saved_spend, totals.unattributed_saved_spend) == (recorded, unattributed) + assert (totals.baseline_spend, totals.saved_pct) == ((40.0, 75.0) if unattributed is None else (None, None)) + group: Final = response.groups[0] + assert group.saved_spend == 30.0 + assert (group.baseline_spend, group.saved_pct) == ((40.0, 75.0) if unattributed is None else (None, None)) + + @pytest.mark.asyncio + async def test_a_window_holding_only_untracked_history_shows_no_router_baseline( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + history_only: Final = self.ROW.model_dump( + exclude={ + "turns", + "spend", + "saved_spend", + "savings_estimated_turns", + "savings_estimated_actual_spend", + "savings_estimated_classifier_cost", + "savings_estimated_saved_spend", + "classifier_cost", + "classifier_cost_recorded_turns", + } + ) + response: Final = await self._benchmarks(monkeypatch, rows=[history_only], model_list=[], recorded=3.0) + assert (response.totals.saved_spend, response.totals.unattributed_saved_spend) == (3.0, 3.0) + group: Final = response.groups[0] + assert (group.sessions, group.turns, group.saved_spend) == (4, 0, 0.0) + assert (group.baseline_spend, group.saved_pct) == (None, None) + def test_an_empty_window_folds_to_zeros(self): from litellm.proxy.management_endpoints.auto_router_endpoints import ( _benchmark_totals, @@ -805,7 +854,7 @@ class TestAutoRouterBenchmarks: "savings_estimated_classifier_cost": 0.0, } ) - summed = _summed_agg_row([self.ROW, other]) + summed = _summed_agg_row([self.ROW, other.model_copy(update={"session_turns": 10})]) totals = _benchmark_totals(summed) assert summed.sessions == 5 assert summed.turns == 50 @@ -868,6 +917,28 @@ class TestAutoRouterBenchmarks: assert response.status_code == 422 query.assert_not_awaited() + @pytest.mark.asyncio + async def test_an_empty_key_filter_is_rejected_before_querying_deployment_data( + self, monkeypatch: pytest.MonkeyPatch + ) -> None: + import httpx + from fastapi import FastAPI + + from litellm.proxy import proxy_server + from litellm.proxy.auth.user_api_key_auth import user_api_key_auth + from litellm.proxy.management_endpoints.auto_router_endpoints import get_auto_router_benchmarks + + query: Final = AsyncMock(return_value=[]) + monkeypatch.setattr(proxy_server, "prisma_client", SimpleNamespace(db=SimpleNamespace(query_raw=query))) + app: Final = FastAPI() + app.get("/auto_router/benchmarks")(get_auto_router_benchmarks) + app.dependency_overrides[user_api_key_auth] = lambda: ADMIN + async with httpx.AsyncClient(transport=httpx.ASGITransport(app=app), base_url="http://test") as client: + response: Final = await client.get("/auto_router/benchmarks", params={"api_key": ""}) + + assert response.status_code == 422 + query.assert_not_awaited() + @pytest.mark.asyncio async def test_a_reversed_window_is_rejected(self, monkeypatch: pytest.MonkeyPatch): from litellm.proxy import proxy_server @@ -891,15 +962,8 @@ class TestAutoRouterBenchmarks: from litellm.proxy import proxy_server from litellm.proxy.management_endpoints.auto_router_endpoints import get_auto_router_benchmarks - captured: dict = {} - - class _DB: - async def query_raw(self, sql: str, *params: object): - captured["sql"] = sql - captured["params"] = params - return [TestAutoRouterBenchmarks.ROW.model_dump()] - - monkeypatch.setattr(proxy_server, "prisma_client", type("P", (), {"db": _DB()})()) + prisma_client: Final = _benchmark_db([TestAutoRouterBenchmarks.ROW.model_dump()]) + monkeypatch.setattr(proxy_server, "prisma_client", prisma_client) response = await get_auto_router_benchmarks( user_api_key_dict=UserAPIKeyAuth(user_role=role, api_key="sk-admin", user_id="viewer"), @@ -908,7 +972,11 @@ class TestAutoRouterBenchmarks: api_key="key-hash", user_id=user_id, ) - assert captured["params"] == ("2026-07-01T00:00:00", "2026-08-02T00:00:00", "key-hash", user_id) + params: Final = tuple(call.args[1:] for call in prisma_client.db.query_raw.await_args_list) + assert params == ( + ("2026-07-01T00:00:00", "2026-08-02T00:00:00", "key-hash", user_id, "2026-07-01", "2026-08-01"), + ("2026-07-01", "2026-08-01", *(([user_id],) if user_id else ()), ["key-hash"]), + ) assert response.routers_in_scope == 1 assert response.groups[0].router_name == "live-auto" assert response.groups[0].saved_pct == response.totals.saved_pct == 75.0 @@ -946,7 +1014,6 @@ class TestAutoRouterBenchmarks: assert response.totals.saved_spend == 29.5 assert response.totals.baseline_spend == 41.5 assert response.totals.saved_pct == 71.1 - assert response.totals.saved_per_session == 5.9 @pytest.mark.asyncio @pytest.mark.parametrize( @@ -958,11 +1025,9 @@ class TestAutoRouterBenchmarks: from litellm.proxy import proxy_server from litellm.proxy.management_endpoints.auto_router_endpoints import get_auto_router_benchmarks - class _DB: - async def query_raw(self, sql: str, *params: object): - return [{**TestAutoRouterBenchmarks.ROW.model_dump(), "tier_turns": wire_value}] - - monkeypatch.setattr(proxy_server, "prisma_client", type("P", (), {"db": _DB()})()) + monkeypatch.setattr( + proxy_server, "prisma_client", _benchmark_db([{**TestAutoRouterBenchmarks.ROW.model_dump(), "tier_turns": wire_value}]) + ) response = await get_auto_router_benchmarks( user_api_key_dict=ADMIN, @@ -1006,7 +1071,7 @@ class TestAutoRouterBenchmarks: 0.0, 0.0, ) - assert (idle.saved_pct, idle.saved_per_session, idle.avg_turns_per_session) == (0.0, 0.0, 0.0) + assert (idle.saved_pct, idle.avg_turns_per_session) == (0.0, 0.0) assert (idle.cache.hit_rate_pct, idle.cache.coverage_pct) == (0.0, 0.0) assert idle.cache.same_model.turns == idle.cache.return_to_tier.hits == 0 assert idle.tier_turns == {} @@ -3724,3 +3789,18 @@ async def test_availability_waits_for_the_first_complete_catalog(monkeypatch): with pytest.raises(HTTPException) as error: await auto_router_endpoints.get_auto_router_availability(AutoRouterAvailabilityRequest(), ADMIN) assert error.value.status_code == 503 + + +class TestPerSessionAverages: + @pytest.mark.parametrize( + "sessions, turns, expected", + [(4, 40, (10.0, 100.0, 1000.0)), (0, 0, (0.0, 0.0, 0.0)), (0, 3, (None, None, None))], + ) + def test_requests_without_session_rows_have_unknown_averages_not_zero( + self, sessions: int, turns: int, expected: tuple[float | None, ...] + ) -> None: + from litellm.proxy.management_endpoints.auto_router_endpoints import _benchmark_totals + + row: Final = TestAutoRouterBenchmarks.ROW.model_copy(update={"sessions": sessions, "turns": turns}) + totals: Final = _benchmark_totals(row) + assert (totals.avg_turns_per_session, totals.avg_session_seconds, totals.avg_tokens_per_session) == expected diff --git a/tests/unit/proxy/management_endpoints/test_key_generate_prisma.py b/tests/unit/proxy/management_endpoints/test_key_generate_prisma.py index fb5e84c8294..348924b4064 100644 --- a/tests/unit/proxy/management_endpoints/test_key_generate_prisma.py +++ b/tests/unit/proxy/management_endpoints/test_key_generate_prisma.py @@ -87,7 +87,6 @@ from litellm.proxy.spend_tracking.spend_management_endpoints import ( ) from litellm.proxy.utils import PrismaClient, ProxyLogging, hash_token, update_spend -verbose_proxy_logger.setLevel(level=logging.DEBUG) from starlette.datastructures import URL diff --git a/tests/unit/proxy/management_endpoints/test_mcp_connector_import.py b/tests/unit/proxy/management_endpoints/test_mcp_connector_import.py index 9b1a0fb4f98..d60cc1fbb15 100644 --- a/tests/unit/proxy/management_endpoints/test_mcp_connector_import.py +++ b/tests/unit/proxy/management_endpoints/test_mcp_connector_import.py @@ -124,7 +124,8 @@ class TestConvertMcpServersMapping: assert isinstance(result, ConvertedConnector) assert result.request.transport == MCPTransport.sse - def test_stdio_connector(self): + def test_stdio_connector(self, monkeypatch): + monkeypatch.setenv("LITELLM_ENABLE_MCP_STDIO", "true") result = _single( { "mcpServers": { @@ -142,11 +143,18 @@ class TestConvertMcpServersMapping: assert result.request.args == ["-y", "@example/mcp-server"] assert result.request.env == {"API_KEY": "value"} - def test_disallowed_stdio_command_returns_error(self): + def test_disallowed_stdio_command_returns_error(self, monkeypatch): + monkeypatch.setenv("LITELLM_ENABLE_MCP_STDIO", "true") result = _single({"mcpServers": {"evil": {"command": "rm", "args": ["-rf", "/"]}}}) assert isinstance(result, ConnectorConversionError) assert "not in the allowed commands list" in result.error + def test_stdio_connector_is_reported_as_an_error_while_stdio_is_not_enabled(self, monkeypatch): + monkeypatch.delenv("LITELLM_ENABLE_MCP_STDIO", raising=False) + result = _single({"mcpServers": {"local": {"command": "npx", "args": ["-y", "@example/mcp-server"]}}}) + assert isinstance(result, ConnectorConversionError) + assert "LITELLM_ENABLE_MCP_STDIO=true" in result.error + def test_unsupported_type_returns_error(self): result = _single({"mcpServers": {"ws": {"type": "websocket", "url": "wss://x.example"}}}) assert isinstance(result, ConnectorConversionError) diff --git a/tests/unit/proxy/management_endpoints/test_mcp_management_endpoints.py b/tests/unit/proxy/management_endpoints/test_mcp_management_endpoints.py index f5fc5ae24d4..2cbf1d578b2 100644 --- a/tests/unit/proxy/management_endpoints/test_mcp_management_endpoints.py +++ b/tests/unit/proxy/management_endpoints/test_mcp_management_endpoints.py @@ -4881,7 +4881,8 @@ class TestMCPApprovalWorkflow: assert "team" in str(exc_info.value.detail).lower() @pytest.mark.asyncio - async def test_register_mcp_server_rejects_stdio_transport(self): + async def test_register_mcp_server_rejects_stdio_transport(self, monkeypatch): + monkeypatch.setenv("LITELLM_ENABLE_MCP_STDIO", "true") # stdio servers spawn a local subprocess on the proxy host. Accepting # them from the non-admin submission endpoint would let a team member # propose a config that an admin could rubber-stamp into local code @@ -11058,3 +11059,89 @@ class TestMCPServerResolutionCharacterization: health_check.assert_not_awaited() effects.assert_no_writes() assert httpx_mock.calls.call_count == 0 + + +@pytest.mark.parametrize("explicit_transport", [False, True]) +def test_modern_sse_create_is_rejected_before_persistence(explicit_transport: bool) -> None: + with pytest.raises(ValidationError, match="Modern MCP requires HTTP or stdio"): + NewMCPServerRequest.model_validate({ + "url": "https://upstream.example/sse", + "mcp_info": {"protocol_version": "2026-07-28"}, + **({"transport": "sse"} if explicit_transport else {}), + }) + + +@pytest.mark.parametrize("metadata", [False, True]) +def test_modern_sse_runtime_configuration_is_rejected(metadata: bool) -> None: + with pytest.raises(ValidationError, match="Modern MCP requires HTTP or stdio"): + MCPServer.model_validate({ + "server_id": "modern", "name": "modern", "transport": "sse", + **({"mcp_info": {"protocol_version": "2026-07-28"}} if metadata else {"protocol_version": "2026-07-28"}), + }) + + +@pytest.mark.parametrize("transport,version", [("http", "2026-07-28"), ("stdio", "2026-07-28"), ("sse", "2025-11-25"), ("sse", "auto")]) +def test_supported_protocol_transport_configurations_remain_valid(transport: str, version: str, monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setenv("LITELLM_ENABLE_MCP_STDIO", "true") + payload: Final = NewMCPServerRequest.model_validate({ + "transport": transport, "url": "https://upstream.example/mcp", "command": "python", "args": ["peer.py"], + "mcp_info": {"protocol_version": version}, + }) + assert payload.transport == transport + assert payload.mcp_info == {"protocol_version": version} + + +@pytest.mark.asyncio +@pytest.mark.parametrize("protocol_only", [False, True]) +async def test_modern_sse_partial_update_rejected_without_writes(protocol_only: bool) -> None: + old_record: Final = LiteLLM_MCPServerTable( + server_id="srv-1", transport="sse" if protocol_only else "http", + mcp_info={"protocol_version": "auto" if protocol_only else "2026-07-28"}, + ) + payload: Final = UpdateMCPServerRequest.model_validate({ + "server_id": "srv-1", + **({"mcp_info": {"protocol_version": "2026-07-28"}} if protocol_only else {"transport": "sse", "url": "https://upstream.example/sse"}), + }) + update_mock: Final = AsyncMock(side_effect=HTTPException(status_code=418, detail="Unexpected persistence")) + p1, p2, p3, p4, p5 = _edit_endpoint_patches(old_record, update_mock) + with p1, p2, p3, p4, p5: + with pytest.raises(HTTPException) as error: + await mgmt_endpoints.edit_mcp_server(payload=payload, user_api_key_dict=UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN)) + assert error.value.status_code == 400 + assert "Modern MCP requires HTTP or stdio" in str(error.value.detail) + update_mock.assert_not_awaited() + + +@pytest.mark.asyncio +async def test_protocol_partial_update_fails_closed_when_stored_configuration_is_unreadable() -> None: + update_mock: Final = AsyncMock(side_effect=HTTPException(status_code=418, detail="Unexpected persistence")) + p1, p2, p3, p4, p5 = _edit_endpoint_patches(RuntimeError("db unavailable"), update_mock) + with p1, p2, p3, p4, p5: + with pytest.raises(HTTPException) as error: + await mgmt_endpoints.edit_mcp_server( + payload=UpdateMCPServerRequest(server_id="srv-1", mcp_info={"protocol_version": "2026-07-28"}), + user_api_key_dict=UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN), + ) + assert error.value.status_code == 503 + update_mock.assert_not_awaited() + + +def test_modern_sse_complete_update_is_rejected() -> None: + with pytest.raises(ValidationError, match="Modern MCP requires HTTP or stdio"): + UpdateMCPServerRequest( + server_id="server", transport=MCPTransport.sse, url="https://upstream.example/sse", + mcp_info={"protocol_version": "2026-07-28"}, + ) + + +@pytest.mark.asyncio +async def test_protocol_update_on_missing_server_preserves_not_found() -> None: + update_mock: Final = AsyncMock(return_value=None) + p1, p2, p3, p4, p5 = _edit_endpoint_patches(None, update_mock) + with p1, p2, p3, p4, p5: + with pytest.raises(HTTPException) as error: + await mgmt_endpoints.edit_mcp_server( + payload=UpdateMCPServerRequest(server_id="missing", mcp_info={"protocol_version": "2026-07-28"}), + user_api_key_dict=UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN), + ) + assert error.value.status_code == 404 diff --git a/tests/unit/proxy/management_endpoints/test_model_insights_endpoints.py b/tests/unit/proxy/management_endpoints/test_model_insights_endpoints.py index 535f32a7f10..7c8d1346944 100644 --- a/tests/unit/proxy/management_endpoints/test_model_insights_endpoints.py +++ b/tests/unit/proxy/management_endpoints/test_model_insights_endpoints.py @@ -7,7 +7,7 @@ from fastapi.testclient import TestClient from litellm.proxy._types import LitellmUserRoles, UserAPIKeyAuth from litellm.proxy.auth.user_api_key_auth import user_api_key_auth -from litellm.proxy.db.model_usage_rollup import increment_daily_model_usage +from litellm.proxy.db.model_usage_rollup import build_model_usage_transaction, flush_model_usage_transactions from litellm.proxy.management_endpoints.model_insights_endpoints import router @@ -199,7 +199,7 @@ class _InMemoryUsageTable: def __init__(self) -> None: self.rows: dict[tuple[str, ...], dict[str, float]] = {} - async def upsert(self, where: dict, data: dict) -> None: + def upsert(self, where: dict, data: dict) -> None: key_fields = where["date_model_group_model_custom_llm_provider_task_type"] key = tuple(key_fields.values()) if key not in self.rows: @@ -221,11 +221,23 @@ class _InMemoryUsageTable: return list(grouped.values()) +class _InMemoryBatcher: + def __init__(self, table: _InMemoryUsageTable) -> None: + self.litellm_dailymodelusage = table + + async def __aenter__(self) -> "_InMemoryBatcher": + return self + + async def __aexit__(self, *args: object) -> None: + return None + + @pytest.mark.asyncio async def test_model_insights_reads_back_what_the_rollup_wrote() -> None: table = _InMemoryUsageTable() prisma = MagicMock() prisma.db.litellm_dailymodelusage = table + prisma.db.batch_ = MagicMock(return_value=_InMemoryBatcher(table)) payload = { "call_type": "acompletion", "spend": 0.5, @@ -240,8 +252,11 @@ async def test_model_insights_reads_back_what_the_rollup_wrote() -> None: "status": "success", } - await increment_daily_model_usage(prisma, payload) - await increment_daily_model_usage(prisma, {**payload, "request_tags": "[]"}) + transactions = ( + build_model_usage_transaction(payload), + build_model_usage_transaction({**payload, "request_tags": "[]"}), + ) + await flush_model_usage_transactions(prisma, [t for t in transactions if t is not None]) body = _call(table, "metric=requests").json() diff --git a/tests/unit/proxy/management_endpoints/test_model_management_endpoints.py b/tests/unit/proxy/management_endpoints/test_model_management_endpoints.py index 5f7807650e1..19cc8bf15ab 100644 --- a/tests/unit/proxy/management_endpoints/test_model_management_endpoints.py +++ b/tests/unit/proxy/management_endpoints/test_model_management_endpoints.py @@ -8,6 +8,8 @@ from typing import Dict, Final, Optional from unittest.mock import AsyncMock, MagicMock, patch import pytest +from fastapi import HTTPException +from fastapi.encoders import jsonable_encoder from fastapi.testclient import TestClient from litellm._uuid import uuid @@ -7515,6 +7517,96 @@ class TestTeamMemberAutoRouterWrites: "model_info": {"id": "allowed-id"}, }]) + @staticmethod + def _classifier_config(classifier: Mapping[str, object], legacy: bool) -> Mapping[str, object]: + return { + "classifier_type": "jev" if legacy else "oss_classifier", + "tiers": {"SIMPLE": "allowed"}, + "jev_classifier_config" if legacy else "opensource_classifier_config": classifier, + } + + @pytest.mark.asyncio + @pytest.mark.parametrize("team_id", [None, "member-team"]) + @pytest.mark.parametrize( + "legacy,provider,model", + [(True, "typesafe", "jev-latest"), (False, "jev", "jev-latest"), (True, "laya", "english"), (False, "laya", "english")], + ) + async def test_classifier_create_stores_only_canonical_configuration( + self, team_id: str | None, legacy: bool, provider: str, model: str + ) -> None: + from litellm.proxy.management_endpoints.model_management_endpoints import add_new_model + + row: Final = self._row() + database: Final = self._database(self._team(), row) + classifier: Final = { + "provider": provider, "model": model, + "api_base": "https://decision.test", "api_key": "stored-secret", + } + deployment: Final = Deployment( + model_name="new-classifier-router", + litellm_params=LiteLLM_Params( + model="auto_router/complexity_router", + complexity_router_config=self._classifier_config(classifier, legacy), + ), + model_info=ModelInfo(id=row.model_id, team_id=team_id), + ) + actor: Final = UserAPIKeyAuth(user_id="admin", user_role=LitellmUserRoles.PROXY_ADMIN) + with ( + self._environment(database, row), + patch("litellm.proxy.proxy_server.proxy_config.add_deployment", new=AsyncMock(return_value=ReconcileOutcome( # test-quality-ok: [TQ008] model reload I/O boundary + still_desired=frozenset((row.model_id,)), live_after=frozenset((row.model_id,)) + ))), + patch("litellm.proxy.management_endpoints.model_management_endpoints.append_team_models", new=AsyncMock()), # test-quality-ok: [TQ008] team allowlist persistence boundary + ): + await add_new_model(deployment, actor) + written: Final = database.db.litellm_proxymodeltable.create.await_args.kwargs["data"] + saved: Final = json.loads(written["litellm_params"])["complexity_router_config"] + assert saved == { + "classifier_type": "oss_classifier", + "tiers": {"SIMPLE": "allowed"}, + "opensource_classifier_config": {**classifier, "provider": "laya" if provider == "laya" else "jev"}, + } + + @pytest.mark.asyncio + @pytest.mark.parametrize("endpoint", ["create", "patch", "legacy"]) + @pytest.mark.parametrize("legacy_config", [None, {"provider": "laya", "model": "english"}]) + async def test_ambiguous_classifier_blocks_are_rejected_before_persistence( + self, endpoint: str, legacy_config: Mapping[str, object] | None + ) -> None: + from litellm.proxy.management_endpoints.model_management_endpoints import add_new_model + + row: Final = self._row() + database: Final = self._database(self._team(), row) + config: Final = { + **self._classifier_config({"provider": "laya", "model": "english"}, False), + "jev_classifier_config": legacy_config, + } + actor: Final = UserAPIKeyAuth(user_id="admin", user_role=LitellmUserRoles.PROXY_ADMIN) + request: Final = updateDeployment( + litellm_params=updateLiteLLMParams(complexity_router_config=config), model_info=ModelInfo(id=row.model_id), + ) + operation: Final = ( + add_new_model( + Deployment( + model_name="ambiguous-classifier-router", + litellm_params=LiteLLM_Params(model="auto_router/complexity_router", complexity_router_config=config), + model_info=ModelInfo(id=row.model_id), + ), + actor, + ) + if endpoint == "create" + else patch_model(row.model_id, request, actor) + if endpoint == "patch" + else update_model(request, actor) + ) + with self._environment(database, row), pytest.raises(ProxyException) as denied: + await operation + assert denied.value.code == "400" + assert "opensource_classifier_config" in denied.value.message + assert "jev_classifier_config" in denied.value.message + database.db.litellm_proxymodeltable.create.assert_not_awaited() + database.db.litellm_proxymodeltable.update.assert_not_awaited() + @pytest.mark.asyncio @pytest.mark.parametrize("endpoint,change", [("patch", "config"), ("legacy", "strategy"), ("patch", "unrelated")]) async def test_admin_router_changes_release_member_scope(self, endpoint: str, change: str) -> None: @@ -7546,15 +7638,16 @@ class TestTeamMemberAutoRouterWrites: @pytest.mark.asyncio @pytest.mark.parametrize("endpoint", ["patch", "legacy"]) + @pytest.mark.parametrize("stored_legacy,supplied_legacy", [(True, True), (True, False), (False, True), (False, False)]) @pytest.mark.parametrize("change", ["save", "rotate", "move", "move-without-key", "reset", "heuristic"]) - async def test_jev_dashboard_save_preserves_server_transport(self, endpoint: str, change: str) -> None: + async def test_jev_dashboard_save_preserves_server_transport( + self, endpoint: str, change: str, stored_legacy: bool, supplied_legacy: bool + ) -> None: original: Final = self._row() transport: Final = {"api_key": "synthetic-original-jev-key", "api_base": "https://jev.example.com"} - stored_config: Final = { - "classifier_type": "jev", - "tiers": {"SIMPLE": "allowed"}, - "jev_classifier_config": {**transport, "instructions": "Old instructions", "timeout_ms": 6100}, - } + stored_config: Final = self._classifier_config( + {**transport, "instructions": "Old instructions", "timeout_ms": 6100}, stored_legacy + ) row: Final = original.model_copy( update={ "litellm_params": { @@ -7572,11 +7665,11 @@ class TestTeamMemberAutoRouterWrites: "reset": {"api_key": None, "api_base": None}, "heuristic": {}, }[change] - config: Final = { - "tiers": {"SIMPLE": "allowed"}, - "classifier_type": "heuristic" if change == "heuristic" else "jev", - **({} if change == "heuristic" else {"jev_classifier_config": {"timeout_ms": 8100, **overrides}}), - } + config: Final = ( + {"tiers": {"SIMPLE": "allowed"}, "classifier_type": "heuristic"} + if change == "heuristic" + else self._classifier_config({"timeout_ms": 8100, **overrides}, supplied_legacy) + ) request: Final = updateDeployment( litellm_params=updateLiteLLMParams(complexity_router_config=config), model_info=ModelInfo(id=row.model_id), @@ -7597,12 +7690,183 @@ class TestTeamMemberAutoRouterWrites: expected: Final = ( config if change == "heuristic" - else {**config, "jev_classifier_config": {**transport, "timeout_ms": 8100, **overrides}} + else { + "classifier_type": "oss_classifier", + "tiers": {"SIMPLE": "allowed"}, + "opensource_classifier_config": {**transport, "timeout_ms": 8100, **overrides}, + } ) assert saved == expected assert row.litellm_params["complexity_router_config"] == stored_config assert request.litellm_params.complexity_router_config == config + @pytest.mark.asyncio + @pytest.mark.parametrize("endpoint", ["patch", "legacy"]) + @pytest.mark.parametrize("stored_legacy,supplied_legacy", [(True, True), (True, False), (False, True), (False, False)]) + @pytest.mark.parametrize( + "stored_provider,stored_base,supplied,expected_transport", + [ + ("bespoke", "https://decision.test", {"provider": "bespoke", "model": "nimble-latest"}, {"api_base": "https://decision.test", "api_key": "stored-secret"}), + ("bespoke", "https://decision.test", {"provider": "bespoke", "model": "nimble-latest", "api_base": "https://new.test"}, {}), + ("bespoke", "https://decision.test", {"provider": "laya", "model": "english"}, {}), + ("laya", "https://decision.test", {"provider": "bespoke", "model": "nimble-latest"}, {}), + ("laya", "https://decision.test", {"provider": "laya", "model": "english"}, {"api_base": "https://decision.test", "api_key": "stored-secret"}), + ("laya", "https://decision.test", {"provider": "laya", "model": "english", "api_base": "https://decision.test"}, {"api_base": "https://decision.test", "api_key": "stored-secret"}), + ( + "laya", + "https://decision.test", + {"provider": "laya", "model": "english", "api_key": None}, + {"api_base": "https://decision.test"}, + ), + ("laya", "https://decision.test", {"provider": "laya", "model": "english", "api_base": "https://new.test"}, {}), + ("laya", "https://decision.test", {"provider": "laya", "model": "english", "api_base": None}, {}), + ("laya", None, {"provider": "laya", "model": "english", "api_base": None}, {}), + ("laya", "https://decision.test", {"provider": "typesafe", "model": "jev-latest"}, {}), + ( + "laya", "https://decision.test", {"model": "english", "timeout_ms": 8100}, + {"provider": "laya", "api_base": "https://decision.test", "api_key": "stored-secret"}, + ), + ("typesafe", "https://decision.test", {"provider": "laya", "model": "english"}, {}), + ( + "typesafe", "https://decision.test", {"provider": "jev", "model": "jev-latest"}, + {"api_base": "https://decision.test", "api_key": "stored-secret"}, + ), + ( + "jev", "https://decision.test", {"provider": "typesafe", "model": "jev-latest"}, + {"api_base": "https://decision.test", "api_key": "stored-secret"}, + ), + ], + ) + async def test_decision_provider_changes_cannot_reuse_a_stored_key( + self, endpoint: str, stored_provider: str, stored_base: str | None, + supplied: Mapping[str, object], expected_transport: Mapping[str, object], + stored_legacy: bool, supplied_legacy: bool, + ) -> None: + original: Final = self._row() + row: Final = original.model_copy(update={"litellm_params": { + "model": "auto_router/complexity_router", + "complexity_router_config": self._classifier_config( + { + "provider": stored_provider, "model": {"laya": "english", "bespoke": "nimble-latest"}.get(stored_provider, "jev-latest"), + "api_base": stored_base, "api_key": "stored-secret", + }, + stored_legacy, + ), + }}) + database: Final = self._database(self._team(), row) + config: Final = self._classifier_config(supplied, supplied_legacy) + request: Final = updateDeployment( + litellm_params=updateLiteLLMParams(complexity_router_config=config), model_info=ModelInfo(id=row.model_id), + ) + actor: Final = UserAPIKeyAuth(user_id="admin", user_role=LitellmUserRoles.PROXY_ADMIN) + with self._environment(database, row): + await (patch_model(row.model_id, request, actor) if endpoint == "patch" else update_model(request, actor)) + written: Final = database.db.litellm_proxymodeltable.update.await_args.kwargs["data"] + saved: Final = json.loads(written["litellm_params"])["complexity_router_config"] + expected_provider: Final = supplied.get("provider", stored_provider) + assert saved == { + "classifier_type": "oss_classifier", + "tiers": {"SIMPLE": "allowed"}, + "opensource_classifier_config": { + **expected_transport, **supplied, + "provider": "jev" if expected_provider == "typesafe" else expected_provider, + }, + } + + @pytest.mark.asyncio + @pytest.mark.parametrize("endpoint", ["patch", "legacy"]) + @pytest.mark.parametrize( + "string_params,reset_field,config_shape", + [ + (False, None, "full"), (True, None, "full"), (False, "api_key", "full"), + (False, "api_base", "full"), (False, None, "omit-provider"), + (False, None, "omit-config"), (False, None, "null-config"), + ], + ) + async def test_member_save_protects_stored_classifier_connection( + self, endpoint: str, string_params: bool, reset_field: str | None, config_shape: str + ) -> None: + original: Final = self._row() + config: Final = { + "classifier_type": "jev", "tiers": {"SIMPLE": "allowed"}, + "jev_classifier_config": {"provider": "laya", "model": "english"}, + } + secret_params: Final = { + "model": "auto_router/complexity_router", + "complexity_router_config": { + **config, "jev_classifier_config": { + **config["jev_classifier_config"], "api_key": "retained-laya-secret", "api_base": "https://laya.test", + }, + }, + } + row: Final = original.model_copy(update={"litellm_params": secret_params}) + team: Final = self._team().model_copy(update={"models": ["allowed", "laya/english"]}) + database: Final = self._database(team, row) + database.transaction.litellm_proxymodeltable.update.return_value = row.model_copy( + update={"litellm_params": json.dumps(secret_params) if string_params else secret_params} + ) + supplied_config: Final = { + **config, "jev_classifier_config": { + **{ + key: value for key, value in config["jev_classifier_config"].items() + if key != "provider" or config_shape != "omit-provider" + }, + **({reset_field: None} if reset_field is not None else {}), + }, + } + patch_params: Final = ( + {"complexity_router_default_model": "allowed"} + if config_shape == "omit-config" + else {"complexity_router_config": None, "complexity_router_default_model": "allowed"} + if config_shape == "null-config" + else {"complexity_router_config": supplied_config} + ) + request: Final = updateDeployment( + litellm_params=updateLiteLLMParams.model_validate(patch_params), + model_info=ModelInfo(id=row.model_id, team_id="member-team"), + ) + actor: Final = UserAPIKeyAuth( + user_id="owner", user_role=LitellmUserRoles.INTERNAL_USER, models=["allowed", "laya/english"], config={"timeout": 60}, + ) + with self._environment(database, row): + if reset_field is not None: + expected_error: Final = HTTPException if endpoint == "patch" else ProxyException + with pytest.raises(expected_error, match="Team members cannot change classifier connections") as denied: + await ( + patch_model(row.model_id, request, actor) if endpoint == "patch" else update_model(request, actor) + ) + assert ( + denied.value.status_code if isinstance(denied.value, HTTPException) else int(denied.value.code) + ) == 403 + database.transaction.litellm_proxymodeltable.update.assert_not_awaited() + assert row.litellm_params == secret_params + return + response: Final = await (patch_model(row.model_id, request, actor) if endpoint == "patch" else update_model(request, actor)) + written: Final = database.transaction.litellm_proxymodeltable.update.await_args.kwargs["data"] + saved_config: Final = json.loads(written["litellm_params"])["complexity_router_config"] + untouched: Final = config_shape in ("omit-config", "null-config") + saved: Final = saved_config["jev_classifier_config" if untouched else "opensource_classifier_config"] + assert saved == secret_params["complexity_router_config"]["jev_classifier_config"] + assert saved_config["classifier_type"] == ("jev" if untouched else "oss_classifier") + if untouched: + from litellm.proxy.common_utils.encrypt_decrypt_utils import decrypt_value_helper + + assert decrypt_value_helper( + json.loads(written["litellm_params"])["complexity_router_default_model"], + key="complexity_router_default_model", return_original_value=True, + ) == "allowed" + response_payload: Final = jsonable_encoder(response) + assert "retained-laya-secret" not in json.dumps(response_payload) + response_params: Final = json.loads(response_payload["litellm_params"]) if string_params else response_payload["litellm_params"] + assert response_params == { + **secret_params, "complexity_router_config": { + **config, "jev_classifier_config": { + **config["jev_classifier_config"], "api_key": "REDACTED", "api_base": "https://laya.test", + }, + }, + } + assert "retained-laya-secret" in row.model_dump_json() + @pytest.mark.asyncio @pytest.mark.parametrize("endpoint", ["patch", "legacy"]) @pytest.mark.parametrize("access", ["owner", "peer", "limited-key"]) diff --git a/tests/unit/proxy/management_helpers/test_audit_logs_proxy.py b/tests/unit/proxy/management_helpers/test_audit_logs_proxy.py index 878e19f5b6f..98922801296 100644 --- a/tests/unit/proxy/management_helpers/test_audit_logs_proxy.py +++ b/tests/unit/proxy/management_helpers/test_audit_logs_proxy.py @@ -14,13 +14,11 @@ import time # this file is to test litellm/proxy import asyncio -import logging load_dotenv() import pytest import litellm -from litellm._logging import verbose_proxy_logger from litellm.proxy.proxy_server import ( LitellmUserRoles, @@ -35,7 +33,6 @@ from litellm.proxy.proxy_server import ( from litellm.proxy.utils import PrismaClient, ProxyLogging, hash_token, update_spend -verbose_proxy_logger.setLevel(level=logging.DEBUG) from starlette.datastructures import URL diff --git a/tests/unit/proxy/management_helpers/test_auto_router_permissions.py b/tests/unit/proxy/management_helpers/test_auto_router_permissions.py index e16271a5189..1ccfbab7b1f 100644 --- a/tests/unit/proxy/management_helpers/test_auto_router_permissions.py +++ b/tests/unit/proxy/management_helpers/test_auto_router_permissions.py @@ -1,3 +1,4 @@ +import json from collections.abc import Mapping from dataclasses import dataclass from typing import Final @@ -137,33 +138,50 @@ def test_tier_config_is_normalized_and_unknown_router_extras_are_rejected() -> N @pytest.mark.parametrize( ("jev_override", "rejected_at"), [ - ({"api_base": "https://collector.invalid"}, "jev_classifier_config"), + ({"api_base": "https://collector.invalid"}, "opensource_classifier_config"), ({"api_key": "sk-member"}, "api_key"), ({"api_base": "https://collector.invalid", "api_key": "sk-member"}, "api_key"), - ({"api_base": "https://collector.invalid", "api_key": ""}, "jev_classifier_config.api_key"), + ({"api_base": "https://collector.invalid", "api_key": ""}, "opensource_classifier_config.api_key"), + ({"provider": "laya", "model": "english", "api_base": "https://collector.invalid"}, "api_base"), + ({"provider": "laya", "model": "english", "api_key": "sk-member"}, "api_key"), + ({"provider": "bespoke", "model": "nimble-latest", "api_base": "https://collector.invalid"}, "api_base"), + ({"provider": "bespoke", "model": "nimble-latest", "api_key": "sk-member"}, "api_key"), ], ) +@pytest.mark.parametrize("legacy", [False, True]) def test_members_cannot_move_the_jev_classifier_off_the_proxys_typesafe_account( - jev_override: Mapping[str, str], rejected_at: str + jev_override: Mapping[str, str], rejected_at: str, legacy: bool ) -> None: with pytest.raises(HTTPException) as denied: validate_member_auto_router_config( - {"tiers": {"SIMPLE": "allowed"}, "classifier_type": "jev", "jev_classifier_config": jev_override} + { + "tiers": {"SIMPLE": "allowed"}, + "classifier_type": "jev" if legacy else "oss_classifier", + "jev_classifier_config" if legacy else "opensource_classifier_config": jev_override, + } ) assert denied.value.status_code == 400 assert denied.value.detail == f"Invalid member auto-router configuration at {rejected_at}." -def test_members_can_still_tune_the_jev_classifier() -> None: +@pytest.mark.parametrize(("provider", "model"), [("typesafe", "jev-preview"), ("laya", "english"), ("bespoke", "nimble-latest")]) +@pytest.mark.parametrize("legacy", [False, True]) +def test_members_can_still_tune_the_jev_classifier(provider: str, model: str, legacy: bool) -> None: validated: Final = validate_member_auto_router_config( { "tiers": {"SIMPLE": "allowed"}, - "classifier_type": "jev", - "jev_classifier_config": {"model": "jev-preview", "timeout_ms": 500}, + "classifier_type": "jev" if legacy else "oss_classifier", + "jev_classifier_config" if legacy else "opensource_classifier_config": { + "provider": provider, "model": model, "timeout_ms": 500, + }, } ) assert validated.jev_classifier_config is not None - assert (validated.jev_classifier_config.model, validated.jev_classifier_config.timeout_ms) == ("jev-preview", 500) + assert ( + validated.jev_classifier_config.provider, + validated.jev_classifier_config.model, + validated.jev_classifier_config.timeout_ms, + ) == ("jev" if provider == "typesafe" else provider, model, 500) assert validate_member_auto_router_config(validated.model_dump()).jev_classifier_config is not None @@ -217,6 +235,92 @@ async def test_member_updates_restrict_fields_and_preserve_an_inherited_default( assert granted.default_model == "allowed" +@pytest.mark.asyncio +@pytest.mark.parametrize( + "nested,expected_identity,restricted", + [ + ("omit-config", "laya/english", False), + ("omit-config", "laya/english", True), + ("omit-block", None, False), + (None, None, False), + ({}, None, False), + ({"timeout_ms": 500}, None, False), + ({"model": "english", "timeout_ms": 500}, "laya/english", False), + ({"model": "english", "timeout_ms": 500}, "laya/english", True), + ({"model": "multilingual"}, "laya/multilingual", False), + ({"provider": "typesafe", "model": "jev-latest"}, "typesafe/jev-latest", False), + ({"provider": "typesafe", "model": "jev-latest"}, "typesafe/jev-latest", True), + ], +) +async def test_member_authorization_and_persistence_resolve_the_same_classifier( + catalog: Router, monkeypatch: pytest.MonkeyPatch, nested: object, expected_identity: str | None, restricted: bool +) -> None: + from litellm.proxy.management_endpoints.model_management_endpoints import ( + _strategy_router_write_violation, + update_db_model, + ) + from litellm.types.management_endpoints.auto_router_endpoints import RequestComplexityRouterConfig + + monkeypatch.setenv("LITELLM_SALT_KEY", "member-router-test-salt") + stored_config: Final = { + "classifier_type": "jev", "tiers": {"SIMPLE": "allowed"}, + "jev_classifier_config": { + "provider": "laya", "model": "english", "timeout_ms": 12000, + "api_base": "https://laya.test", "api_key": "stored-classifier-key", + }, + } + existing: Final = Deployment( + model_name="member-router", + litellm_params=LiteLLM_Params(model="auto_router/complexity_router", complexity_router_config=stored_config), + model_info=ModelInfo(id="router-a", team_id="team-a"), created_by="owner", + ) + incoming_config: Final = ( + None if nested == "omit-config" else { + "classifier_type": "jev", "tiers": {"SIMPLE": "allowed"}, + **({} if nested == "omit-block" else {"jev_classifier_config": nested}), + } + ) + patch: Final = updateDeployment.model_validate({"litellm_params": { + "complexity_router_config": incoming_config, "complexity_router_default_model": "allowed", + }}) + operation: Final = authorize_member_auto_router_write( + incoming=patch, existing=existing, user_api_key_dict=_actor( + models=["allowed"] if restricted or expected_identity is None else ["allowed", expected_identity], + ), + team=_team(models=["allowed", "laya/english", "laya/multilingual", "typesafe/jev-latest"]), + premium_user=True, prisma_client=_Client(), llm_router=catalog, + ) + violation: Final = _strategy_router_write_violation(patch.litellm_params, existing.litellm_params) + if expected_identity is None: + assert violation is not None + with pytest.raises(HTTPException) as rejected: + await operation + assert rejected.value.status_code == 400 + return + assert violation is None + if restricted: + with pytest.raises(ProxyException, match=expected_identity): + await operation + return + grant: Final = await operation + persisted: Final = update_db_model(existing, patch) + saved: Final = RequestComplexityRouterConfig.model_validate( + json.loads(persisted["litellm_params"])["complexity_router_config"] + ) + assert grant.config == saved + assert saved.jev_classifier_config is not None + assert ( + "typesafe" if saved.jev_classifier_config.provider == "jev" else saved.jev_classifier_config.provider + ) + f"/{saved.jev_classifier_config.model}" == expected_identity + assert saved.jev_classifier_config.api_key == ( + "stored-classifier-key" if expected_identity.startswith("laya/") else None + ) + assert saved.jev_classifier_config.timeout_ms == ( + 12000 if nested == "omit-config" else 500 if nested == {"model": "english", "timeout_ms": 500} else 3000 + ) + assert existing.litellm_params.complexity_router_config == stored_config + + @pytest.mark.asyncio @pytest.mark.parametrize("target", ["missing", "nested"]) async def test_member_dependencies_require_plain_configured_models(target: str) -> None: @@ -246,13 +350,17 @@ async def test_member_dependencies_require_plain_configured_models(target: str) @pytest.mark.asyncio @pytest.mark.parametrize("restricted", ["key", "team", None]) +@pytest.mark.parametrize(("provider", "model"), [("typesafe", "jev-latest"), ("laya", "english"), ("bespoke", "nimble-latest")]) async def test_jev_evaluation_requires_model_access_but_no_completion_deployment( - catalog: Router, restricted: str | None + catalog: Router, restricted: str | None, provider: str, model: str ) -> None: - permitted: Final = ["allowed", "typesafe/jev-latest"] + permitted: Final = ["allowed", f"{provider}/{model}"] operation: Final = authorize_member_auto_router_dependencies( config=validate_member_auto_router_config( - {"tiers": {"SIMPLE": "allowed"}, "classifier_type": "jev", "jev_classifier_config": {}} + { + "tiers": {"SIMPLE": "allowed"}, "classifier_type": "jev", + "jev_classifier_config": {"provider": provider, "model": model}, + } ), default_model=None, user_api_key_dict=_actor(models=["allowed"] if restricted == "key" else permitted), @@ -261,17 +369,20 @@ async def test_jev_evaluation_requires_model_access_but_no_completion_deployment llm_router=catalog, ) if restricted is not None: - with pytest.raises(ProxyException, match="jev-latest"): + with pytest.raises(ProxyException, match=model): await operation return await operation - assert not catalog.get_model_list("typesafe/jev-latest") + assert not catalog.get_model_list(f"{provider}/{model}") @pytest.mark.asyncio @pytest.mark.parametrize("restricted", ["member", "project", "organization", None]) -async def test_jev_evaluation_obeys_each_containing_scope(catalog: Router, restricted: str | None) -> None: - allowed: Final = ["allowed", "typesafe/jev-latest"] +@pytest.mark.parametrize(("provider", "model"), [("typesafe", "jev-latest"), ("laya", "english"), ("bespoke", "nimble-latest")]) +async def test_jev_evaluation_obeys_each_containing_scope( + catalog: Router, restricted: str | None, provider: str, model: str +) -> None: + allowed: Final = ["allowed", f"{provider}/{model}"] membership: Final = LiteLLM_TeamMembership.model_validate( { "user_id": "owner", @@ -293,7 +404,10 @@ async def test_jev_evaluation_obeys_each_containing_scope(catalog: Router, restr ) operation: Final = authorize_member_auto_router_dependencies( config=validate_member_auto_router_config( - {"tiers": {"SIMPLE": "allowed"}, "classifier_type": "jev", "jev_classifier_config": {}} + { + "tiers": {"SIMPLE": "allowed"}, "classifier_type": "jev", + "jev_classifier_config": {"provider": provider, "model": model}, + } ), default_model=None, user_api_key_dict=_actor(models=allowed, project_id="project-a"), @@ -303,8 +417,8 @@ async def test_jev_evaluation_obeys_each_containing_scope(catalog: Router, restr dependency_objects=MemberAutoRouterDependencyObjects(membership, organization, project), ) if restricted is not None: - with pytest.raises(ProxyException, match="jev-latest"): + with pytest.raises(ProxyException, match=model): await operation return await operation - assert not catalog.get_model_list("typesafe/jev-latest") + assert not catalog.get_model_list(f"{provider}/{model}") diff --git a/tests/unit/proxy/pass_through_endpoints/llm_provider_handlers/test_typesafe_passthrough_logging_handler.py b/tests/unit/proxy/pass_through_endpoints/llm_provider_handlers/test_typesafe_passthrough_logging_handler.py index e0a5ef063e8..7961d2a911b 100644 --- a/tests/unit/proxy/pass_through_endpoints/llm_provider_handlers/test_typesafe_passthrough_logging_handler.py +++ b/tests/unit/proxy/pass_through_endpoints/llm_provider_handlers/test_typesafe_passthrough_logging_handler.py @@ -1,10 +1,12 @@ from datetime import datetime +from typing import Final from unittest.mock import MagicMock import httpx import pytest import litellm +from litellm.litellm_core_utils.litellm_logging import Logging from litellm.proxy.pass_through_endpoints.llm_provider_handlers.typesafe_passthrough_logging_handler import ( TypeSafePassthroughLoggingHandler, ) @@ -137,6 +139,89 @@ def test_success_handler_dispatches_to_typesafe_handler(): assert normalized["kwargs"]["model"] == "typesafe/jev-1.13.0" +@pytest.mark.asyncio +@pytest.mark.parametrize("guardrail_cost", [0.0, 0.25]) +@pytest.mark.parametrize("metadata_slot", ["metadata", "litellm_metadata"]) +@pytest.mark.parametrize("provider,requested,routing_model", [ + ("laya", "english", "multilingual"), ("laya", "english", None), + ("bespoke", "nimble-latest", None), + ("bespoke", "bespokelabs/Bespoke-Nimble-9B", None), +]) +async def test_oss_gateway_accounts_for_checkpoint_usage_and_registered_cost( + monkeypatch: pytest.MonkeyPatch, routing_model: str | None, metadata_slot: str, guardrail_cost: float, + provider: str, requested: str +) -> None: + checkpoint: Final = routing_model or requested + model: Final = f"{provider}/{checkpoint}" + input_rate: Final = 0.002 + output_rate: Final = 0.005 + monkeypatch.setitem(litellm.model_cost, model, { + "input_cost_per_token": input_rate, "output_cost_per_token": output_rate, + "litellm_provider": provider, "mode": "evaluation", + }) + start: Final = datetime.now() + logging_obj: Final = Logging( + model=requested, messages=[], stream=False, call_type="pass_through_endpoint", + start_time=start, litellm_call_id="oss-accounting", function_id="oss-accounting", kwargs={}, + ) + from fastapi import Request + from litellm.proxy._types import UserAPIKeyAuth + from litellm.proxy.pass_through_endpoints.pass_through_endpoints import HttpPassThroughEndpointHelpers + + request: Final = Request({ + "type": "http", "method": "POST", "path": f"/{provider}/v1/systemone", + "headers": [], "query_string": b"", + }) + auth: Final = UserAPIKeyAuth( + api_key="oss-budget-key", token="oss-budget-key", + model_max_budget={f"{provider}/{requested}": {"budget_limit": 0.01, "time_period": "1d"}}, + ) + request_body: Final = {"model": requested, metadata_slot: {"model_group": "unbounded-client-choice"}} + logging_kwargs: Final = HttpPassThroughEndpointHelpers._init_kwargs_for_pass_through_endpoint( + request=request, user_api_key_dict=auth, logging_obj=logging_obj, + passthrough_logging_payload={"url": f"https://{provider}.test/v1/systemone"}, _parsed_body=request_body, + ) + logging_kwargs["litellm_params"]["metadata"]["standard_logging_guardrail_information"] = [ + {"guardrail_name": "trusted-hook", "guardrail_cost": guardrail_cost}, + ] + logging_obj.update_environment_variables( + model=requested, user="unknown", optional_params={}, + litellm_params=logging_kwargs["litellm_params"], call_type="pass_through_endpoint", + ) + body: Final = { + "model": "laya-rl-agent" if provider == "laya" else requested, "usage": {"input_tokens": 10, "output_tokens": 3}, + **({"routing": {"model": routing_model}} if routing_model else {}), + } + normalized: Final = PassThroughEndpointLogging().normalize_llm_passthrough_logging_payload( + httpx_response=httpx.Response(200, request=httpx.Request("POST", f"https://{provider}.test/v1/systemone"), json=body), + response_body=body, request_body={"model": requested}, logging_obj=logging_obj, + url_route=f"https://{provider}.test/v1/systemone", result="{}", start_time=start, + end_time=datetime.now(), cache_hit=False, custom_llm_provider=provider, **logging_kwargs, + ) + logged: Final = normalized["kwargs"] + expected_cost: Final = 10 * input_rate + 3 * output_rate + assert (logged["model"], logged["custom_llm_provider"]) == (model, provider) + assert logged["response_cost"] == pytest.approx(expected_cost) + assert logged["combined_usage_object"].model_dump(exclude_none=True) == { + "prompt_tokens": 10, "completion_tokens": 3, "total_tokens": 13, + } + assert logging_obj.model_call_details["model"] == model + assert logging_obj.model_call_details["response_cost"] == pytest.approx(expected_cost) + assert logged["standard_logging_object"]["model"] == model + assert logged["standard_logging_object"]["model_group"] == f"{provider}/{requested}" + assert logged["standard_logging_object"]["response_cost"] == pytest.approx(expected_cost + guardrail_cost) + + from litellm.caching.caching import DualCache + from litellm.exceptions import BudgetExceededError + from litellm.proxy.hooks.model_max_budget_limiter import _PROXY_VirtualKeyModelMaxBudgetLimiter + + budget_limiter: Final = _PROXY_VirtualKeyModelMaxBudgetLimiter(DualCache()) + assert await budget_limiter.is_key_within_model_budget(auth, f"{provider}/{requested}") + await budget_limiter.async_log_success_event(logged, None, start, datetime.now()) + with pytest.raises(BudgetExceededError): + await budget_limiter.is_key_within_model_budget(auth, f"{provider}/{requested}") + + def test_openrouter_decisions_response_is_priced_from_request_model_registry_row(): logging_obj = _logging_obj() model_cost = litellm.model_cost["openrouter/typesafe/jev-1.13"] diff --git a/tests/unit/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py b/tests/unit/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py index a0772c7d4f3..cad065c64d1 100644 --- a/tests/unit/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py +++ b/tests/unit/proxy/pass_through_endpoints/test_llm_pass_through_endpoints.py @@ -23,6 +23,8 @@ from starlette.datastructures import FormData import litellm +from litellm.caching.caching import DualCache +from litellm.types.utils import CallTypesLiteral from litellm.proxy.common_request_processing import ProxyBaseLLMRequestProcessing from tests.unit.llms.bedrock.event_loop_probe import EventLoopProbe from litellm.constants import LITELLM_PROXY_MASTER_KEY_ALIAS @@ -7296,6 +7298,8 @@ class TestTypeSafePassthroughRoute: "provider, endpoint, is_decision_request", ( ("typesafe", "systemone", True), + ("laya", "systemone", True), + ("bespoke", "systemone", True), ("typesafe", "systemone/", True), ("typesafe", "systemone?trace=1", True), ("typesafe", "systemone/?trace=1", True), @@ -7314,7 +7318,7 @@ class TestTypeSafePassthroughRoute: self, client: TestClient, monkeypatch: pytest.MonkeyPatch, - provider: Literal["typesafe", "openrouter"], + provider: Literal["typesafe", "openrouter", "laya", "bespoke"], endpoint: str, is_decision_request: bool, quota_scope: Literal["key", "project_output"], @@ -7335,12 +7339,15 @@ class TestTypeSafePassthroughRoute: monkeypatch.setattr(proxy_server, "proxy_logging_obj", ProxyLogging(user_api_key_cache=cache)) monkeypatch.setenv("OPENROUTER_API_KEY", "openrouter-test-key") monkeypatch.setenv("OPENROUTER_API_BASE", "https://typesafe.example/base") - model: Final = "jev-latest" if provider == "typesafe" else "test-generative-model" + monkeypatch.setenv("LAYA_API_BASE", "https://typesafe.example/base") + monkeypatch.setenv("BESPOKE_API_BASE", "https://typesafe.example/base") + model: Final = {"typesafe": "jev-latest", "laya": "english", "bespoke": "nimble-latest"}.get(provider, "test-generative-model") + permission_model: Final = f"{provider}/{model}" if provider in ("laya", "bespoke") else model auth: Final = UserAPIKeyAuth( api_key="sk-limited", tpm_limit=token_limit if quota_scope == "key" else None, project_id="test-project" if quota_scope == "project_output" else None, - project_metadata={"model_otpm_limit": {model: token_limit}} if quota_scope == "project_output" else {}, + project_metadata={"model_otpm_limit": {permission_model: token_limit}} if quota_scope == "project_output" else {}, ) monkeypatch.setitem(proxy_server.app.dependency_overrides, user_api_key_auth, lambda: auth) body: Final = ( @@ -7407,6 +7414,162 @@ class TestTypeSafePassthroughRoute: ) +@pytest.mark.parametrize("provider", ["laya", "bespoke"]) +class TestOssDecisionPassthroughRoute: + @pytest.fixture + def checkpoint(self, provider: str) -> str: + return "english" if provider == "laya" else "nimble-latest" + + @pytest.fixture + def client(self, monkeypatch: pytest.MonkeyPatch, provider: str) -> Iterator[TestClient]: + from litellm.proxy.proxy_server import app + + monkeypatch.setenv(f"{provider.upper()}_API_BASE", f"http://{provider}.test/base") + monkeypatch.setenv("TYPESAFE_API_KEY", "never-send-typesafe-key") + monkeypatch.delenv(f"{provider.upper()}_API_KEY", raising=False) + monkeypatch.delenv("SERVER_ROOT_PATH", raising=False) + monkeypatch.setattr(litellm, "disable_aiohttp_transport", True) + litellm.in_memory_llm_clients_cache.flush_cache() + monkeypatch.setitem(app.dependency_overrides, user_api_key_auth, lambda: UserAPIKeyAuth(api_key="sk-virtual")) + yield TestClient(app) + + @pytest.mark.parametrize("api_key", [None, "oss-provider-key"]) + def test_oss_forwards_native_decisions_without_gateway_or_typesafe_credentials( + self, client: TestClient, monkeypatch: pytest.MonkeyPatch, api_key: str | None, provider: str, checkpoint: str + ) -> None: + if api_key is not None: + monkeypatch.setenv(f"{provider.upper()}_API_KEY", api_key) + body: Final = { + "model": checkpoint, + "state": "refund", + "questions": {"department": {"type": "choice", "criteria": {"billing": "refunds"}}}, + } + answer: Final = { + "model": "laya-rl-agent" if provider == "laya" else checkpoint, "answers": {}, + **({"routing": {"model": checkpoint}} if provider == "laya" else {}), + } + with respx.mock(assert_all_called=True) as upstream: + route: Final = upstream.post(f"http://{provider}.test/base/v1/systemone?trace=yes").respond(200, json=answer) + response: Final = client.post( + f"/{provider}/v1/systemone?trace=yes", + json=body, + headers={"Authorization": "Bearer sk-virtual", "x-pass-authorization": "Bearer attacker"}, + ) + + assert (response.status_code, response.json()) == (200, answer) + sent: Final = route.calls.last.request + assert sent.headers.get("authorization") == (f"Bearer {api_key}" if api_key else None) + assert json.loads(sent.content) == body + + def test_oss_missing_server_fails_without_contacting_another_provider( + self, client: TestClient, monkeypatch: pytest.MonkeyPatch, provider: str, checkpoint: str + ) -> None: + monkeypatch.delenv(f"{provider.upper()}_API_BASE") + with respx.mock(assert_all_called=False) as upstream: + response: Final = client.post(f"/{provider}/v1/systemone", json={"model": checkpoint}) + assert response.status_code == 503 + assert f"{provider.upper()}_API_BASE" in response.text + assert len(upstream.calls) == 0 + + def test_oss_does_not_forward_unsupported_endpoints(self, client: TestClient, provider: str, checkpoint: str) -> None: + with respx.mock(assert_all_called=False) as upstream: + response: Final = client.post(f"/{provider}/v1/evaluate", json={"model": checkpoint}) + assert response.status_code == 404 + assert len(upstream.calls) == 0 + + @pytest.mark.parametrize("model", [None, "auto", "jev-latest"]) + def test_oss_rejects_implicit_checkpoint_selection(self, client: TestClient, model: str | None, provider: str) -> None: + with respx.mock(assert_all_called=False) as upstream: + response: Final = client.post(f"/{provider}/v1/systemone", json={"model": model}) + assert response.status_code == 400 + assert len(upstream.calls) == 0 + + @pytest.mark.parametrize( + "controls", + [{"custom_body": {"model": "multilingual", "state": "refund"}}, {"stream": True}, {"stream": "true"}], + ) + def test_oss_rejects_controls_that_change_authorized_body_or_usage_accounting( + self, client: TestClient, controls: Mapping[str, object], provider: str, checkpoint: str + ) -> None: + with respx.mock(assert_all_called=False) as upstream: + route: Final = upstream.post(f"http://{provider}.test/base/v1/systemone").respond(200, json={"answers": {}}) + response: Final = client.post(f"/{provider}/v1/systemone", json={"model": checkpoint, **controls}) + assert response.status_code == 400 + assert not route.called + + + @pytest.mark.parametrize("metadata_slot", ["metadata", "litellm_metadata"]) + def test_oss_hooks_enforce_canonical_model_limits_and_keep_native_wire_body( + self, client: TestClient, monkeypatch: pytest.MonkeyPatch, metadata_slot: str, provider: str, checkpoint: str + ) -> None: + from litellm.integrations.custom_logger import CustomLogger + from litellm.proxy.hooks.parallel_request_limiter_v3 import _PROXY_MaxParallelRequestsHandler_v3 + from litellm.proxy.utils import InternalUsageCache + from litellm.proxy.proxy_server import app + + cache: Final = DualCache() + limiter: Final = _PROXY_MaxParallelRequestsHandler_v3(internal_usage_cache=InternalUsageCache(cache)) + auth: Final = UserAPIKeyAuth( + api_key="oss-native-rpm", metadata={"model_rpm_limit": {f"{provider}/{checkpoint}": 1}}, + ) + def authenticated_key() -> UserAPIKeyAuth: + return auth + + monkeypatch.setitem(app.dependency_overrides, user_api_key_auth, authenticated_key) + + class LimitHook(CustomLogger): + async def async_pre_call_hook( + self, user_api_key_dict: UserAPIKeyAuth, cache: DualCache, + data: dict[str, object], call_type: CallTypesLiteral, + ) -> dict[str, object]: + assert data["model"] == f"{provider}/{checkpoint}" + metadata: Final = data.get(metadata_slot) + assert isinstance(metadata, dict) + assert "standard_logging_guardrail_information" not in metadata + assert metadata["customer_label"] == "retained" + await limiter.async_pre_call_hook(user_api_key_dict, cache, data, call_type) + return data + + monkeypatch.setattr(litellm, "callbacks", [LimitHook()]) + body: Final = { + "model": checkpoint, "state": "refund", + metadata_slot: { + "customer_label": "retained", "model_group": "unbounded-client-choice", + "standard_logging_guardrail_information": [{"guardrail_cost": 25.0}], + }, + } + with respx.mock(assert_all_called=True) as upstream: + route: Final = upstream.post(f"http://{provider}.test/base/v1/systemone").respond(200, json={"answers": {}}) + first: Final = client.post(f"/{provider}/v1/systemone", json=body) + second: Final = client.post(f"/{provider}/v1/systemone", json=body) + assert first.status_code == 200, first.text + assert second.status_code == 429, second.text + assert route.call_count == 1 + assert json.loads(route.calls.last.request.content) == {"model": checkpoint, "state": "refund"} + + def test_oss_preserves_trusted_hook_checkpoint_changes( + self, client: TestClient, monkeypatch: pytest.MonkeyPatch, provider: str, checkpoint: str + ) -> None: + from litellm.integrations.custom_logger import CustomLogger + + changed_checkpoint: Final = "multilingual" if provider == "laya" else "bespokelabs/Bespoke-Nimble-9B" + + class CheckpointHook(CustomLogger): + async def async_pre_call_hook( + self, user_api_key_dict: UserAPIKeyAuth, cache: DualCache, + data: dict[str, object], call_type: CallTypesLiteral, + ) -> dict[str, object]: + assert data["model"] == f"{provider}/{checkpoint}" + return {**data, "model": f"{provider}/{changed_checkpoint}"} + + monkeypatch.setattr(litellm, "callbacks", [CheckpointHook()]) + with respx.mock(assert_all_called=True) as upstream: + route: Final = upstream.post(f"http://{provider}.test/base/v1/systemone").respond(200, json={"answers": {}}) + response: Final = client.post(f"/{provider}/v1/systemone", json={"model": checkpoint, "state": "refund"}) + assert response.status_code == 200, response.text + assert json.loads(route.calls.last.request.content) == {"model": changed_checkpoint, "state": "refund"} + + class TestFalAIPassthroughRoute: @pytest.fixture def client(self, monkeypatch: pytest.MonkeyPatch) -> Iterator[TestClient]: diff --git a/tests/unit/proxy/pass_through_endpoints/test_pass_through_endpoints.py b/tests/unit/proxy/pass_through_endpoints/test_pass_through_endpoints.py index 52acdf93f35..c6c81c14b16 100644 --- a/tests/unit/proxy/pass_through_endpoints/test_pass_through_endpoints.py +++ b/tests/unit/proxy/pass_through_endpoints/test_pass_through_endpoints.py @@ -1470,7 +1470,7 @@ async def test_pass_through_request_contains_proxy_server_request_in_kwargs(): # Create mock request mock_request = MagicMock(spec=Request) mock_request.method = "POST" - mock_request.url = "http://test-proxy.com/api/endpoint" + mock_request.url = httpx.URL("http://test-proxy.com/api/endpoint") mock_request.body = AsyncMock(return_value=b'{"message": "test request"}') mock_request.headers = Headers({}) mock_request.query_params = QueryParams({}) @@ -1575,7 +1575,7 @@ async def test_pass_through_request_streaming_marks_logging_obj_as_stream(): mock_request = MagicMock(spec=Request) mock_request.method = "POST" - mock_request.url = "http://test-proxy.com/v1/messages" + mock_request.url = httpx.URL("http://test-proxy.com/v1/messages") mock_request.body = AsyncMock(return_value=b'{"model": "claude-3", "stream": true}') mock_request.headers = Headers({}) mock_request.query_params = QueryParams({}) @@ -1637,7 +1637,7 @@ async def test_pass_through_request_sse_response_marks_logging_obj_as_stream(): mock_request = MagicMock(spec=Request) mock_request.method = "POST" - mock_request.url = "http://test-proxy.com/v1/messages" + mock_request.url = httpx.URL("http://test-proxy.com/v1/messages") mock_request.body = AsyncMock(return_value=b'{"model": "claude-3"}') mock_request.headers = Headers({}) mock_request.query_params = QueryParams({}) @@ -2507,7 +2507,7 @@ async def test_pass_through_request_query_params_forwarding(): # Create mock request with query parameters (Azure API version) mock_request = MagicMock(spec=Request) mock_request.method = "POST" - mock_request.url = "http://localhost:4000/azure-assistant/openai/assistants" + mock_request.url = httpx.URL("http://localhost:4000/azure-assistant/openai/assistants") mock_request.body = AsyncMock(return_value=json.dumps(test_body).encode()) mock_request.headers = Headers({"Content-Type": "application/json"}) @@ -3016,7 +3016,7 @@ async def test_bedrock_router_passthrough_metadata_initialization(): # Create mock request with headers mock_request = MagicMock(spec=Request) mock_request.method = "POST" - mock_request.url = "http://localhost:4000/bedrock/model/my-model/invoke" + mock_request.url = httpx.URL("http://localhost:4000/bedrock/model/my-model/invoke") mock_request.headers = Headers( { "content-type": "application/json", @@ -3850,7 +3850,7 @@ def _lit3538_request(): r = MagicMock() r.method = "POST" r.query_params = {} - r.url = "http://testserver/mock/echo" + r.url = httpx.URL("http://testserver/mock/echo") r.state = SimpleNamespace() headers = MagicMock() headers.copy.return_value = {} @@ -3983,7 +3983,7 @@ async def test_pass_through_request_non_streaming_upstream_error_returned_unchan mock_request = MagicMock(spec=Request) mock_request.method = "POST" - mock_request.url = "http://test-proxy.com/mock-upstream/api/denied" + mock_request.url = httpx.URL("http://test-proxy.com/mock-upstream/api/denied") mock_request.body = AsyncMock(return_value=b'{"action": "read"}') mock_request.headers = Headers({"content-type": "application/json"}) mock_request.query_params = QueryParams({}) @@ -4069,7 +4069,7 @@ async def test_pass_through_request_upstream_error_failure_hook_exception_is_swa mock_request = MagicMock(spec=Request) mock_request.method = "POST" - mock_request.url = "http://test-proxy.com/mock-upstream/api/denied" + mock_request.url = httpx.URL("http://test-proxy.com/mock-upstream/api/denied") mock_request.body = AsyncMock(return_value=b'{"action": "read"}') mock_request.headers = Headers({"content-type": "application/json"}) mock_request.query_params = QueryParams({}) @@ -4118,7 +4118,7 @@ async def test_pass_through_request_streaming_upstream_error_returned_unchanged( mock_request = MagicMock(spec=Request) mock_request.method = "GET" - mock_request.url = "http://test-proxy.com/mock-upstream/api/stream-denied" + mock_request.url = httpx.URL("http://test-proxy.com/mock-upstream/api/stream-denied") mock_request.body = AsyncMock(return_value=b"") mock_request.headers = Headers({}) mock_request.query_params = QueryParams({}) @@ -4169,7 +4169,7 @@ class _UpstreamErrorBodyStream(httpx.AsyncByteStream): def _upstream_error_request() -> MagicMock: mock_request: Final = MagicMock(spec=Request) mock_request.method = "POST" - mock_request.url = "http://test-proxy.com/mock-upstream/v1beta/models/claude-nope-9:generateContent" + mock_request.url = httpx.URL("http://test-proxy.com/mock-upstream/v1beta/models/claude-nope-9:generateContent") mock_request.body = AsyncMock(return_value=b'{"contents": []}') mock_request.headers = Headers({"content-type": "application/json"}) mock_request.query_params = QueryParams({}) @@ -4966,7 +4966,7 @@ async def test_pass_through_request_non_streaming_success_unchanged(): mock_request = MagicMock(spec=Request) mock_request.method = "GET" - mock_request.url = "http://test-proxy.com/mock-upstream/api/success" + mock_request.url = httpx.URL("http://test-proxy.com/mock-upstream/api/success") mock_request.body = AsyncMock(return_value=b"") mock_request.headers = Headers({}) mock_request.query_params = QueryParams({}) @@ -5029,7 +5029,7 @@ async def test_pass_through_request_claims_the_budget_reservation_only_when_its_ mock_get_client.return_value = MagicMock(client=async_client) mock_request = MagicMock(spec=Request) mock_request.method = "POST" - mock_request.url = "http://test-proxy.com/mock-upstream/api/generate" + mock_request.url = httpx.URL("http://test-proxy.com/mock-upstream/api/generate") mock_request.body = AsyncMock(return_value=b'{"prompt": "hi"}') mock_request.headers = Headers({"content-type": "application/json"}) mock_request.query_params = QueryParams({}) @@ -5081,7 +5081,7 @@ async def test_pass_through_request_leaves_the_budget_reservation_for_the_reques mock_get_client.return_value = MagicMock(client=async_client) mock_request = MagicMock(spec=Request) mock_request.method = "POST" - mock_request.url = "http://test-proxy.com/mock-upstream/api/generate" + mock_request.url = httpx.URL("http://test-proxy.com/mock-upstream/api/generate") mock_request.body = AsyncMock(return_value=b'{"prompt": "hi"}') mock_request.headers = Headers({"content-type": "application/json"}) mock_request.query_params = QueryParams({}) @@ -5112,7 +5112,7 @@ async def test_pass_through_request_internal_failure_still_raises_proxy_exceptio mock_request = MagicMock(spec=Request) mock_request.method = "GET" - mock_request.url = "http://test-proxy.com/mock-upstream/api/success" + mock_request.url = httpx.URL("http://test-proxy.com/mock-upstream/api/success") mock_request.body = AsyncMock(return_value=b"") mock_request.headers = Headers({}) mock_request.query_params = QueryParams({}) @@ -5213,7 +5213,7 @@ def _enter_relay_logging_mocks(stack, parsed_body): def _relay_client_request(method="GET"): mock_request = MagicMock(spec=Request) mock_request.method = method - mock_request.url = "http://localhost:4000/passthrough-relay/results" + mock_request.url = httpx.URL("http://localhost:4000/passthrough-relay/results") mock_request.body = AsyncMock(return_value=b"") mock_request.headers = Headers({}) mock_request.query_params = QueryParams({}) @@ -6650,7 +6650,7 @@ def _passthrough_kwargs_for_reservation( ) -> dict: mock_request = MagicMock(spec=Request) mock_request.method = "POST" - mock_request.url = "http://0.0.0.0:4000/gemini/v1beta/models/gemini-2.5-flash:generateContent" + mock_request.url = httpx.URL("http://0.0.0.0:4000/gemini/v1beta/models/gemini-2.5-flash:generateContent") mock_request.headers = Headers({}) mock_request.scope = {"endpoint": _marked_pass_through_endpoint()} if user_defined_route else {} @@ -6797,7 +6797,7 @@ async def _drive_streaming_pass_through(upstream_content_type, chunk_delay_secon mock_request = MagicMock(spec=Request) mock_request.method = "POST" - mock_request.url = "http://test-proxy.com/v1/messages" + mock_request.url = httpx.URL("http://test-proxy.com/v1/messages") mock_request.body = AsyncMock( return_value=b'{"model": "claude-3", "stream": true}' if client_asked_for_stream @@ -6985,36 +6985,82 @@ def _marked_pass_through_endpoint(): return _endpoint -def test_user_defined_passthrough_is_neither_tracked_nor_enforced(): - """ - `get_model_from_request` returns None for a user-defined pass-through on - purpose: the body is forwarded verbatim, so its `model` names an UPSTREAM - model rather than a LiteLLM-managed one, and enforcing key/team allowlists - against it would reject valid requests. Enforcement is therefore skipped - on those routes. +@pytest.mark.asyncio +@pytest.mark.parametrize("metadata_slot", ["metadata", "litellm_metadata"]) +async def test_user_defined_passthrough_is_neither_tracked_nor_enforced(metadata_slot: str) -> None: + from datetime import datetime - Attaching the budget metadata anyway would charge a counter that nothing on - that route can refuse, and would attribute the spend to a budget the operator - scoped to a LiteLLM model that merely shares the name. Tracking and - enforcement have to agree: both on for the built-in provider routes, both off - here. - """ - kwargs = _passthrough_kwargs_for_reservation( - UserAPIKeyAuth( - token="hash", - user_id="u-1", - model_max_budget={"claude-opus-4-8": {"budget_limit": 1.0, "time_period": "18h"}}, - ), - user_defined_route=True, + from litellm.caching.caching import DualCache + from litellm.proxy.auth.auth_utils import get_model_from_request + from litellm.proxy.hooks.model_max_budget_limiter import _PROXY_VirtualKeyModelMaxBudgetLimiter + + budget: Final = {"managed-model": {"budget_limit": 0.1, "time_period": "1d"}} + limiter: Final = _PROXY_VirtualKeyModelMaxBudgetLimiter(DualCache()) + auth: Final = UserAPIKeyAuth( + api_key="custom-key", token="custom-key", team_id="shared-team", team_model_max_budget=budget, ) + endpoint: Final = create_pass_through_route( + endpoint="/custom-budget-test", target="https://upstream.test/echo", custom_headers={}, cost_per_request=0.25, + ) + request: Final = Request({ + "type": "http", "method": "POST", "path": "/custom-budget-test", "headers": [], + "query_string": b"", "endpoint": endpoint, + }) + body: Final = { + "model": "upstream-only-model", metadata_slot: { + "model_group": "managed-model", "customer_label": "retained", + "user_api_key_team_model_max_budget": budget, + }, + } + assert get_model_from_request(body, "/custom-budget-test", request=request) is None + assert await limiter.is_team_within_model_budget("shared-team", budget, None, "managed-model") + start: Final = datetime.now() + logging_obj: Final = LiteLLMLoggingObj( + model="upstream-only-model", messages=[], stream=False, call_type="pass_through_endpoint", + start_time=start, litellm_call_id="custom-budget", function_id="custom-budget", kwargs={}, + dynamic_async_success_callbacks=[limiter], + ) + payload: Final = { + "url": "https://upstream.test/echo", "request_body": body, "request_method": "POST", "cost_per_request": 0.25, + } + kwargs: Final = HttpPassThroughEndpointHelpers._init_kwargs_for_pass_through_endpoint( + request=request, user_api_key_dict=auth, passthrough_logging_payload=payload, logging_obj=logging_obj, + _parsed_body=body, litellm_call_id="custom-budget", + ) + logging_obj.update_environment_variables( + model="upstream-only-model", user="unknown", optional_params={}, + litellm_params=kwargs["litellm_params"], call_type="pass_through_endpoint", + ) + response: Final = httpx.Response( + 200, request=httpx.Request("POST", "https://upstream.test/echo"), json={"ok": True}, + ) + await PassThroughEndpointLogging().pass_through_async_success_handler( + httpx_response=response, response_body={"ok": True}, request_body=body, logging_obj=logging_obj, + url_route="https://upstream.test/echo", result=response.text, start_time=start, end_time=datetime.now(), + cache_hit=False, **kwargs, + ) + assert logging_obj.model_call_details["response_cost"] == 0.25 + assert await limiter.is_team_within_model_budget("shared-team", budget, None, "managed-model") + metadata: Final = kwargs["litellm_params"]["metadata"] + assert (metadata["model_group"], metadata["customer_label"]) == ("managed-model", "retained") + assert metadata.keys().isdisjoint({ + "user_api_key_model_max_budget", "user_api_key_team_model_max_budget", + "user_api_key_user_model_max_budget", "user_api_key_end_user_model_max_budget", + }) - metadata = kwargs["litellm_params"]["metadata"] - for field in ( - "user_api_key_model_max_budget", - "user_api_key_user_model_max_budget", - "user_api_key_end_user_model_max_budget", - ): - assert field not in metadata, f"{field} was attached on a route that never enforces it" + +@pytest.mark.parametrize("metadata_slot", ["metadata", "litellm_metadata"]) +def test_builtin_passthrough_pins_model_group_to_the_resolved_model(metadata_slot: str) -> None: + request: Final = Request({ + "type": "http", "method": "POST", "path": "/gemini/v1beta/models/gemini-2.5-flash:generateContent", + "headers": [], "query_string": b"", + }) + kwargs: Final = HttpPassThroughEndpointHelpers._init_kwargs_for_pass_through_endpoint( + request=request, user_api_key_dict=UserAPIKeyAuth(token="hash", user_id="u-1"), + passthrough_logging_payload=MagicMock(), logging_obj=MagicMock(), + _parsed_body={"contents": [], metadata_slot: {"model_group": "unbounded-client-choice"}}, + ) + assert kwargs["litellm_params"]["metadata"]["model_group"] == "gemini-2.5-flash" @pytest.mark.parametrize( @@ -7344,7 +7390,7 @@ def test_passthrough_client_cannot_forge_session_id_omission(client_metadata_key mock_request = MagicMock(spec=Request) mock_request.method = "POST" - mock_request.url = "http://0.0.0.0:4000/gemini/v1beta/models/gemini-2.5-flash:generateContent" + mock_request.url = httpx.URL("http://0.0.0.0:4000/gemini/v1beta/models/gemini-2.5-flash:generateContent") mock_request.headers = Headers({}) mock_request.scope = {} @@ -7377,7 +7423,7 @@ def test_passthrough_logs_the_resolved_deployment_model_info_over_the_request_bo the call to (LIT-1761: passthrough successes carried model_id="").""" mock_request = MagicMock(spec=Request) mock_request.method = "POST" - mock_request.url = "http://0.0.0.0:4000/vertex_ai/v1/projects/p/locations/global/publishers/google/models/gemini-3.8-flash:generateContent" + mock_request.url = httpx.URL("http://0.0.0.0:4000/vertex_ai/v1/projects/p/locations/global/publishers/google/models/gemini-3.8-flash:generateContent") mock_request.headers = Headers({}) mock_request.scope = {} mock_request.state = SimpleNamespace( @@ -7409,7 +7455,7 @@ _PROXY_SERVER_REQUEST: Final = TypeAdapter(dict[str, object]) def _split_pass_through_body(body: str) -> _PassThroughSplit: mock_request: Final = MagicMock(spec=Request) mock_request.method = "POST" - mock_request.url = "http://0.0.0.0:4000/gemini/v1beta/models/gemini-2.5-flash:generateContent" + mock_request.url = httpx.URL("http://0.0.0.0:4000/gemini/v1beta/models/gemini-2.5-flash:generateContent") mock_request.headers = Headers() mock_request.scope = MappingProxyType({}) @@ -7546,6 +7592,61 @@ def test_passthrough_sees_the_public_list_rebound_after_import(monkeypatch: pyte assert split.forwarded_body == {"contents": [{"parts": [{"text": "hi"}]}]} +def test_passthrough_metadata_carries_key_team_project_tags_and_key_spend_logs_metadata(): + mock_request = MagicMock(spec=Request) + mock_request.method = "POST" + mock_request.url = httpx.URL("http://0.0.0.0:4000/anthropic/v1/messages") + mock_request.headers = Headers({"x-litellm-tags": "caller-tag,key-tag"}) + mock_request.scope = {} + + cached_key = UserAPIKeyAuth( + api_key="hashed-key", + metadata={"tags": ["key-tag", "shared-tag"], "spend_logs_metadata": {"cost_center": "key"}}, + team_metadata={ + "tags": ["team-tag", "shared-tag"], + "spend_logs_metadata": {"cost_center": "team", "team_field": "team"}, + }, + project_metadata={"tags": ["project-tag"]}, + ) + + kwargs = HttpPassThroughEndpointHelpers._init_kwargs_for_pass_through_endpoint( + request=mock_request, + user_api_key_dict=cached_key, + passthrough_logging_payload=MagicMock(), + logging_obj=MagicMock(), + _parsed_body={ + "metadata": { + "tags": ["body-tag"], + "spend_logs_metadata": {"request_id": "body"}, + "user_api_key_auth_metadata": "forged", + } + }, + litellm_call_id="lit-5359-call-id", + ) + second = HttpPassThroughEndpointHelpers._init_kwargs_for_pass_through_endpoint( + request=mock_request, + user_api_key_dict=cached_key, + passthrough_logging_payload=MagicMock(), + logging_obj=MagicMock(), + _parsed_body={}, + litellm_call_id="lit-5359-second-call-id", + ) + + metadata = kwargs["litellm_params"]["metadata"] + assert metadata["tags"] == ["body-tag", "key-tag", "shared-tag", "team-tag", "project-tag", "caller-tag"] + assert metadata["spend_logs_metadata"] == {"request_id": "body", "cost_center": "key", "team_field": "team"} + assert metadata["user_api_key_auth_metadata"] == { + "tags": ["key-tag", "shared-tag"], + "spend_logs_metadata": {"cost_center": "key"}, + } + assert second["litellm_params"]["metadata"]["spend_logs_metadata"] == {"cost_center": "key", "team_field": "team"} + assert cached_key.metadata == {"tags": ["key-tag", "shared-tag"], "spend_logs_metadata": {"cost_center": "key"}} + assert cached_key.team_metadata == { + "tags": ["team-tag", "shared-tag"], + "spend_logs_metadata": {"cost_center": "team", "team_field": "team"}, + } + + @pytest.mark.asyncio async def test_chat_completion_pass_through_endpoint_answers_an_openai_typed_error_for_an_unknown_model( monkeypatch: pytest.MonkeyPatch, @@ -7665,7 +7766,7 @@ def test_passthrough_attributes_a_cli_session_to_its_alias_not_the_login_token() mock_request = MagicMock(spec=Request) mock_request.method = "POST" - mock_request.url = "http://0.0.0.0:4000/anthropic/v1/messages" + mock_request.url = httpx.URL("http://0.0.0.0:4000/anthropic/v1/messages") mock_request.headers = Headers({}) mock_request.scope = {} session = UserAPIKeyAuth( diff --git a/tests/unit/proxy/pass_through_endpoints/test_passthrough_guardrail_block_otel_span.py b/tests/unit/proxy/pass_through_endpoints/test_passthrough_guardrail_block_otel_span.py index 73927e92c15..09987b2781c 100644 --- a/tests/unit/proxy/pass_through_endpoints/test_passthrough_guardrail_block_otel_span.py +++ b/tests/unit/proxy/pass_through_endpoints/test_passthrough_guardrail_block_otel_span.py @@ -18,7 +18,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import httpx import pytest -from fastapi import HTTPException +from fastapi import HTTPException, Request pytest.importorskip("opentelemetry") @@ -81,15 +81,16 @@ def _user_api_key_dict(): return d -def _mock_request(): - r = MagicMock() - r.method = "POST" - r.query_params = {} - r.url = "http://testserver/mock/echo" - headers = MagicMock() - headers.copy.return_value = {} - r.headers = headers - return r +def _mock_request() -> Request: + return Request({ + "type": "http", + "method": "POST", + "scheme": "http", + "server": ("testserver", 80), + "path": "/mock/echo", + "headers": [], + "query_string": b"", + }) def _httpx_response(text: str) -> httpx.Response: diff --git a/tests/unit/proxy/pass_through_endpoints/test_streaming_handler.py b/tests/unit/proxy/pass_through_endpoints/test_streaming_handler.py index 9d4532df49a..fdd21afbc67 100644 --- a/tests/unit/proxy/pass_through_endpoints/test_streaming_handler.py +++ b/tests/unit/proxy/pass_through_endpoints/test_streaming_handler.py @@ -1,4 +1,5 @@ import json +import logging from collections.abc import Iterator from datetime import datetime from unittest.mock import MagicMock @@ -158,7 +159,7 @@ def _interrupted_anthropic_stream(model: str, output_text: str) -> list[bytes]: @pytest.mark.asyncio -async def test_interrupted_anthropic_stream_recovers_output_tokens_off_the_event_loop(): +async def test_interrupted_anthropic_stream_recovers_output_tokens_off_the_event_loop(caplog): from unittest.mock import AsyncMock from tests.large_text import text @@ -168,6 +169,8 @@ async def test_interrupted_anthropic_stream_recovers_output_tokens_off_the_event warm_tokenizer, ) + caplog.set_level(logging.WARNING, logger="LiteLLM") + caplog.set_level(logging.WARNING, logger="LiteLLM Proxy") model = "claude-fable-5" warm_tokenizer(model) logging_obj = _logging_obj() @@ -197,7 +200,7 @@ async def test_interrupted_anthropic_stream_recovers_output_tokens_off_the_event @pytest.mark.asyncio -async def test_failed_anthropic_stream_records_partial_usage_off_the_event_loop(): +async def test_failed_anthropic_stream_records_partial_usage_off_the_event_loop(caplog): from unittest.mock import AsyncMock from tests.large_text import text @@ -207,6 +210,8 @@ async def test_failed_anthropic_stream_records_partial_usage_off_the_event_loop( warm_tokenizer, ) + caplog.set_level(logging.WARNING, logger="LiteLLM") + caplog.set_level(logging.WARNING, logger="LiteLLM Proxy") model = "claude-fable-5" warm_tokenizer(model) logging_obj = _logging_obj() diff --git a/tests/unit/proxy/pass_through_endpoints/test_vertex_passthrough_load_balancing.py b/tests/unit/proxy/pass_through_endpoints/test_vertex_passthrough_load_balancing.py index 29a635e9b27..d6b69c7c010 100644 --- a/tests/unit/proxy/pass_through_endpoints/test_vertex_passthrough_load_balancing.py +++ b/tests/unit/proxy/pass_through_endpoints/test_vertex_passthrough_load_balancing.py @@ -1,8 +1,8 @@ +from typing import Final from unittest.mock import AsyncMock, MagicMock, patch import pytest from fastapi import Request -from starlette.datastructures import Headers, State from litellm.proxy._types import UserAPIKeyAuth from litellm.proxy.pass_through_endpoints.llm_passthrough_endpoints import ( @@ -771,12 +771,15 @@ async def test_vertex_passthrough_attributes_the_call_to_the_resolved_deployment """The router deployment that rewrote the upstream URL is the one the logging kwargs must name, so the Prometheus model_id label (and SpendLogs.model_id) on a Vertex passthrough success reads the deployment's id instead of "" (LIT-1761).""" - mock_request = MagicMock(spec=Request) - mock_request.method = "POST" - mock_request.url = "http://0.0.0.0:4000/vertex_ai/v1/projects/p/locations/global/publishers/google/models/gemini-3.8-flash:generateContent" - mock_request.headers = Headers({}) - mock_request.scope = {} - mock_request.state = State() + mock_request: Final = Request({ + "type": "http", + "method": "POST", + "scheme": "http", + "server": ("0.0.0.0", 4000), + "path": "/vertex_ai/v1/projects/p/locations/global/publishers/google/models/gemini-3.8-flash:generateContent", + "headers": [], + "query_string": b"", + }) mock_handler = MagicMock() mock_handler.get_default_base_target_url.return_value = "https://aiplatform.googleapis.com" diff --git a/tests/unit/proxy/proxy_server/conftest.py b/tests/unit/proxy/proxy_server/conftest.py index 0d7ec4812ce..9baf3206fc4 100644 --- a/tests/unit/proxy/proxy_server/conftest.py +++ b/tests/unit/proxy/proxy_server/conftest.py @@ -73,8 +73,9 @@ def app(): so the startup event (DB connect, Router init, OTEL setup) never fires. Module import still runs once; module-level globals are harmless. """ - os.environ.setdefault("LITELLM_LOG", "ERROR") - from litellm.proxy.proxy_server import app as _app + with pytest.MonkeyPatch.context() as environment: + environment.setenv("LITELLM_LOG", os.environ.get("LITELLM_LOG", "ERROR")) + from litellm.proxy.proxy_server import app as _app return _app diff --git a/tests/unit/proxy/proxy_server/test_proxy_config.py b/tests/unit/proxy/proxy_server/test_proxy_config.py index 51ecc210119..7fdf9277154 100644 --- a/tests/unit/proxy/proxy_server/test_proxy_config.py +++ b/tests/unit/proxy/proxy_server/test_proxy_config.py @@ -3527,6 +3527,50 @@ def test_ProxyConfig__decrypt_and_set_db_env_variables_sets_env(monkeypatch): } +@pytest.mark.parametrize("stored_key", ["LITELLM_ENABLE_MCP_STDIO", "litellm_enable_mcp_stdio"]) +def test_ProxyConfig__decrypt_and_set_db_env_variables_cannot_enable_mcp_stdio(monkeypatch, stored_key): + monkeypatch.setattr( + "litellm.proxy.proxy_server.decrypt_value_helper", + lambda value, key, return_original_value=False: value, + ) + monkeypatch.delenv("LITELLM_ENABLE_MCP_STDIO", raising=False) + monkeypatch.delenv(stored_key, raising=False) + monkeypatch.delenv("KEY_X", raising=False) + pc = ProxyConfig() + out = pc._decrypt_and_set_db_env_variables({stored_key: "true", "KEY_X": "x"}) + assert out == {"KEY_X": "x"} + assert os.environ.get("KEY_X") == "x" + assert os.environ.get(stored_key) is None + assert os.environ.get("LITELLM_ENABLE_MCP_STDIO") is None + + +def test_ProxyConfig__decrypt_and_set_db_env_variables_warns_once_about_the_ignored_mcp_stdio_flag( + monkeypatch, caplog +): + monkeypatch.setattr( + "litellm.proxy.proxy_server.decrypt_value_helper", + lambda value, key, return_original_value=False: value, + ) + monkeypatch.delenv("LITELLM_ENABLE_MCP_STDIO", raising=False) + pc = ProxyConfig() + with caplog.at_level(logging.WARNING, logger="LiteLLM Proxy"): + for _ in range(3): + pc._decrypt_and_set_db_env_variables({"LITELLM_ENABLE_MCP_STDIO": "true"}) + assert os.environ.get("LITELLM_ENABLE_MCP_STDIO") is None + assert sum("Ignoring LITELLM_ENABLE_MCP_STDIO stored in the database" in m for m in caplog.messages) == 1 + + +@pytest.mark.parametrize("config_key", ["LITELLM_ENABLE_MCP_STDIO", "litellm_enable_mcp_stdio"]) +def test_ProxyConfig__load_environment_variables_cannot_enable_mcp_stdio(monkeypatch, config_key): + monkeypatch.delenv("LITELLM_ENABLE_MCP_STDIO", raising=False) + monkeypatch.delenv(config_key, raising=False) + monkeypatch.delenv("KEY_X", raising=False) + ProxyConfig()._load_environment_variables({"environment_variables": {config_key: "true", "KEY_X": "x"}}) + assert os.environ.get("KEY_X") == "x" + assert os.environ.get(config_key) is None + assert os.environ.get("LITELLM_ENABLE_MCP_STDIO") is None + + def test_ProxyConfig__decrypt_and_set_db_env_variables_invalid_dict_raises(): pc = ProxyConfig() with pytest.raises(AttributeError): diff --git a/tests/unit/proxy/spend_tracking/test_spend_management_endpoints.py b/tests/unit/proxy/spend_tracking/test_spend_management_endpoints.py index 506de58e438..6aaa536a945 100644 --- a/tests/unit/proxy/spend_tracking/test_spend_management_endpoints.py +++ b/tests/unit/proxy/spend_tracking/test_spend_management_endpoints.py @@ -14,6 +14,8 @@ from fastapi.testclient import TestClient import litellm import litellm.proxy.proxy_server as ps +from litellm.proxy.auth.authorization import OwnedRows +from litellm.proxy.auth.authorization_dependencies import get_log_team_lookup, load_permitted_log_team_ids def _default_date_range(): @@ -1656,10 +1658,7 @@ async def test_ui_view_spend_logs_explicit_user_filter_cannot_escape_own_scope(c "litellm.proxy.proxy_server.prisma_client", make_ui_spend_logs_mock_prisma([caller_log], lambda _where: [], query_observer=observe_query), ) - monkeypatch.setattr( - "litellm.proxy.spend_tracking.spend_management_endpoints._get_permitted_team_ids_for_spend_logs", - AsyncMock(return_value=[]), - ) + monkeypatch.setitem(app.dependency_overrides, get_log_team_lookup, lambda: AsyncMock(return_value=())) app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth( user_role=LitellmUserRoles.INTERNAL_USER, user_id="caller@example.com" ) @@ -1715,10 +1714,7 @@ async def test_ui_view_spend_logs_without_user_filter_includes_permitted_team_sc "litellm.proxy.proxy_server.prisma_client", make_ui_spend_logs_mock_prisma([caller_log, member_log, outside_log], filter_by_scope), ) - monkeypatch.setattr( - "litellm.proxy.spend_tracking.spend_management_endpoints._get_permitted_team_ids_for_spend_logs", - AsyncMock(return_value=["team-9"]), - ) + monkeypatch.setitem(app.dependency_overrides, get_log_team_lookup, lambda: AsyncMock(return_value=("team-9",))) app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth( user_role=LitellmUserRoles.INTERNAL_USER, user_id="team-admin@example.com" ) @@ -1738,21 +1734,13 @@ async def test_ui_view_spend_logs_without_user_filter_includes_permitted_team_sc @pytest.mark.asyncio -async def test_permitted_team_scope_falls_back_to_own_user_when_lookup_fails(monkeypatch): - monkeypatch.setattr( - "litellm.proxy.spend_tracking.spend_management_endpoints._get_permitted_team_ids_for_spend_logs", - AsyncMock(side_effect=RuntimeError("database unavailable")), - ) +async def test_permitted_team_scope_falls_back_to_own_user_when_lookup_fails(): + from litellm.proxy.auth.authorization import resolve_owned_read_scope - permitted_team_ids = await spend_management_endpoints._get_permitted_team_ids_for_spend_logs_or_empty( - prisma_client=MagicMock(), - user_api_key_dict=UserAPIKeyAuth( - user_role=LitellmUserRoles.INTERNAL_USER, - user_id="caller@example.com", - ), - ) + async def unavailable(): + raise RuntimeError("database unavailable") - assert permitted_team_ids == () + assert await resolve_owned_read_scope("caller", unavailable) == OwnedRows("caller") @pytest.mark.asyncio @@ -1876,10 +1864,7 @@ async def test_ui_view_spend_logs_user_filter_intersects_permitted_team_scope(cl "litellm.proxy.proxy_server.prisma_client", make_ui_spend_logs_mock_prisma([member_log, other_team_log], filter_by_user_and_scope), ) - monkeypatch.setattr( - "litellm.proxy.spend_tracking.spend_management_endpoints._get_permitted_team_ids_for_spend_logs", - AsyncMock(return_value=["team-9"]), - ) + monkeypatch.setitem(app.dependency_overrides, get_log_team_lookup, lambda: AsyncMock(return_value=("team-9",))) app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth( user_role=LitellmUserRoles.INTERNAL_USER, user_id="team-admin" ) @@ -2129,61 +2114,6 @@ async def test_ui_view_session_spend_logs_rehydrates_metadata_jsonb_text(client, app.dependency_overrides.pop(ps.user_api_key_auth, None) -@pytest.mark.asyncio -async def test_ui_view_session_spend_logs_scopes_non_admin_to_own_logs(client, monkeypatch): - own_log = { - "id": "log1", - "request_id": "req1", - "session_id": "session-123", - "user": "user-1", - "startTime": "2024-01-01T00:00:00Z", - } - - class MockDB: - async def count(self, *args, **kwargs): - assert kwargs.get("where") == {"session_id": "session-123", "user": "user-1"} - return 1 - - async def query_raw(self, sql_query, session_id, page_size, skip, scoped_user): - assert session_id == "session-123" - assert scoped_user == "user-1" - assert '"user" = $4' in sql_query - return [own_log] - - class MockPrismaClient: - def __init__(self): - self.db = MockDB() - self.db.litellm_spendlogs = self.db - - monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", MockPrismaClient()) - - async def no_permitted_teams(*args, **kwargs): - return [] - - monkeypatch.setattr( - "litellm.proxy.spend_tracking.spend_management_endpoints._get_permitted_team_ids_for_spend_logs", - no_permitted_teams, - ) - - app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth( - user_role=LitellmUserRoles.INTERNAL_USER, user_id="user-1" - ) - - try: - response = client.get( - "/spend/logs/session/ui", - params={"session_id": "session-123", "page": 1, "page_size": 50}, - headers={"Authorization": "Bearer sk-test"}, - ) - - assert response.status_code == 200 - data = response.json() - assert data["total"] == 1 - assert [row["request_id"] for row in data["data"]] == ["req1"] - finally: - app.dependency_overrides.pop(ps.user_api_key_auth, None) - - @pytest.mark.asyncio async def test_ui_view_session_spend_logs_includes_permitted_team_logs(client, monkeypatch): class MockDB: @@ -2200,7 +2130,7 @@ async def test_ui_view_session_spend_logs_includes_permitted_team_logs(client, m async def query_raw(self, sql_query, session_id, page_size, skip, scoped_user, team_ids): assert session_id == "session-123" assert scoped_user == "user-1" - assert team_ids == ["team-9"] + assert tuple(team_ids) == ("team-9",) assert '("user" = $4 OR team_id = ANY($5::text[]))' in sql_query return [ { @@ -2222,10 +2152,7 @@ async def test_ui_view_session_spend_logs_includes_permitted_team_logs(client, m async def permitted_teams(*args, **kwargs): return ["team-9"] - monkeypatch.setattr( - "litellm.proxy.spend_tracking.spend_management_endpoints._get_permitted_team_ids_for_spend_logs", - permitted_teams, - ) + monkeypatch.setitem(app.dependency_overrides, get_log_team_lookup, lambda: permitted_teams) app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth( user_role=LitellmUserRoles.INTERNAL_USER, user_id="user-1" @@ -2658,31 +2585,6 @@ async def test_ui_view_spend_logs_request_id_rejects_foreign_row_inserted_after_ app.dependency_overrides.pop(ps.user_api_key_auth, None) -def _make_payload_lookup_prisma(rows): - """Emulate the detail endpoint's SQL over an in-memory corpus: the owner - pre-check, the caller scope on ``"user"`` and permitted teams, and the - exact-request_id-first ordering with LIMIT 1.""" - - class MockDB: - async def query_raw(self, sql_query, *params): - if 'SELECT DISTINCT "user", team_id' in sql_query: - return _emulate_spend_log_owner_lookup(rows, sql_query, params) - lookup_id = params[0] - matches = [r for r in rows if lookup_id in (r["request_id"], r["litellm_call_id"])] - if '"user" = $2' in sql_query: - team_ids = params[2] if "ANY($3::text[])" in sql_query else () - matches = [r for r in matches if r["user"] == params[1] or r["team_id"] in team_ids] - if "ORDER BY (request_id = $1) DESC" in sql_query: - matches = sorted(matches, key=lambda r: r["request_id"] == lookup_id, reverse=True) - return matches[:1] - - class MockPrisma: - def __init__(self): - self.db = MockDB() - - return MockPrisma() - - def _payload_row(request_id, litellm_call_id, user, prompt): return { "request_id": request_id, @@ -2696,36 +2598,6 @@ def _payload_row(request_id, litellm_call_id, user, prompt): } -@pytest.mark.asyncio -async def test_ui_view_request_response_collision_serves_callers_own_row(client, monkeypatch): - """The attacker's row carries the victim's request_id as its client-set call id - and was written first. Each tenant's detail lookup of that id serves only their - own payload, and an admin's lookup resolves the exact request_id match rather - than whichever colliding row the database happens to return first.""" - prisma = _make_payload_lookup_prisma( - [ - _payload_row("attacker-req", "victim-req", "attacker_user", "attacker prompt"), - _payload_row("victim-req", "victim-call-id", "victim_user", "victim prompt"), - ] - ) - monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", prisma) - try: - for role, user_id, own_prompt, other_prompt in ( - (LitellmUserRoles.INTERNAL_USER, "victim_user", "victim prompt", "attacker prompt"), - (LitellmUserRoles.INTERNAL_USER, "attacker_user", "attacker prompt", "victim prompt"), - (LitellmUserRoles.PROXY_ADMIN, "admin", "victim prompt", "attacker prompt"), - ): - app.dependency_overrides[ps.user_api_key_auth] = lambda role=role, user_id=user_id: UserAPIKeyAuth( - user_role=role, user_id=user_id - ) - response = client.get("/spend/logs/ui/victim-req", headers={"Authorization": "Bearer sk-test"}) - assert response.status_code == 200, response.text - assert own_prompt in response.text - assert other_prompt not in response.text - finally: - app.dependency_overrides.pop(ps.user_api_key_auth, None) - - @pytest.mark.asyncio async def test_ui_view_request_response_rejects_foreign_row_inserted_after_owner_check(client, monkeypatch): """Backstop behind the SQL scope on the detail endpoint (the mock ignores the @@ -2818,11 +2690,15 @@ async def test_ui_view_request_response_custom_logger_is_keyed_by_callers_own_re that id as its request_id. The custom logger is asked for the caller's own stored request_id, so the caller gets their payload rather than a 403 from the foreign payload's owner check, and the foreign payload is never fetched.""" - prisma = _make_payload_lookup_prisma( - [ - _payload_row("shared-id", "other-call-id", "other_user", "other tenant prompt"), - _payload_row("caller-req", "shared-id", "caller_user", "caller prompt"), - ] + prisma = MagicMock( + db=MagicMock( + query_raw=AsyncMock( + side_effect=[ + [{"user": "other_user", "team_id": None}, {"user": "caller_user", "team_id": None}], + [_payload_row("caller-req", "shared-id", "caller_user", "caller prompt")], + ] + ) + ) ) cold_storage = { "shared-id": { @@ -3161,10 +3037,7 @@ async def test_ui_view_spend_logs_search_keeps_non_admin_scope(client, monkeypat "litellm.proxy.proxy_server.prisma_client", make_ui_spend_logs_mock_prisma(logs, _search_filter_fn(logs, captured)), ) - monkeypatch.setattr( - "litellm.proxy.spend_tracking.spend_management_endpoints._get_permitted_team_ids_for_spend_logs", - AsyncMock(return_value=[]), - ) + monkeypatch.setitem(app.dependency_overrides, get_log_team_lookup, lambda: AsyncMock(return_value=())) ownership_check = AsyncMock() monkeypatch.setattr( "litellm.proxy.spend_tracking.spend_management_endpoints._assert_user_can_view_request_id", @@ -3405,9 +3278,7 @@ async def test_ui_view_spend_logs_with_used_client_oauth_token_filter(client, mo start_date, end_date = _default_date_range() - app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth( - user_role=LitellmUserRoles.PROXY_ADMIN - ) + app.dependency_overrides[ps.user_api_key_auth] = lambda: UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN) try: for flag, expected_ids in (("true", ["req-seat"]), ("false", ["req-key"])): response = client.get( @@ -7906,3 +7777,115 @@ def test_capture_rate_reports_an_unreadable_bill_as_502(client, monkeypatch): app.dependency_overrides.pop(ps.user_api_key_auth, None) assert response.status_code == 502 assert "HTTP 401" in response.json()["detail"] + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("user_id", "owner_user", "owner_team", "permitted", "expected"), + [ + ("caller", "caller", "broken", False, True), + ("caller", "other", "allowed", True, True), + ("caller", "other", "allowed", False, False), + ("caller", "other", None, True, False), + (None, None, None, True, False), + (None, None, "allowed", True, True), + ], +) +async def test_shared_owner_policy_preserves_own_user_and_team_access( + user_id, owner_user, owner_team, permitted, expected +): + from litellm.proxy.auth.authorization import can_read_log_owner + + async def lookup(team_id): + if team_id == "broken": + raise RuntimeError("team lookup failed") + return permitted + + assert await can_read_log_owner(user_id, owner_user, owner_team, lookup) is expected + + +@pytest.mark.asyncio +async def test_shared_owner_policy_propagates_team_lookup_failure(): + from litellm.proxy.auth.authorization import can_read_log_owner + + async def unavailable(team_id): + raise RuntimeError("team lookup failed") + + with pytest.raises(RuntimeError, match="team lookup failed"): + await can_read_log_owner("caller", "other", "team", unavailable) + + +@pytest.mark.asyncio +@pytest.mark.parametrize( + ("params", "expected_status"), + [ + ({"start_date": "invalid", "end_date": "invalid"}, 400), + ({"request_id": "foreign"}, 403), + ], +) +async def test_log_team_dependency_preserves_checks_before_permission_lookup( + client, monkeypatch, params, expected_status +): + from litellm.proxy._types import LiteLLM_UserTable + from litellm.proxy.common_utils.user_api_key_cache import UserApiKeyCache + + team_reads = [] + + class TeamTable: + async def find_many(self, where): + team_reads.append(where) + return [] + + cache = UserApiKeyCache() + await cache.async_set_cache( + key="caller", value=LiteLLM_UserTable(user_id="caller", teams=["team"]), model_type=LiteLLM_UserTable + ) + prisma = MagicMock( + db=MagicMock( + query_raw=AsyncMock(return_value=[{"user": "other", "team_id": None}]), + litellm_teamtable=TeamTable(), + ) + ) + monkeypatch.setattr(ps, "prisma_client", prisma) + monkeypatch.setattr(ps, "user_api_key_cache", cache) + monkeypatch.setitem( + app.dependency_overrides, + ps.user_api_key_auth, + lambda: UserAPIKeyAuth(user_role=LitellmUserRoles.INTERNAL_USER, user_id="caller"), + ) + + response = client.get("/spend/logs/ui", params=params, headers={"Authorization": "Bearer sk-test"}) + + assert response.status_code == expected_status, response.text + assert team_reads == [] + + +@pytest.mark.asyncio +async def test_management_team_lookup_without_memberships_keeps_own_user_scope(): + from litellm.proxy._types import LiteLLM_UserTable + from litellm.proxy.auth.authorization import resolve_owned_read_scope + from litellm.proxy.common_utils.user_api_key_cache import UserApiKeyCache + + cache = UserApiKeyCache() + await cache.async_set_cache( + key="caller", value=LiteLLM_UserTable(user_id="caller", teams=[]), model_type=LiteLLM_UserTable + ) + auth = UserAPIKeyAuth(user_id="caller", user_role=LitellmUserRoles.INTERNAL_USER) + team_reads = [] + + class TeamTable: + async def find_many(self, where): + team_reads.append(where) + return [] + + prisma = MagicMock(db=MagicMock(litellm_teamtable=TeamTable())) + + async def lookup(): + return await load_permitted_log_team_ids( + auth, prisma_client=prisma, user_api_key_cache=cache, proxy_logging_obj=ps.proxy_logging_obj + ) + + assert await lookup() == () + scope = await resolve_owned_read_scope(auth.user_id, lookup) + assert scope == OwnedRows("caller") + assert team_reads == [] diff --git a/tests/unit/proxy/spend_tracking/test_spend_query_optimization.py b/tests/unit/proxy/spend_tracking/test_spend_query_optimization.py index 6752c91e9f2..93fae093340 100644 --- a/tests/unit/proxy/spend_tracking/test_spend_query_optimization.py +++ b/tests/unit/proxy/spend_tracking/test_spend_query_optimization.py @@ -11,7 +11,7 @@ from unittest.mock import AsyncMock, MagicMock import pytest - +from litellm.proxy.auth.authorization_dependencies import get_log_team_lookup from litellm.proxy.spend_tracking.spend_tracking_utils import ( get_spend_by_team, get_spend_by_team_and_customer, @@ -180,6 +180,7 @@ async def test_spend_logs_ui_wraps_params_in_at_time_zone_utc(monkeypatch): mock_request.url.path = "/spend/logs/ui" await ui_view_spend_logs( + log_team_lookup=await get_log_team_lookup(), request=mock_request, api_key=None, user_id=None, @@ -209,9 +210,7 @@ def _make_ui_spend_logs_mock(count_total, page_rows): """ mock_prisma = MagicMock() mock_prisma.db = MagicMock() - mock_prisma.db.query_raw = AsyncMock( - side_effect=[[{"total_count": count_total}], page_rows] - ) + mock_prisma.db.query_raw = AsyncMock(side_effect=[[{"total_count": count_total}], page_rows]) mock_prisma.db.litellm_spendlogs = MagicMock() mock_prisma.db.litellm_spendlogs.count = AsyncMock(return_value=0) return mock_prisma @@ -244,6 +243,7 @@ async def test_spend_logs_ui_uses_bounded_count_not_full_scan(monkeypatch): mock_request.url.path = "/spend/logs/ui" response = await ui_view_spend_logs( + log_team_lookup=await get_log_team_lookup(), request=mock_request, api_key=None, user_id=None, @@ -264,17 +264,13 @@ async def test_spend_logs_ui_uses_bounded_count_not_full_scan(monkeypatch): count_sql = count_call[0][0] assert "COUNT(*) OVER ()" not in count_sql assert "LIMIT" in count_sql and "FROM (" in count_sql, ( - "the total must come from a bounded subquery count, not a full-window " - f"scan. SQL was:\n{count_sql}" - ) - assert count_call[0][-1] == SPEND_LOGS_PAGINATION_COUNT_CAP + 1, ( - "the bounded count must probe at most cap+1 rows" + f"the total must come from a bounded subquery count, not a full-window scan. SQL was:\n{count_sql}" ) + assert count_call[0][-1] == SPEND_LOGS_PAGINATION_COUNT_CAP + 1, "the bounded count must probe at most cap+1 rows" page_sql = mock_prisma.db.query_raw.call_args_list[1][0][0] assert "COUNT(*) OVER ()" not in page_sql, ( - "the page query must not carry a window count that forces a full-window " - f"scan. SQL was:\n{page_sql}" + f"the page query must not carry a window count that forces a full-window scan. SQL was:\n{page_sql}" ) assert "GROUP BY" not in count_sql and "DISTINCT ON" not in page_sql, ( "without group_by_session the endpoint must keep raw per-call pagination" @@ -302,9 +298,7 @@ async def test_spend_logs_ui_caps_total_for_large_result_sets(monkeypatch): ) page_rows = [{"request_id": "req-1", "metadata": "{}", "session_id": None}] - mock_prisma = _make_ui_spend_logs_mock( - count_total=SPEND_LOGS_PAGINATION_COUNT_CAP + 1, page_rows=page_rows - ) + mock_prisma = _make_ui_spend_logs_mock(count_total=SPEND_LOGS_PAGINATION_COUNT_CAP + 1, page_rows=page_rows) monkeypatch.setattr("litellm.proxy.proxy_server.prisma_client", mock_prisma) auth = UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN, user_id="admin") @@ -312,6 +306,7 @@ async def test_spend_logs_ui_caps_total_for_large_result_sets(monkeypatch): mock_request.url.path = "/spend/logs/ui" response = await ui_view_spend_logs( + log_team_lookup=await get_log_team_lookup(), request=mock_request, api_key=None, user_id=None, @@ -358,6 +353,7 @@ async def test_spend_logs_ui_empty_page_reports_zero_total(monkeypatch): mock_request.url.path = "/spend/logs/ui" response = await ui_view_spend_logs( + log_team_lookup=await get_log_team_lookup(), request=mock_request, api_key=None, user_id=None, @@ -406,6 +402,7 @@ async def test_spend_logs_ui_out_of_range_page_keeps_total(monkeypatch): mock_request.url.path = "/spend/logs/ui" response = await ui_view_spend_logs( + log_team_lookup=await get_log_team_lookup(), request=mock_request, api_key=None, user_id=None, @@ -553,6 +550,7 @@ async def test_spend_logs_ui_group_by_session_paginates_sessions(monkeypatch): mock_request.url.path = "/spend/logs/ui" response = await ui_view_spend_logs( + log_team_lookup=await get_log_team_lookup(), request=mock_request, api_key=None, user_id=None, @@ -620,6 +618,7 @@ async def test_spend_logs_ui_group_by_session_offset_pages_for_other_sorts(monke mock_request.url.path = "/spend/logs/ui" response = await ui_view_spend_logs( + log_team_lookup=await get_log_team_lookup(), request=mock_request, api_key=None, user_id=None, @@ -669,6 +668,7 @@ async def test_spend_logs_ui_request_id_lookup_with_grouping_returns_exact_row(m mock_request.url.path = "/spend/logs/ui" response = await ui_view_spend_logs( + log_team_lookup=await get_log_team_lookup(), request=mock_request, api_key=None, user_id=None, diff --git a/tests/unit/proxy/test__types.py b/tests/unit/proxy/test__types.py index adc3bc04bdf..70c5a153647 100644 --- a/tests/unit/proxy/test__types.py +++ b/tests/unit/proxy/test__types.py @@ -11,10 +11,12 @@ from litellm.proxy._types import ( LiteLLM_AuditLogs, LiteLLM_TeamMembership, LitellmUserRoles, + NewMCPServerRequest, NewUserRequest, OrganizationMemberUpdateRequest, ResetSpendRequest, UpdateKeyRequest, + UpdateMCPServerRequest, UpdateUserRequest, UserAPIKeyAuth, ) @@ -395,7 +397,7 @@ def test_mcp_advertised_versions_reject_unavailable_revisions(versions): ConfigGeneralSettings(mcp_advertised_versions=versions) -@pytest.mark.parametrize("revision", ["2026-07-28", "unknown", None]) +@pytest.mark.parametrize("revision", ["unknown", None]) def test_mcp_metadata_rejects_unavailable_upstream_protocol(revision): from litellm.proxy._types import NewMCPServerRequest, UpdateMCPServerRequest @@ -403,3 +405,74 @@ def test_mcp_metadata_rejects_unavailable_upstream_protocol(revision): for model in (NewMCPServerRequest, UpdateMCPServerRequest): with pytest.raises(ValidationError): model.model_validate(payload) + + +MCP_SERVER_REQUESTS = (NewMCPServerRequest, UpdateMCPServerRequest) +STDIO_SERVER_FIELDS = {"server_id": "stdio-1", "transport": "stdio", "command": "python", "args": ["server.py"]} + + +@pytest.mark.parametrize("request_model", MCP_SERVER_REQUESTS) +def test_a_stdio_mcp_server_is_refused_while_stdio_is_not_enabled(monkeypatch, request_model): + monkeypatch.delenv("LITELLM_ENABLE_MCP_STDIO", raising=False) + + with pytest.raises(ValidationError, match="LITELLM_ENABLE_MCP_STDIO=true"): + request_model(**STDIO_SERVER_FIELDS) + + +@pytest.mark.parametrize("request_model", MCP_SERVER_REQUESTS) +@pytest.mark.parametrize("flag", ["true", "TRUE", " True "]) +def test_a_stdio_mcp_server_is_accepted_once_stdio_is_enabled(monkeypatch, request_model, flag): + monkeypatch.setenv("LITELLM_ENABLE_MCP_STDIO", flag) + + assert request_model(**STDIO_SERVER_FIELDS).command == "python" + + +@pytest.mark.parametrize("request_model", MCP_SERVER_REQUESTS) +@pytest.mark.parametrize("flag", ["false", "1", "yes", ""]) +def test_only_an_explicit_true_enables_stdio_mcp_servers(monkeypatch, request_model, flag): + monkeypatch.setenv("LITELLM_ENABLE_MCP_STDIO", flag) + + with pytest.raises(ValidationError, match="LITELLM_ENABLE_MCP_STDIO=true"): + request_model(**STDIO_SERVER_FIELDS) + + +@pytest.mark.parametrize("request_model", MCP_SERVER_REQUESTS) +def test_a_stdio_command_outside_the_allowlist_is_refused_even_when_stdio_is_enabled(monkeypatch, request_model): + monkeypatch.setenv("LITELLM_ENABLE_MCP_STDIO", "true") + + with pytest.raises(ValidationError, match="not in the allowed commands list"): + request_model(**{**STDIO_SERVER_FIELDS, "command": "/bin/sh"}) + + +@pytest.mark.parametrize("request_model", MCP_SERVER_REQUESTS) +@pytest.mark.parametrize("missing", ["command", "args"]) +def test_an_enabled_stdio_mcp_server_still_needs_a_command_and_args(monkeypatch, request_model, missing): + monkeypatch.setenv("LITELLM_ENABLE_MCP_STDIO", "true") + + with pytest.raises(ValidationError, match=f"{missing} is required for stdio transport"): + request_model(**{k: v for k, v in STDIO_SERVER_FIELDS.items() if k != missing}) + + +@pytest.mark.parametrize("request_model", MCP_SERVER_REQUESTS) +def test_an_http_mcp_server_is_unaffected_by_the_stdio_flag(monkeypatch, request_model): + monkeypatch.delenv("LITELLM_ENABLE_MCP_STDIO", raising=False) + + assert request_model(server_id="http-1", transport="http", url="https://mcp.example.com").url == "https://mcp.example.com" + with pytest.raises(ValidationError, match="url or spec_path is required"): + request_model(server_id="http-1", transport="http") + + +@pytest.mark.parametrize("request_model", MCP_SERVER_REQUESTS) +def test_a_non_mapping_mcp_server_payload_gets_a_validation_error(request_model): + with pytest.raises(ValidationError, match="valid dictionary"): + request_model.model_validate("not-a-server") + + +@pytest.mark.parametrize("request_model", MCP_SERVER_REQUESTS) +def test_modern_http_upstream_protocol_is_available(request_model): + parsed = request_model.model_validate({ + "server_id": "modern", "transport": "http", "url": "https://example.com/mcp", + "mcp_info": {"protocol_version": "2026-07-28"}, + }) + assert parsed.mcp_info["protocol_version"] == "2026-07-28" + assert parsed.transport == "http" diff --git a/tests/unit/proxy/test_component_allowlists.py b/tests/unit/proxy/test_component_allowlists.py index 3641a2d9be9..1a210fcb445 100644 --- a/tests/unit/proxy/test_component_allowlists.py +++ b/tests/unit/proxy/test_component_allowlists.py @@ -26,7 +26,18 @@ RDS IAM token when ``IAM_TOKEN_DB_AUTH`` is set). import json import os import sys -from typing import Final +from collections.abc import AsyncGenerator, Mapping +from contextlib import asynccontextmanager +from functools import partial +from typing import Final, Literal + +import pytest +from starlette.applications import Starlette +from starlette.requests import Request +from starlette.responses import JSONResponse +from starlette.routing import Mount, Route +from starlette.testclient import TestClient +from starlette.types import Lifespan # Importing ``litellm.proxy.proxy_server`` runs its module-level setup, which # reads ``DATABASE_URL`` (Prisma) and ``LITELLM_MASTER_KEY``. Tier-zero CI @@ -43,7 +54,6 @@ _PRE_EXISTING_ENV = {key: os.environ.get(key) for key in _THROWAWAY_ENV} for _key, _value in _THROWAWAY_ENV.items(): os.environ.setdefault(_key, _value) -from fastapi.routing import Mount from prometheus_client import make_asgi_app # gateway/ and backend/ live at the repo root, not inside litellm/. @@ -53,6 +63,7 @@ if _REPO_ROOT not in sys.path: from backend.routes.allowlist import BACKEND_MOUNT_PATHS from gateway.routes.allowlist import GATEWAY_MOUNT_PATHS +from litellm.proxy._lazy_features import LazyFeature, attach_lazy_features from litellm.proxy.proxy_server import app from tests.test_litellm_rust.support.child_interpreter import run_child_interpreter @@ -74,7 +85,10 @@ _DB_ENV_KEYS = ( ) _PRE_DB_ENV = {_key: os.environ.pop(_key, None) for _key in _DB_ENV_KEYS} _PRE_COMPONENT_LIFESPAN = app.router.lifespan_context -from gateway.main import _is_gateway_route +from gateway.main import _gateway_lifespan, _is_gateway_route + +app.router.lifespan_context = _PRE_COMPONENT_LIFESPAN +from backend.main import _backend_lifespan app.router.lifespan_context = _PRE_COMPONENT_LIFESPAN for _key, _previous in _PRE_DB_ENV.items(): @@ -85,7 +99,7 @@ for _key, _previous in _PRE_DB_ENV.items(): _COVERAGE_PROBE: Final = """ import json, os, sys sys.path.insert(0, os.environ["LITELLM_COMPONENT_ALLOWLIST_REPO_ROOT"]) -from fastapi.routing import Mount +from starlette.routing import Mount from backend.routes.allowlist import BACKEND_EXACT_PATHS, BACKEND_PATH_PREFIXES from gateway.routes.allowlist import GATEWAY_EXACT_PATHS, GATEWAY_PATH_PREFIXES from litellm.proxy._lazy_features import loaded_lazy_modules @@ -112,6 +126,101 @@ json.dump({ """ +@pytest.mark.parametrize( + "component_lifespan", (None, _gateway_lifespan, _backend_lifespan), ids=("proxy", "gateway", "backend") +) +@pytest.mark.parametrize("eager", (False, True), ids=("lazy", "eager")) +@pytest.mark.parametrize("state_kind", ("enabled", "disabled", "stateless")) +def test_composed_lifespan_preserves_request_state_and_teardown( + monkeypatch: pytest.MonkeyPatch, + component_lifespan: Lifespan[Starlette] | None, + eager: bool, + state_kind: Literal["enabled", "disabled", "stateless"], +) -> None: + monkeypatch.setenv("LITELLM_DISABLE_LAZY_ROUTES", str(eager).lower()) + receiver: Final = object() + resource: Final = object() + state: Final[Mapping[str, object]] = { + "tracing_receiver": receiver if state_kind == "enabled" else None, + "other_resource": resource, + } + events: Final[list[str]] = [] # mutable-ok: observe startup, requests and teardown across the ASGI boundary + + async def trace_state(request: Request) -> JSONResponse: + events.append("request") + assert events[0] == "startup" and "shutdown" not in events + assert getattr(request.state, "other_resource", None) is (resource if state_kind != "stateless" else None) + assert getattr(request.state, "tracing_receiver", None) is (receiver if state_kind == "enabled" else None) + return JSONResponse({"keys": sorted(request.scope["state"])}) + + def register_trace_route(application: Starlette, module: object) -> None: + application.router.routes.append(Route("/v1/traces", trace_state)) + + @asynccontextmanager + async def stateful_lifespan(application: Starlette) -> AsyncGenerator[Mapping[str, object], None]: + events.append("startup") + application.router.routes.append(Route("/not-a-component-route", trace_state)) + try: + yield state + finally: + events.append("shutdown") + + @asynccontextmanager + async def stateless_lifespan(application: Starlette) -> AsyncGenerator[None, None]: + async with stateful_lifespan(application): + yield + + application: Final = type(app)(lifespan=stateless_lifespan if state_kind == "stateless" else stateful_lifespan) + feature: Final = LazyFeature("traces", __name__, ("/v1/traces",), register_fn=register_trace_route) + attach_lazy_features(application, (feature,)) + if component_lifespan is not None: + application.router.lifespan_context = partial(component_lifespan, lifespan=application.router.lifespan_context) + + with TestClient(application) as client: + response: Final = client.get("/v1/traces") + assert response.status_code == 200, response.text + assert response.json() == {"keys": [] if state_kind == "stateless" else sorted(state)} + filtered: Final = client.get("/not-a-component-route") + assert filtered.status_code == (200 if component_lifespan is None else 404), filtered.text + assert events == (["startup", "request", "request"] if component_lifespan is None else ["startup", "request"]) + assert events == ( + ["startup", "request", "request", "shutdown"] if component_lifespan is None else ["startup", "request", "shutdown"] + ) + + +@pytest.mark.parametrize( + "component_lifespan", (None, _gateway_lifespan, _backend_lifespan), ids=("proxy", "gateway", "backend") +) +@pytest.mark.parametrize("eager", (False, True), ids=("lazy", "eager")) +@pytest.mark.parametrize("phase", ("startup", "shutdown")) +def test_composed_lifespan_propagates_lifecycle_failures( + monkeypatch: pytest.MonkeyPatch, component_lifespan: Lifespan[Starlette] | None, eager: bool, phase: str +) -> None: + monkeypatch.setenv("LITELLM_DISABLE_LAZY_ROUTES", str(eager).lower()) + failure: Final = RuntimeError(f"{phase} failed") + events: Final[list[str]] = [] # mutable-ok: observe lifecycle events across the ASGI boundary + + @asynccontextmanager + async def inner_lifespan(application: Starlette) -> AsyncGenerator[Mapping[str, object], None]: + events.append("startup") + if phase == "startup": + raise failure + yield {} + events.append("shutdown") + raise failure + + application: Final = type(app)(lifespan=inner_lifespan) + attach_lazy_features(application, ()) + if component_lifespan is not None: + application.router.lifespan_context = partial(component_lifespan, lifespan=application.router.lifespan_context) + + with pytest.raises(RuntimeError) as caught: + with TestClient(application): + events.append("serving") + assert caught.value is failure + assert events == (["startup"] if phase == "startup" else ["startup", "serving", "shutdown"]) + + def test_gateway_plus_backend_covers_full_app(): """Every route on the proxy app must be served by gateway or backend. diff --git a/tests/unit/proxy/test_proxy_cli.py b/tests/unit/proxy/test_proxy_cli.py index 56980b141b6..47071827f2c 100644 --- a/tests/unit/proxy/test_proxy_cli.py +++ b/tests/unit/proxy/test_proxy_cli.py @@ -2265,6 +2265,63 @@ class TestRunServerDbSetup: assert "prisma CLI is neither on PATH" not in capsys.readouterr().out mock_setup_database.assert_called_once_with(use_migrate=True, use_v2_resolver=True) + @pytest.mark.parametrize( + ("database_url", "exits"), + (("postgresql://test:test@localhost:5432/test", True), (None, False)), + ids=("database-url-set", "no-database-url"), + ) + @patch("atexit.register") + def test_startup_exits_when_the_prisma_toolchain_is_missing_only_if_a_database_is_configured( + self, + mock_atexit_register, + database_url, + exits, + tmp_path, + capsys, + ): + """A DATABASE_URL with no way to run the Prisma CLI is fatal; no DATABASE_URL needs no Prisma at all.""" + from litellm_proxy_extras import prisma_toolchain + + from litellm.proxy.proxy_cli import run_server + + empty_bin = tmp_path / "emptybin" + empty_bin.mkdir() + real_find_spec = prisma_toolchain.importlib.util.find_spec + + def hide_prisma(name, package=None): + return None if name == "prisma" else real_find_spec(name, package) + + mock_proxy_module = MagicMock( + app=MagicMock(), + ProxyConfig=MagicMock(), + KeyManagementSettings=MagicMock(), + save_worker_config=MagicMock(), + ) + clean_env = {k: v for k, v in os.environ.items() if k not in ("DATABASE_URL", "DIRECT_URL")} + clean_env["PATH"] = str(empty_bin) + if database_url is not None: + clean_env["DATABASE_URL"] = database_url + + with ( + patch.dict(os.environ, clean_env, clear=True), + patch.dict( + "sys.modules", + {"proxy_server": mock_proxy_module, "litellm.proxy.proxy_server": mock_proxy_module}, + ), + patch.object(prisma_toolchain.importlib.util, "find_spec", side_effect=hide_prisma), + pytest.raises(SystemExit) if exits else nullcontext() as exit_info, + ): + run_server.main(["--local", "--skip_server_startup"], standalone_mode=False) + + out = capsys.readouterr().out + if exits: + assert exit_info.value.code == 1 + assert "a database URL is set but the prisma CLI is neither on PATH nor importable" in out + assert "pip install 'litellm[extra_proxy]'" in out + else: + assert "prisma CLI" not in out + assert "Setup complete" in out + @patch("subprocess.run") @patch("atexit.register") @patch("litellm.proxy.db.prisma_client.PrismaManager.setup_database") diff --git a/tests/unit/proxy/test_proxy_reject_logging.py b/tests/unit/proxy/test_proxy_reject_logging.py index d5a3acb2cd7..4e250ed3c52 100644 --- a/tests/unit/proxy/test_proxy_reject_logging.py +++ b/tests/unit/proxy/test_proxy_reject_logging.py @@ -74,18 +74,20 @@ class testLogger(CustomLogger): self.reaches_sync_failure_event = True -router = Router( - model_list=[ - { - "model_name": "fake-model", - "litellm_params": { - "model": "openai/fake", - "api_base": "https://exampleopenaiendpoint-production.up.railway.app/", - "api_key": "sk-12345", - }, - } - ] -) +@pytest.fixture +def router() -> Router: + return Router( + model_list=[ + { + "model_name": "fake-model", + "litellm_params": { + "model": "openai/fake", + "api_base": "https://exampleopenaiendpoint-production.up.railway.app/", + "api_key": "sk-12345", + }, + } + ] + ) def _register_proxy_test_logger(callback_logger: testLogger) -> None: @@ -130,7 +132,7 @@ def _register_proxy_test_logger(callback_logger: testLogger) -> None: ], ) @pytest.mark.asyncio -async def test_chat_completion_request_with_redaction(route, body): +async def test_chat_completion_request_with_redaction(route, body, router, monkeypatch): """ IMPORTANT Enterprise Test - Do not delete it: Makes a /chat/completions request on LiteLLM Proxy @@ -139,7 +141,7 @@ async def test_chat_completion_request_with_redaction(route, body): """ from litellm.proxy import proxy_server - setattr(proxy_server, "llm_router", router) + monkeypatch.setattr(proxy_server, "llm_router", router) _test_logger = testLogger() _register_proxy_test_logger(_test_logger) litellm.set_verbose = True diff --git a/tests/unit/proxy/test_proxy_token_counter.py b/tests/unit/proxy/test_proxy_token_counter.py index 8590e959961..fcd72e3777c 100644 --- a/tests/unit/proxy/test_proxy_token_counter.py +++ b/tests/unit/proxy/test_proxy_token_counter.py @@ -2,7 +2,6 @@ # 1. Generate a Key, and use it to make a call -import logging from unittest.mock import AsyncMock, MagicMock, patch import httpx @@ -18,7 +17,6 @@ from fastapi import HTTPException, Request import litellm from litellm import Router -from litellm._logging import verbose_proxy_logger from litellm.llms.bedrock.common_utils import BedrockError from litellm.llms.bedrock.count_tokens.bedrock_token_counter import BedrockTokenCounter from litellm.llms.bedrock.count_tokens.handler import BedrockCountTokensHandler @@ -29,7 +27,6 @@ from litellm.proxy.anthropic_endpoints.endpoints import ( from litellm.proxy.proxy_server import token_counter from litellm.types.utils import TokenCountResponse -verbose_proxy_logger.setLevel(level=logging.DEBUG) @pytest.mark.asyncio diff --git a/tests/unit/proxy/test_proxy_utils.py b/tests/unit/proxy/test_proxy_utils.py index ab787bbbe27..44ab2155734 100644 --- a/tests/unit/proxy/test_proxy_utils.py +++ b/tests/unit/proxy/test_proxy_utils.py @@ -1881,9 +1881,10 @@ async def test_health_check_not_called_when_disabled(monkeypatch): } }, ) -def test_custom_openapi(mock_get_openapi_schema): - from litellm.proxy.proxy_server import custom_openapi +def test_custom_openapi(mock_get_openapi_schema, monkeypatch): + from litellm.proxy.proxy_server import app, custom_openapi + monkeypatch.setattr(app, "openapi_schema", None) openapi_schema = custom_openapi() assert openapi_schema is not None diff --git a/tests/unit/proxy/test_spend_log_cleanup.py b/tests/unit/proxy/test_spend_log_cleanup.py index 46ac1234615..399c76d97c1 100644 --- a/tests/unit/proxy/test_spend_log_cleanup.py +++ b/tests/unit/proxy/test_spend_log_cleanup.py @@ -796,19 +796,23 @@ async def test_spend_logs_retention_alone_does_not_touch_the_session_rollup(): assert any('"LiteLLM_SpendLogs"' in sql for sql in tables) assert not any('"LiteLLM_AutoRouterSession"' in sql for sql in tables) assert not any('"LiteLLM_AutoRouterUserSession"' in sql for sql in tables) + assert not any('"LiteLLM_AutoRouterDailySpend"' in sql for sql in tables) assert not any('"LiteLLM_HealthCheckTable"' in sql for sql in tables) @pytest.mark.asyncio -async def test_session_retention_alone_cleans_both_session_rollups(): - client = _mock_prisma_for_retention([0, 0]) +async def test_session_retention_alone_cleans_both_session_rollups_and_the_daily_rollup(): + client = _mock_prisma_for_retention([0, 0, 0]) cleaner = SpendLogCleanup(general_settings={"maximum_autorouter_session_retention_period": "365d"}) cleaner.pod_lock_manager = None await cleaner.cleanup_old_spend_logs(client) - tables = [call[0][0] for call in client.db.execute_raw.call_args_list] - assert len(tables) == 2 + calls = client.db.execute_raw.call_args_list + tables = [call[0][0] for call in calls] + assert len(tables) == 3 assert '"LiteLLM_AutoRouterSession"' in tables[0] assert '"LiteLLM_AutoRouterUserSession"' in tables[1] + assert '"LiteLLM_AutoRouterDailySpend"' in tables[2] + assert calls[2][0][1] == calls[0][0][1].date().isoformat() @pytest.mark.asyncio @@ -852,7 +856,7 @@ async def test_spend_logs_retention_alone_keeps_daily_tag_spend_forever(): @pytest.mark.asyncio async def test_each_retention_key_cuts_off_at_its_own_horizon(): - client = _mock_prisma_for_retention([0, 0, 0, 0, 0]) + client = _mock_prisma_for_retention([0, 0, 0, 0, 0, 0]) cleaner = SpendLogCleanup( general_settings={ "maximum_spend_logs_retention_period": "7d", @@ -868,6 +872,8 @@ async def test_each_retention_key_cuts_off_at_its_own_horizon(): if '"LiteLLM_AutoRouterSession"' in call[0][0] else "LiteLLM_AutoRouterUserSession" if '"LiteLLM_AutoRouterUserSession"' in call[0][0] + else "LiteLLM_AutoRouterDailySpend" + if '"LiteLLM_AutoRouterDailySpend"' in call[0][0] else "LiteLLM_HealthCheckTable" if '"LiteLLM_HealthCheckTable"' in call[0][0] else "logs" @@ -878,6 +884,7 @@ async def test_each_retention_key_cuts_off_at_its_own_horizon(): assert (now - cutoffs["logs"]).days == 7 assert (now - cutoffs["LiteLLM_AutoRouterSession"]).days == 365 assert cutoffs["LiteLLM_AutoRouterUserSession"] == cutoffs["LiteLLM_AutoRouterSession"] + assert cutoffs["LiteLLM_AutoRouterDailySpend"] == cutoffs["LiteLLM_AutoRouterSession"].date().isoformat() assert (now - cutoffs["LiteLLM_HealthCheckTable"]).days == 30 diff --git a/tests/unit/proxy/test_tracing_endpoints.py b/tests/unit/proxy/test_tracing_endpoints.py index 2c587897938..d901222aaf4 100644 --- a/tests/unit/proxy/test_tracing_endpoints.py +++ b/tests/unit/proxy/test_tracing_endpoints.py @@ -2,9 +2,10 @@ Tests for the agent tracing endpoints (litellm/proxy/tracing_endpoints.py). """ -from collections.abc import AsyncGenerator +from collections.abc import AsyncGenerator, Mapping from contextlib import asynccontextmanager -from typing import Final +from types import ModuleType +from typing import Final, Literal from unittest.mock import AsyncMock, MagicMock import pytest @@ -13,15 +14,56 @@ from fastapi.testclient import TestClient from litellm.proxy import tracing_endpoints from litellm.proxy._types import LitellmUserRoles, ProxyLifespanState, UserAPIKeyAuth +from litellm.proxy.auth.authorization import OwnedRows, ReadScope +from litellm.proxy.auth.authorization_dependencies import get_log_team_lookup from litellm.proxy.auth.user_api_key_auth import user_api_key_auth from litellm.proxy.tracing_runtime import manage_tracing, provide_storage -from litellm.rust_bridge.traces import ClickHouseStorage +from litellm.rust_bridge import loader +from litellm.rust_bridge.trace_queries import SPAN_DETAIL, SpanDetailParams +from litellm.rust_bridge.trace_query_responses import TraceQueryHelp, TraceSQLResponse +from litellm.rust_bridge.traces import AllQueryScope, ClickHouseStorage, TraceStorageConfig from litellm.tracing import TraceReceiver, TracingPayloadTooLargeError from litellm.tracing.store import TraceStore from litellm.tracing.types import TraceScope +SQL_ENVELOPE: Final = { + "meta": [{"name": "value", "type": "UInt64"}], + "data": [{"value": "9007199254740993"}], + "rows": 1, + "statistics": {"elapsed": 0.01, "rows_read": 1, "bytes_read": 8}, + "rows_before_limit_at_least": 1, +} +QUERY_HELP: Final[Mapping[str, object]] = { + "dialect": "test SQL", + "access": "authenticated scope", + "response": "JSON envelope", + "tables": [{"name": "otel_traces", "columns": [{"name": "value", "type": "String", "comment": "label"}]}], + "normalized_fields": [], + "metadata": { + "table": "spend_logs", + "column": "metadata", + "fields": [], + "sampled_rows": 0, + "invalid_json_rows": 0, + "truncated": True, + "sample_sql": "SELECT metadata FROM traces", + "scope": "bounded sample", + "error": "discovery unavailable", + }, + "attributes": [], + "relationships": [], + "examples": [{"name": "recent", "sql": "SELECT * FROM traces LIMIT 1"}], + "gotchas": ["Keep queries bounded"], + "guide": "scoped", +} + + TEAM_KEY = UserAPIKeyAuth( - token="hashed-key", team_id="team-research", org_id="org-1", user_role=LitellmUserRoles.INTERNAL_USER + user_id="user", + token="hashed-key", + team_id="team-research", + org_id="org-1", + user_role=LitellmUserRoles.INTERNAL_USER, ) TRACE_RESPONSE: Final = { "summary": { @@ -61,38 +103,47 @@ SPAN_DETAIL_RESPONSE: Final = { ( pytest.param( UserAPIKeyAuth(token="admin-key", team_id="team-a", user_role=LitellmUserRoles.PROXY_ADMIN), - TraceScope(team_ids=(), api_key_hash=""), + TraceScope(all_teams=1, user_id="", team_ids=()), True, id="admin", ), pytest.param( UserAPIKeyAuth(token="view-key", team_id="team-a", user_role=LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY), - TraceScope(team_ids=(), api_key_hash=""), + TraceScope(all_teams=1, user_id="", team_ids=()), False, id="view-only-admin", ), pytest.param( TEAM_KEY, - TraceScope(team_ids=("team-research",), api_key_hash=""), + TraceScope(all_teams=0, user_id="user", team_ids=()), True, id="team-key", ), pytest.param( - UserAPIKeyAuth(token="hashed-key", user_role=LitellmUserRoles.INTERNAL_USER), - TraceScope(team_ids=("",), api_key_hash="hashed-key"), + UserAPIKeyAuth(user_id="user", token="hashed-key", user_role=LitellmUserRoles.INTERNAL_USER), + TraceScope(all_teams=0, user_id="user", team_ids=()), True, id="teamless-key", ), + pytest.param( + UserAPIKeyAuth(token="hashed-key", user_role=LitellmUserRoles.INTERNAL_USER), + None, + True, + id="key-without-user-can-only-write", + ), ), ) def test_trace_read_and_write_permissions( - client: TestClient, receiver: MagicMock, auth: UserAPIKeyAuth, scope: TraceScope, can_write: bool + client: TestClient, receiver: MagicMock, auth: UserAPIKeyAuth, scope: TraceScope | None, can_write: bool ) -> None: client.app.dependency_overrides[user_api_key_auth] = lambda: auth read: Final = client.get("/v1/traces?start_ms=1&end_ms=2") - assert read.status_code == 200, read.text - receiver.list_traces.assert_awaited_once_with(scope=scope, start_ms=1, end_ms=2, cursor=None) + assert read.status_code == (403 if scope is None else 200), read.text + if scope is None: + receiver.list_traces.assert_not_awaited() + else: + receiver.list_traces.assert_awaited_once_with(scope=scope, start_ms=1, end_ms=2, cursor=None) write: Final = client.post("/v1/traces", json={}) assert write.status_code == (200 if can_write else 403), write.text @@ -124,6 +175,11 @@ def client() -> TestClient: app = FastAPI() app.include_router(tracing_endpoints.router) app.dependency_overrides[user_api_key_auth] = lambda: TEAM_KEY + + async def lookup(auth: UserAPIKeyAuth) -> tuple[str, ...]: + return () + + app.dependency_overrides[get_log_team_lookup] = lambda: lookup return TestClient(app) @@ -191,7 +247,10 @@ def test_list_traces_passes_scope_window_and_cursor(client, receiver): assert response.status_code == 200 assert response.json() == {"data": [], "next_cursor": None} receiver.list_traces.assert_awaited_once_with( - scope={"team_ids": ("team-research",), "api_key_hash": ""}, start_ms=1, end_ms=2, cursor="abc" + scope={"all_teams": 0, "user_id": "user", "team_ids": ()}, + start_ms=1, + end_ms=2, + cursor="abc", ) @@ -208,7 +267,7 @@ def test_get_trace_404_and_200(client, receiver): response = client.get("/v1/traces/t1") assert response.status_code == 200 assert response.json() == TRACE_RESPONSE - receiver.get_trace.assert_awaited_with("t1", {"team_ids": ("team-research",), "api_key_hash": ""}, "") + receiver.get_trace.assert_awaited_with("t1", {"all_teams": 0, "user_id": "user", "team_ids": ()}, "") def test_get_span_404_and_200(client, receiver): @@ -217,7 +276,7 @@ def test_get_span_404_and_200(client, receiver): response = client.get("/v1/traces/t1/spans/s1") assert response.status_code == 200 assert response.json()["span_id"] == "s1" - receiver.get_span.assert_awaited_with("t1", "s1", {"team_ids": ("team-research",), "api_key_hash": ""}, "") + receiver.get_span.assert_awaited_with("t1", "s1", {"all_teams": 0, "user_id": "user", "team_ids": ()}, "") def test_get_span_serves_ui_content_from_stored_payloads(client): @@ -227,7 +286,7 @@ def test_get_span_serves_ui_content_from_stored_payloads(client): return_value=[{"span_id": "s1", "input": '{"city": "Paris"}', "output": stored_output, "attributes": {}}] ) client.app.dependency_overrides[tracing_endpoints.provide_receiver] = lambda: TraceReceiver(TraceStore(storage)) - body = client.get("/v1/traces/t1/spans/s1").json() + body = client.get("/v1/traces/t1/spans/s1?trace_ref=run-one").json() assert body["output"] == stored_output assert body["input_ui"] == {"kind": "fields", "fields": [{"key": "city", "value": "Paris"}]} assert body["output_ui"] == { @@ -241,7 +300,7 @@ def test_get_span_serves_ui_content_from_stored_payloads(client): def test_trace_detail_passes_scoped_reference(client, receiver): receiver.get_trace.return_value = TRACE_RESPONSE assert client.get("/v1/traces/t1?trace_ref=run-one").status_code == 200 - receiver.get_trace.assert_awaited_with("t1", {"team_ids": ("team-research",), "api_key_hash": ""}, "run-one") + receiver.get_trace.assert_awaited_with("t1", {"all_teams": 0, "user_id": "user", "team_ids": ()}, "run-one") def test_invalid_export_and_cursor_are_client_errors(client, receiver): @@ -253,12 +312,34 @@ def test_invalid_export_and_cursor_are_client_errors(client, receiver): assert client.get("/v1/traces?cursor=broken").status_code == 400 -def test_teamless_key_without_token_gets_403_on_reads(client, receiver): - client.app.dependency_overrides[user_api_key_auth] = lambda: UserAPIKeyAuth( - user_role=LitellmUserRoles.INTERNAL_USER - ) - assert client.get("/v1/traces").status_code == 403 - receiver.list_traces.assert_not_called() +@pytest.mark.parametrize( + "auth", + ( + UserAPIKeyAuth(user_role=LitellmUserRoles.INTERNAL_USER), + UserAPIKeyAuth(token="key"), + UserAPIKeyAuth(token="key", team_id="unpermitted"), + UserAPIKeyAuth(user_id="", token="key"), + ), +) +def test_key_without_user_cannot_read_traces(client: TestClient, auth: UserAPIKeyAuth) -> None: + storage: Final = MagicMock(spec=ClickHouseStorage) + client.app.dependency_overrides[user_api_key_auth] = lambda: auth + client.app.dependency_overrides[tracing_endpoints.provide_receiver] = lambda: TraceReceiver(TraceStore(storage)) + client.app.dependency_overrides[tracing_endpoints.provide_trace_query_secret] = lambda: "test-secret" + for path in ( + "/v1/traces", + "/v1/traces/t1", + "/v1/traces/t1/spans/s1", + "/v1/traces/t1/spans/s1/error", + "/v1/traces/query/help", + ): + response: Final = client.get(path) + assert response.status_code == 403, response.text + query: Final = client.post("/v1/traces/query", json={"sql": "SELECT * FROM otel_traces"}) + assert query.status_code == 403, query.text + storage.query.assert_not_called() + storage.query_sql.assert_not_called() + storage.query_help.assert_not_called() def test_view_only_admin_cannot_ingest_traces(client, receiver): @@ -350,6 +431,7 @@ def test_injected_receiver_persists_authenticated_tenant(client: TestClient) -> "litellm.team_id": TEAM_KEY.team_id, "litellm.api_key_hash": TEAM_KEY.token, "litellm.org_id": TEAM_KEY.org_id, + "litellm.user_id": TEAM_KEY.user_id or "", } @@ -428,24 +510,26 @@ def test_lifespan_receivers_are_app_local() -> None: } assert first_storage.query.await_count == 2 first_storage.query.assert_awaited_with( - "span_detail", - { - "team_ids": (TEAM_KEY.team_id,), - "api_key_hash": "", - "trace_id": "t1", - "span_id": "first-span", - "trace_ref": "first-run", - }, + SPAN_DETAIL, + SpanDetailParams( + all_teams=0, + user_id=TEAM_KEY.user_id, + team_ids=(), + trace_id="t1", + span_id="first-span", + trace_ref="first-run", + ), ) second_storage.query.assert_awaited_once_with( - "span_detail", - { - "team_ids": (TEAM_KEY.team_id,), - "api_key_hash": "", - "trace_id": "t1", - "span_id": "second-span", - "trace_ref": "second-run", - }, + SPAN_DETAIL, + SpanDetailParams( + all_teams=0, + user_id=TEAM_KEY.user_id, + team_ids=(), + trace_id="t1", + span_id="second-span", + trace_ref="second-run", + ), ) @@ -504,7 +588,7 @@ def test_lens_reads_from_the_lifespan_storage() -> None: assert response.status_code == 200, response.text assert response.json()["executions"] == [] storage.lens_sample.assert_awaited_once() - assert storage.lens_sample.await_args.args[0]["all_teams"] == 1 + assert storage.lens_sample.await_args.args[0].all_teams == 1 def test_lens_reads_from_injected_storage_without_receiver() -> None: @@ -532,14 +616,17 @@ def test_lens_reads_from_injected_storage_without_receiver() -> None: @pytest.mark.parametrize( ("auth", "expected_scope"), ( - (UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN), {"kind": "admin"}), - (UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY), {"kind": "admin"}), - (TEAM_KEY, {"kind": "team", "team_id": "team-research"}), + (UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN), {"kind": "all"}), + (UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY), {"kind": "all"}), + (TEAM_KEY, {"kind": "owned", "user_id": "user", "team_ids": ()}), ( - UserAPIKeyAuth(token="project-key", team_id="team-a", project_id="project-a"), - {"kind": "key", "team_id": "team-a", "api_key_hash": "project-key"}, + UserAPIKeyAuth(user_id="user", token="project-key", team_id="team-a", project_id="project-a"), + {"kind": "owned", "user_id": "user", "team_ids": ()}, + ), + ( + UserAPIKeyAuth(user_id="user", token="solo-key"), + {"kind": "owned", "user_id": "user", "team_ids": ()}, ), - (UserAPIKeyAuth(token="solo-key"), {"kind": "key", "team_id": "", "api_key_hash": "solo-key"}), ), ) def test_sql_and_help_use_authenticated_scope( @@ -547,19 +634,19 @@ def test_sql_and_help_use_authenticated_scope( ) -> None: client.app.dependency_overrides[user_api_key_auth] = lambda: auth client.app.dependency_overrides[tracing_endpoints.provide_trace_query_secret] = lambda: "test-secret" - receiver.store.storage.query_sql = AsyncMock(return_value='{"data":[{"value":1}]}') - receiver.store.storage.query_help = AsyncMock(return_value='{"guide":"scoped"}') + receiver.store.storage.query_sql = AsyncMock(return_value=TraceSQLResponse.model_validate(SQL_ENVELOPE)) + receiver.store.storage.query_help = AsyncMock(return_value=TraceQueryHelp.model_validate(QUERY_HELP)) result: Final = client.post("/v1/traces/query", json={"sql": "SELECT * FROM otel_traces"}) assert result.status_code == 200, result.text - assert result.json() == {"data": [{"value": 1}]} + assert result.json() == SQL_ENVELOPE receiver.store.storage.query_sql.assert_awaited_once_with( "SELECT * FROM otel_traces", expected_scope, "test-secret" ) help_result: Final = client.get("/v1/traces/query/help") assert help_result.status_code == 200, help_result.text - assert help_result.json() == {"guide": "scoped"} + assert help_result.json() == QUERY_HELP receiver.store.storage.query_help.assert_awaited_once_with(expected_scope, "test-secret") - forged: Final = client.post("/v1/traces/query", json={"sql": "SELECT 1", "scope": {"kind": "admin"}}) + forged: Final = client.post("/v1/traces/query", json={"sql": "SELECT 1", "scope": {"kind": "all"}}) assert forged.status_code == 422, forged.text assert receiver.store.storage.query_sql.await_count == 1 @@ -588,7 +675,7 @@ def test_sql_reports_rejected_queries_and_unavailable_readers( result: Final = client.post("/v1/traces/query", json={"sql": "SELECT 1"}) assert result.status_code == status, result.text receiver.store.storage.query_sql.assert_awaited_once_with( - "SELECT 1", {"kind": "team", "team_id": "team-research"}, "test-secret" + "SELECT 1", {"kind": "owned", "user_id": "user", "team_ids": ()}, "test-secret" ) @@ -598,7 +685,7 @@ def test_query_help_does_not_fall_back_when_reader_provisioning_fails(client: Te result: Final = client.get("/v1/traces/query/help") assert result.status_code == 503, result.text receiver.store.storage.query_help.assert_awaited_once_with( - {"kind": "team", "team_id": "team-research"}, "test-secret" + {"kind": "owned", "user_id": "user", "team_ids": ()}, "test-secret" ) @@ -609,7 +696,7 @@ def test_queries_require_a_proxy_secret( from litellm.proxy import proxy_server monkeypatch.setattr(proxy_server, "master_key", secret) - receiver.store.storage.query_sql = AsyncMock(return_value='{"data":[]}') + receiver.store.storage.query_sql = AsyncMock(return_value=TraceSQLResponse.model_validate(SQL_ENVELOPE)) result: Final = client.post("/v1/traces/query", json={"sql": "SELECT 1"}) if secret is None: assert result.status_code == 503, result.text @@ -618,5 +705,148 @@ def test_queries_require_a_proxy_secret( return assert result.status_code == 200, result.text receiver.store.storage.query_sql.assert_awaited_once_with( - "SELECT 1", {"kind": "team", "team_id": "team-research"}, secret + "SELECT 1", {"kind": "owned", "user_id": "user", "team_ids": ()}, secret ) + + +@pytest.mark.parametrize( + ("auth", "teams", "expected"), + ( + (UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN), ("team-a",), (1, "", ())), + (UserAPIKeyAuth(user_role=LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY), ("team-a",), (1, "", ())), + (UserAPIKeyAuth(user_id="user", token="key", team_id="unpermitted"), ("a", "b"), (0, "user", ("a", "b"))), + (UserAPIKeyAuth(user_id="user", token="key"), (), (0, "user", ())), + (UserAPIKeyAuth(user_id="user"), ("a",), (0, "user", ("a",))), + ), +) +def test_shared_trace_permissions_reach_read_and_sql_boundaries( + client: TestClient, + auth: UserAPIKeyAuth, + teams: tuple[str, ...], + expected: tuple[Literal[0, 1], str, tuple[str, ...]], +) -> None: + async def lookup(caller: UserAPIKeyAuth) -> tuple[str, ...]: + assert caller is auth + return teams + + team_lookup: Final = AsyncMock(side_effect=lookup) + storage: Final = MagicMock(spec=ClickHouseStorage) + storage.query = AsyncMock(return_value=[{"span_id": "s1", "input": "", "output": "", "attributes": {}}]) + storage.query_sql = AsyncMock(return_value=TraceSQLResponse.model_validate(SQL_ENVELOPE)) + storage.query_help = AsyncMock(return_value=TraceQueryHelp.model_validate(QUERY_HELP)) + client.app.dependency_overrides[user_api_key_auth] = lambda: auth + client.app.dependency_overrides[get_log_team_lookup] = lambda: team_lookup + client.app.dependency_overrides[tracing_endpoints.provide_receiver] = lambda: TraceReceiver(TraceStore(storage)) + client.app.dependency_overrides[tracing_endpoints.provide_trace_query_secret] = lambda: "test-secret" + + response: Final = client.get("/v1/traces/t1/spans/s1?trace_ref=run-one") + assert response.status_code == 200, response.text + assert response.json()["span_id"] == "s1" + storage.query.assert_awaited_once_with( + SPAN_DETAIL, + SpanDetailParams( + all_teams=expected[0], + user_id=expected[1], + team_ids=expected[2], + trace_id="t1", + span_id="s1", + trace_ref="run-one", + ), + ) + sql_response: Final = client.post("/v1/traces/query", json={"sql": "SELECT * FROM otel_traces"}) + assert sql_response.status_code == 200, sql_response.text + assert sql_response.json() == SQL_ENVELOPE + assert client.get("/v1/traces/query/help").json() == QUERY_HELP + query_scope: Final = ( + {"kind": "all"} + if expected[0] + else { + "kind": "owned", + "user_id": expected[1], + "team_ids": expected[2], + } + ) + storage.query_sql.assert_awaited_once_with("SELECT * FROM otel_traces", query_scope, "test-secret") + storage.query_help.assert_awaited_once_with(query_scope, "test-secret") + assert team_lookup.await_count == ( + 3 + if auth.user_id and auth.user_role not in (LitellmUserRoles.PROXY_ADMIN, LitellmUserRoles.PROXY_ADMIN_VIEW_ONLY) + else 0 + ) + + +@pytest.mark.parametrize( + ("scope", "expected"), + ( + (OwnedRows(None), ("", ())), + (OwnedRows("user"), ("user", ())), + (OwnedRows("user", ("a", "b")), ("user", ("a", "b"))), + ), +) +def test_trace_storage_permissions_map_owned_rows( + scope: ReadScope, + expected: tuple[str, tuple[str, ...]], +) -> None: + assert tracing_endpoints._trace_scope(scope) == TraceScope(all_teams=0, user_id=expected[0], team_ids=expected[1]) + assert tracing_endpoints.trace_query_scope(scope) == { + "kind": "owned", + "user_id": expected[0], + "team_ids": expected[1], + } + + +class _NativeConfig: + def __init__(self, database: str, url: str, retention_days: int) -> None: + pass + + +class _NativeReturningHelp(ModuleType): + def __init__(self, help_payload: Mapping[str, object]) -> None: + super().__init__("native_traces") + + class Storage: + def __init__(self, config: _NativeConfig) -> None: + pass + + async def query_help(self, scope: AllQueryScope, secret: str) -> Mapping[str, object]: + return help_payload + + self.NativeTraceConfig: Final = _NativeConfig + self.NativeTraceStorage: Final = Storage + self.trace_decode_otlp: Final = list + self.trace_encode_error: Final = bytes + self.trace_normalized_field_definitions: Final = list + + +async def test_storage_validates_the_native_query_help_value(monkeypatch: pytest.MonkeyPatch) -> None: + monkeypatch.setattr(loader, "_cached_bridge", _NativeReturningHelp(QUERY_HELP)) + storage: Final = ClickHouseStorage(TraceStorageConfig("http://clickhouse:8123")) + assert await storage.query_help({"kind": "all"}, "secret") == TraceQueryHelp.model_validate(QUERY_HELP) + + +@pytest.mark.parametrize( + "drift", + ( + { + "metadata": { + "table": "spend_logs", + "column": "metadata", + "fields": [{"path": ["a"], "types": ["boolen"], "expression": "a"}], + "sampled_rows": 1, + "invalid_json_rows": 0, + "truncated": False, + "sample_sql": "SELECT metadata FROM spend_logs", + "scope": "bounded sample", + } + }, + {"tables": [{"name": "traces", "columns": [{"name": "value", "type": "String"}]}]}, + {"unexpected": True}, + ), +) +async def test_storage_rejects_native_query_help_that_drifts_from_the_contract( + monkeypatch: pytest.MonkeyPatch, drift: Mapping[str, object] +) -> None: + monkeypatch.setattr(loader, "_cached_bridge", _NativeReturningHelp({**QUERY_HELP, **drift})) + storage: Final = ClickHouseStorage(TraceStorageConfig("http://clickhouse:8123")) + with pytest.raises(RuntimeError, match="invalid response"): + await storage.query_help({"kind": "all"}, "secret") diff --git a/tests/unit/proxy/test_update_spend.py b/tests/unit/proxy/test_update_spend.py index ebe505b3d60..6b92320762b 100644 --- a/tests/unit/proxy/test_update_spend.py +++ b/tests/unit/proxy/test_update_spend.py @@ -36,6 +36,7 @@ class MockPrismaClient: self.spend_log_transactions = [] self.daily_user_spend_transactions = {} self.tool_usage_transactions = [] + self.model_usage_transactions = [] self.autorouter_turn_transactions = [] self.baseline_accounting_transactions = [] self.baseline_accounting_lock = asyncio.Lock() @@ -49,6 +50,7 @@ class MockPrismaClient: self._spend_log_transactions_lock = asyncio.Lock() self.spend_log_write_lock = asyncio.Lock() self._tool_usage_transactions_lock = asyncio.Lock() + self._model_usage_transactions_lock = asyncio.Lock() self._autorouter_turn_transactions_lock = asyncio.Lock() def jsonify_object(self, obj): diff --git a/tests/unit/proxy/utils/prisma_and_spend/conftest.py b/tests/unit/proxy/utils/prisma_and_spend/conftest.py index 455eb423ddc..e37a82a023b 100644 --- a/tests/unit/proxy/utils/prisma_and_spend/conftest.py +++ b/tests/unit/proxy/utils/prisma_and_spend/conftest.py @@ -133,6 +133,8 @@ def mock_prisma_client() -> MagicMock: client.spend_log_write_lock = asyncio.Lock() client.tool_usage_transactions = [] client._tool_usage_transactions_lock = asyncio.Lock() + client.model_usage_transactions = [] + client._model_usage_transactions_lock = asyncio.Lock() client.jsonify_object = lambda data: dict(data) client.db.is_connected = MagicMock(return_value=False) client.db.connect = AsyncMock() diff --git a/tests/unit/proxy/utils/prisma_and_spend/test_spend_functions.py b/tests/unit/proxy/utils/prisma_and_spend/test_spend_functions.py index d6f41ba55db..7aa06bcafae 100644 --- a/tests/unit/proxy/utils/prisma_and_spend/test_spend_functions.py +++ b/tests/unit/proxy/utils/prisma_and_spend/test_spend_functions.py @@ -223,6 +223,33 @@ async def test_update_spend_logs_job_drains_tool_queue_when_spend_queue_empty( assert mock_prisma_client.tool_usage_transactions == [] +@pytest.mark.asyncio +async def test_update_spend_logs_job_drains_the_whole_model_usage_queue_in_one_run( + mock_prisma_client: Any, monkeypatch: pytest.MonkeyPatch +) -> None: + import litellm.proxy.db.model_usage_rollup as model_usage_mod + import litellm.proxy.db.spend_log_tool_index as tool_mod + import litellm.proxy.guardrails.usage_tracking as guard_mod + + proxy_logging = MagicMock() + proxy_logging.failure_handler = AsyncMock() + queued = [MagicMock() for _ in range(25_000)] + mock_prisma_client.model_usage_transactions = list(queued) + monkeypatch.setattr(guard_mod, "process_spend_logs_guardrail_usage", AsyncMock(), raising=False) + monkeypatch.setattr(tool_mod, "flush_tool_usage_transactions", AsyncMock(), raising=False) + flush_stub = AsyncMock() + monkeypatch.setattr(model_usage_mod, "flush_model_usage_transactions", flush_stub, raising=False) + + await update_spend_logs_job( + prisma_client=mock_prisma_client, + db_writer_client=None, + proxy_logging_obj=proxy_logging, + ) + + assert flush_stub.await_args.kwargs["transactions"] == queued + assert mock_prisma_client.model_usage_transactions == [] + + @pytest.mark.asyncio async def test_update_spend_logs_job_processes_and_clears_queue( mock_prisma_client: Any, make_spend_log_row: Any, monkeypatch: pytest.MonkeyPatch diff --git a/tests/unit/responses/litellm_completion_transformation/test_reasoning_items.py b/tests/unit/responses/litellm_completion_transformation/test_reasoning_items.py new file mode 100644 index 00000000000..093d1744418 --- /dev/null +++ b/tests/unit/responses/litellm_completion_transformation/test_reasoning_items.py @@ -0,0 +1,62 @@ +import json + +from litellm.responses.litellm_completion_transformation.reasoning_items import ( + decode_thinking_blocks, + encode_thinking_blocks, + is_litellm_minted_reasoning_item, + is_minted_reasoning_item_id, + mint_reasoning_item_id, +) + +A_PROVIDER_OWNED_REASONING_ITEM_ID = "rs_08d3a89dbb92277a006abf04f4266087d0b4eedacd7848f306" +A_PROVIDER_OWNED_ENCRYPTED_BLOB = "gAAAAABo-opaque-provider-blob" +SIGNED_BLOCK = {"type": "thinking", "thinking": "Paris first.", "signature": "sig-paris"} +UNSIGNED_BLOCK = {"type": "thinking", "thinking": "never signed"} +REDACTED_BLOCK = {"type": "redacted_thinking", "data": "opaque"} + + +def test_minted_ids_are_recognized_and_provider_owned_ids_are_not(): + minted = mint_reasoning_item_id() + assert is_minted_reasoning_item_id(minted) + assert not is_minted_reasoning_item_id(A_PROVIDER_OWNED_REASONING_ITEM_ID) + assert not is_minted_reasoning_item_id(minted.replace("-", "")) + assert not is_minted_reasoning_item_id(minted.removeprefix("rs_")) + assert not is_minted_reasoning_item_id(None) + + +def test_encoded_thinking_blocks_decode_back_to_the_verifiable_blocks_only(): + encoded = encode_thinking_blocks([SIGNED_BLOCK, UNSIGNED_BLOCK, REDACTED_BLOCK]) + assert encoded is not None + assert decode_thinking_blocks(encoded) == (SIGNED_BLOCK, REDACTED_BLOCK) + assert encode_thinking_blocks([UNSIGNED_BLOCK]) is None + assert decode_thinking_blocks(A_PROVIDER_OWNED_ENCRYPTED_BLOB) is None + assert decode_thinking_blocks(json.dumps(SIGNED_BLOCK)) is None + assert decode_thinking_blocks(json.dumps([{"type": "text", "text": "not thinking"}])) is None + + +def test_decoding_keeps_the_verifiable_blocks_of_a_mixed_array_and_skips_the_rest(): + mixed = json.dumps([SIGNED_BLOCK, "a stray string", 7, None, UNSIGNED_BLOCK, {"type": "thinking"}, REDACTED_BLOCK]) + assert decode_thinking_blocks(mixed) == (SIGNED_BLOCK, REDACTED_BLOCK) + assert decode_thinking_blocks(json.dumps(["only", "strings", 3])) is None + assert decode_thinking_blocks(json.dumps([UNSIGNED_BLOCK])) is None + + +def test_a_reasoning_item_is_litellm_minted_by_its_id_or_by_its_encoded_thinking_blocks(): + assert is_litellm_minted_reasoning_item({"type": "reasoning", "id": mint_reasoning_item_id(), "summary": []}) + assert is_litellm_minted_reasoning_item( + { + "type": "reasoning", + "id": A_PROVIDER_OWNED_REASONING_ITEM_ID, + "encrypted_content": encode_thinking_blocks([SIGNED_BLOCK]), + } + ) + assert not is_litellm_minted_reasoning_item( + { + "type": "reasoning", + "id": A_PROVIDER_OWNED_REASONING_ITEM_ID, + "summary": [], + "encrypted_content": A_PROVIDER_OWNED_ENCRYPTED_BLOB, + } + ) + assert not is_litellm_minted_reasoning_item({"type": "message", "id": mint_reasoning_item_id(), "role": "assistant"}) + assert not is_litellm_minted_reasoning_item("a bare string input") diff --git a/tests/unit/router_strategy/complexity_router/test_jev_classifier.py b/tests/unit/router_strategy/complexity_router/test_jev_classifier.py index 45070dfd3a7..418bf522b6a 100644 --- a/tests/unit/router_strategy/complexity_router/test_jev_classifier.py +++ b/tests/unit/router_strategy/complexity_router/test_jev_classifier.py @@ -8,6 +8,7 @@ from unittest.mock import create_autospec import httpx import pytest +import respx import litellm from litellm._logging import verbose_router_logger @@ -30,14 +31,15 @@ from litellm.types.utils import AUTOROUTER_CLASSIFIER_CALL_ORIGIN class _UsageRecorder(CustomLogger): - def __init__(self) -> None: + def __init__(self, model_key: str = "typesafe/jev-accounting") -> None: super().__init__() + self.model_key = model_key self.calls: tuple[Mapping[str, object], ...] = () async def async_log_success_event( self, kwargs: Mapping[str, object], response_obj: object, start_time: datetime, end_time: datetime ) -> None: - if str(kwargs.get("model", "")).removeprefix("typesafe/") != "jev-accounting": + if str(kwargs.get("model", "")) != self.model_key: return self.calls = (*self.calls, kwargs) @@ -167,8 +169,9 @@ async def test_jev_invalid_usage_never_reaches_spend_callbacks( @pytest.mark.asyncio @pytest.mark.parametrize("answer", ["SIMPLE", "UNAVAILABLE", "malformed"]) @pytest.mark.parametrize("private", [False, True]) +@pytest.mark.parametrize("legacy", [False, True]) async def test_jev_accounts_once_with_parent_identity_even_when_the_verdict_fails( - monkeypatch: pytest.MonkeyPatch, answer: str, private: bool + monkeypatch: pytest.MonkeyPatch, answer: str, private: bool, legacy: bool ) -> None: recorder: Final = _UsageRecorder() monkeypatch.setattr(litellm, "_async_success_callback", [recorder]) @@ -196,7 +199,15 @@ async def test_jev_accounts_once_with_parent_identity_even_when_the_verdict_fail router: Final = ComplexityRouter( "jev-router", litellm.Router(model_list=[]), - {"classifier_type": "jev", "jev_classifier_config": {}, "tiers": {"SIMPLE": "cheap"}}, + { + "classifier_type": "jev" if legacy else "oss_classifier", + "jev_classifier_config" if legacy else "opensource_classifier_config": { + "provider": "typesafe" if legacy else "jev", + }, + "tiers": {"SIMPLE": "cheap"}, + "session_affinity": False, + "deployment_affinity": False, + }, jev_client=provider, derive_savings_baseline=False, ) @@ -209,8 +220,9 @@ async def test_jev_accounts_once_with_parent_identity_even_when_the_verdict_fail "user_api_key_budget_reservation": {"reservation_id": "parent-reservation"}, "user_api_key_auth": {"budget_reservation": {"reservation_id": "parent-reservation"}}, } - outcome: Final = await router.aclassify( - "private current ask", + result: Final = await router.async_pre_routing_hook( + model="jev-router", + messages=[{"role": "user", "content": "private current ask"}], request_kwargs={ "metadata": metadata, "litellm_session_id": "session-a", @@ -221,7 +233,15 @@ async def test_jev_accounts_once_with_parent_identity_even_when_the_verdict_fail await GLOBAL_LOGGING_WORKER.flush() await handler.client.aclose() - assert (outcome.cause == "jev_classifier") is (answer == "SIMPLE") + assert result is not None and result.model == "cheap" + assert result.routing_decision is not None + decision: Final = result.routing_decision + assert (decision["cause"] == "jev_classifier") is (answer == "SIMPLE") + if answer == "SIMPLE": + assert decision["classifier_model"] == "typesafe/jev-accounting" + assert decision["classifier_cost"] == pytest.approx(0.007) + assert "jev-classifier:SIMPLE" in decision["signals"] + assert "jev-confidence=1.000000" in decision["signals"] assert len(recorder.calls) == 1 event: Final = recorder.calls[0] assert event["response_cost"] == pytest.approx(0.007) @@ -416,10 +436,103 @@ def _answer(choice: str = "SIMPLE") -> JevChoiceAnswer: def test_jev_config_requires_classifier_config() -> None: - with pytest.raises(ValueError, match="jev_classifier_config is required"): + with pytest.raises(ValueError, match="opensource_classifier_config is required"): ComplexityRouterConfig.model_validate({"classifier_type": "jev"}) +@pytest.mark.parametrize( + ("classifier_type", "config_key"), + [ + ("oss_classifier", "opensource_classifier_config"), + ("jev", "jev_classifier_config"), + ("oss_classifier", "jev_classifier_config"), + ("jev", "opensource_classifier_config"), + ], +) +@pytest.mark.parametrize( + ("provider", "model", "canonical_provider"), + [(None, "jev-latest", "jev"), ("typesafe", "jev-latest", "jev"), ("jev", "jev-latest", "jev"), ("laya", "english", "laya"), ("bespoke", "nimble-latest", "bespoke")], +) +def test_classifier_aliases_load_and_serialize_one_canonical_config( + classifier_type: str, config_key: str, provider: str | None, model: str, canonical_provider: str +) -> None: + incoming: Final = { + "classifier_type": classifier_type, + config_key: {"model": model, "api_key": None, **({"provider": provider} if provider is not None else {})}, + } + original: Final = deepcopy(incoming) + config: Final = ComplexityRouterConfig.model_validate(incoming) + assert config.classifier_type == "oss_classifier" + assert config.opensource_classifier_config is not None + assert config.opensource_classifier_config.provider == canonical_provider + assert config.opensource_classifier_config.model == model + assert config.opensource_classifier_config.api_key is None + assert "api_key" in config.opensource_classifier_config.model_fields_set + assert "api_base" not in config.opensource_classifier_config.model_fields_set + assert "jev_classifier_config" not in config.model_dump() + assert config.jev_classifier_config is config.opensource_classifier_config + assert incoming == original + + +@pytest.mark.parametrize("provider", ["laya", "bespoke"]) +@pytest.mark.parametrize("model", [None, " "]) +def test_oss_requires_its_own_checkpoint(provider: str, model: str | None) -> None: + with pytest.raises(ValueError, match=f"{provider} model must be"): + JevClassifierConfig.model_validate({"provider": provider, **({"model": model} if model is not None else {})}) + + +@pytest.mark.asyncio +@pytest.mark.parametrize("provider,model", [("laya", "english"), ("bespoke", "nimble-latest")]) +@pytest.mark.parametrize("custom_base", [False, True]) +@pytest.mark.parametrize("legacy", [False, True]) +async def test_oss_routes_with_its_own_credentials_and_accounts_the_checkpoint( + monkeypatch: pytest.MonkeyPatch, custom_base: bool, legacy: bool, provider: str, model: str +) -> None: + monkeypatch.setenv("TYPESAFE_API_KEY", "never-send-typesafe-key") + monkeypatch.setenv(f"{provider.upper()}_API_BASE", f"https://{provider}.test") + monkeypatch.setenv(f"{provider.upper()}_API_KEY", "oss-env-key") + monkeypatch.setattr(litellm, "disable_aiohttp_transport", True) + monkeypatch.setitem(litellm.model_cost, f"{provider}/{model}", {"input_cost_per_token": 0.01}) + recorder: Final = _UsageRecorder(f"{provider}/{model}") + monkeypatch.setattr(litellm, "_async_success_callback", [recorder]) + router: Final = ComplexityRouter( + f"{provider}-route", + litellm.Router(model_list=[]), + { + "classifier_type": "jev" if legacy else "oss_classifier", + "jev_classifier_config" if legacy else "opensource_classifier_config": { + "provider": provider, + "model": model, + **({"api_base": f"https://{provider}.test"} if custom_base else {}), + }, + "tiers": {"SIMPLE": "cheap"}, + }, + derive_savings_baseline=False, + ) + with respx.mock(assert_all_called=True) as upstream: + route: Final = upstream.post(f"https://{provider}.test/v1/systemone").respond( + 200, + json={ + "model": "laya-rl-agent" if provider == "laya" else model, + **({"routing": {"model": model}} if provider == "laya" else {}), + "answers": {"tier": _answer().model_dump()}, + "usage": {"input_tokens": 31, "output_tokens": 0}, + }, + ) + outcome: Final = await router.aclassify("choose a tier") + await GLOBAL_LOGGING_WORKER.flush() + + assert outcome.cause == "jev_classifier" + assert outcome.jev_verdict is not None + assert (outcome.jev_verdict.provider, outcome.jev_verdict.model) == (provider, model) + assert outcome.classifier_cost == pytest.approx(0.31) + sent: Final = route.calls.last.request + assert sent.headers.get("authorization") == (None if custom_base else "Bearer oss-env-key") + assert json.loads(sent.content)["model"] == model + assert len(recorder.calls) == 1 + assert recorder.calls[0]["response_cost"] == pytest.approx(0.31) + + def test_jev_config_is_rejected_for_other_classifier_types() -> None: with pytest.raises(ValueError, match="has no effect"): ComplexityRouterConfig.model_validate( @@ -437,7 +550,7 @@ def test_jev_instructions_reject_blank_values() -> None: @pytest.mark.parametrize( ("missing_key", "rejection"), [ - ({}, r"api_base requires jev_classifier_config\.api_key"), + ({}, r"api_base requires opensource_classifier_config\.api_key"), ({"api_key": ""}, r"api_key must be non-empty"), ({"api_key": " "}, r"api_key must be non-empty"), ], diff --git a/tests/unit/router_utils/test_auto_router_model_naming.py b/tests/unit/router_utils/test_auto_router_model_naming.py index 645f9e5e62a..87b23ce93ae 100644 --- a/tests/unit/router_utils/test_auto_router_model_naming.py +++ b/tests/unit/router_utils/test_auto_router_model_naming.py @@ -6,11 +6,11 @@ import pytest from litellm.router_strategy.complexity_router.fuse_presets import get_fuse_presets from litellm.router_strategy.complexity_router.jev_classifier import DEFAULT_JEV_INSTRUCTIONS from litellm.router_utils.auto_router_model_naming import ( - carries_complexity_router_settings, - classify_strategy_router_model, GATED_AUTO_ROUTER_CAPABILITIES, capability_limit_violation, + carries_complexity_router_settings, claimed_capability, + classify_strategy_router_model, count_capability_routers, gated_capability_of, strategy_router_dependencies, @@ -23,27 +23,60 @@ COMPLEXITY_FIELDS = frozenset({"complexity_router_config"}) SEMANTIC_FIELDS = frozenset({"auto_router_config", "auto_router_default_model", "auto_router_embedding_model"}) -@pytest.mark.parametrize("model", ["jev-latest", "jev-preview"]) -def test_jev_enumerates_a_paid_evaluation_without_a_completion_classifier(model: str) -> None: - found = strategy_router_dependencies( +@pytest.mark.parametrize( + ("classifier_type", "config_key"), + [ + ("jev", "jev_classifier_config"), + ("oss_classifier", "opensource_classifier_config"), + ("jev", "opensource_classifier_config"), + ("oss_classifier", "jev_classifier_config"), + ], +) +@pytest.mark.parametrize( + ("provider", "model", "accounting_provider"), + [ + (None, "jev-latest", "typesafe"), + ("typesafe", "jev-preview", "typesafe"), + ("jev", "jev-preview", "typesafe"), + ("laya", "english", "laya"), + ("bespoke", "nimble-latest", "bespoke"), + ], +) +def test_open_source_classifier_enumerates_its_accounting_model( + classifier_type: str, config_key: str, provider: str | None, model: str, accounting_provider: str +) -> None: + found: Final = strategy_router_dependencies( { "model": "auto_router/complexity_router", "complexity_router_config": { - "classifier_type": "jev", - "jev_classifier_config": {"model": model}, + "classifier_type": classifier_type, + config_key: {"model": model, **({"provider": provider} if provider else {})}, "tiers": {"SIMPLE": "cheap"}, }, } ) assert tuple((dep.model_name, dep.role) for dep in found) == ( ("cheap", "tier"), - (f"typesafe/{model}", "evaluation"), + (f"{accounting_provider}/{model}", "evaluation"), ) @pytest.mark.parametrize("instructions", [None, DEFAULT_JEV_INSTRUCTIONS, "Route conservatively"]) -def test_only_non_default_jev_instructions_claim_the_shared_customization_slot(instructions: str | None) -> None: - capability = claimed_capability({"classifier_type": "jev", "jev_classifier_config": {"instructions": instructions}}) +@pytest.mark.parametrize( + ("classifier_type", "config_key"), + [ + ("jev", "jev_classifier_config"), + ("oss_classifier", "opensource_classifier_config"), + ("jev", "opensource_classifier_config"), + ("oss_classifier", "jev_classifier_config"), + ], +) +def test_only_non_default_open_source_instructions_claim_the_shared_customization_slot( + instructions: str | None, classifier_type: str, config_key: str +) -> None: + capability: Final = claimed_capability( + {"classifier_type": classifier_type, config_key: {"instructions": instructions}} + ) assert (capability.key if capability else None) == ( "tier_or_classifier_prompt" if instructions == "Route conservatively" else None ) @@ -123,6 +156,21 @@ VALID_TIERS = { } +@pytest.mark.parametrize("legacy_config", [None, {}, {"provider": "laya", "model": "english"}]) +def test_dual_classifier_blocks_return_a_write_validation_error(legacy_config: Mapping[str, object] | None) -> None: + violation: Final = validate_complexity_router_config_write( + { + "tiers": VALID_TIERS, + "classifier_type": "oss_classifier", + "opensource_classifier_config": {"provider": "laya", "model": "english"}, + "jev_classifier_config": legacy_config, + } + ) + assert violation is not None + assert "opensource_classifier_config" in violation + assert "jev_classifier_config" in violation + + @pytest.mark.parametrize( "keyword_tier_rules,expected_fragment", [ @@ -408,6 +456,8 @@ def test_complexity_embedding_model_is_a_dependency_only_when_semantic_matching_ ("token_thresholds", "dimension_weights"), ("reasoning_override_min_score",), ("tiers",), + ("jev_classifier_config",), + ("opensource_classifier_config",), ], ) def test_placement_rejects_settings_written_beside_the_config(misplaced): @@ -447,7 +497,7 @@ def test_placement_guards_every_setting_the_config_owns(): ComplexityRouterConfig, ) - assert COMPLEXITY_ROUTER_CONFIG_KEYS == frozenset(ComplexityRouterConfig.model_fields) + assert COMPLEXITY_ROUTER_CONFIG_KEYS == frozenset(ComplexityRouterConfig.model_fields) | {"jev_classifier_config"} assert {"tier_boundaries", "token_thresholds", "dimension_weights"} <= COMPLEXITY_ROUTER_CONFIG_KEYS diff --git a/tests/unit/rust_bridge/test_trace_queries.py b/tests/unit/rust_bridge/test_trace_queries.py new file mode 100644 index 00000000000..76ead3582a4 --- /dev/null +++ b/tests/unit/rust_bridge/test_trace_queries.py @@ -0,0 +1,57 @@ +from typing import Final + +import pytest +from pydantic import JsonValue, ValidationError + +from litellm.rust_bridge.trace_queries import SPAN_DETAIL, SPAN_ERROR, SpanDetailParams +from litellm.rust_bridge.trace_query_responses import TraceSQLResponse + + +@pytest.mark.parametrize("offset", (-1, 2**64)) +def test_named_query_rejects_offsets_outside_the_native_integer_range(offset: int) -> None: + with pytest.raises(ValidationError) as error: + SPAN_ERROR.parameters.model_validate( + { + "all_teams": 0, + "user_id": "", + "team_ids": ["team"], + "trace_id": "trace", + "trace_ref": "ref", + "span_id": "span", + "error_offset": offset, + "error_version": "", + } + ) + assert error.value.error_count() == 1 + + +def test_named_query_rejects_parameters_for_a_different_query() -> None: + detail: Final = SpanDetailParams( + all_teams=0, + user_id="", + team_ids=("team",), + trace_id="trace", + trace_ref="ref", + span_id="span", + ) + with pytest.raises(ValidationError) as error: + SPAN_ERROR.parameters.model_validate(detail) + assert error.value.error_count() == 1 + + +def test_named_query_rejects_rows_missing_required_result_fields() -> None: + with pytest.raises(ValidationError) as error: + SPAN_DETAIL.response.validate_json('{"data":[{"span_id":"span","input":"input","output":"output"}]}') + assert error.value.error_count() == 1 + + +def test_sql_envelope_preserves_nested_data_large_integer_strings_and_extra_fields() -> None: + envelope: Final[dict[str, JsonValue]] = { + "meta": [{"name": "count", "type": "UInt64", "comment": "label"}], + "data": [{"count": "9007199254740993", "nested": [True, None, {"value": 2}]}], + "rows": "1", + "statistics": {"elapsed": 0.01, "rows_read": "1", "bytes_read": "8", "extra_stat": 4}, + "totals": {"count": "9007199254740993"}, + } + result: Final = TraceSQLResponse.model_validate(envelope) + assert result.model_dump(mode="json", exclude_unset=True) == envelope diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 4c36f99d2c8..ab7ecfcda05 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -1020,6 +1020,7 @@ def test_aaamodel_prices_and_context_window_json_is_valid(): "/v1/videos", "/vertex_ai/live", "/v1/listen", + "/v1/systemone", "/v1beta/interactions", ], }, diff --git a/ui/litellm-dashboard/public/assets/logos/google-adk.png b/ui/litellm-dashboard/public/assets/logos/google-adk.png new file mode 100644 index 00000000000..9f967caa300 Binary files /dev/null and b/ui/litellm-dashboard/public/assets/logos/google-adk.png differ diff --git a/ui/litellm-dashboard/public/assets/logos/hermes.png b/ui/litellm-dashboard/public/assets/logos/hermes.png new file mode 100644 index 00000000000..de47b728d12 Binary files /dev/null and b/ui/litellm-dashboard/public/assets/logos/hermes.png differ diff --git a/ui/litellm-dashboard/public/assets/logos/openclaw.png b/ui/litellm-dashboard/public/assets/logos/openclaw.png new file mode 100644 index 00000000000..563c79b0e6b Binary files /dev/null and b/ui/litellm-dashboard/public/assets/logos/openclaw.png differ diff --git a/ui/litellm-dashboard/public/assets/logos/strands.svg b/ui/litellm-dashboard/public/assets/logos/strands.svg new file mode 100644 index 00000000000..466fb64465e --- /dev/null +++ b/ui/litellm-dashboard/public/assets/logos/strands.svg @@ -0,0 +1,4 @@ + + + + diff --git a/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsPage.tsx b/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsPage.tsx index 2e82fe3c418..ac40a28a258 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsPage.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/access-groups/_components/AccessGroupsPage.tsx @@ -1,9 +1,10 @@ +import { Page, PageContent } from "@/components/shared/Page"; import { AccessGroupResponse, useAccessGroups } from "@/app/(dashboard)/hooks/accessGroups/useAccessGroups"; import { useDeleteAccessGroup } from "@/app/(dashboard)/hooks/accessGroups/useDeleteAccessGroup"; import { Boxes, Plus, SearchIcon, X } from "lucide-react"; import { useMemo, useState } from "react"; import DeleteResourceModal from "@/components/common_components/DeleteResourceModal"; -import { PageHeader } from "@/components/shared/PageHeader"; +import { PageHeader, PageHeaderControls, PageHeaderDescription, PageHeaderTitle } from "@/components/shared/PageHeader"; import { Button } from "@/components/ui/button"; import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from "@/components/ui/input-group"; import { AccessGroupDetail } from "./AccessGroupsDetailsPage"; @@ -59,49 +60,53 @@ export function AccessGroupsPage() { } return ( -
- } - title="Access Groups" - subtitle="Manage resource permissions for your organization" - primaryAction={ - canModify ? ( + + + + + Access Groups + + Manage resource permissions for your organization + {canModify && ( + - ) : undefined - } - /> + + )} + -
- - - - - setSearchText(e.target.value)} - /> - {searchText && ( - - setSearchText("")}> - - + +
+ + + - )} - -
+ setSearchText(e.target.value)} + /> + {searchText && ( + + setSearchText("")}> + + + + )} +
+
- 0} - canModify={canModify} - onGroupClick={setSelectedGroupId} - onDeleteClick={setGroupToDelete} - /> + 0} + canModify={canModify} + onGroupClick={setSelectedGroupId} + onDeleteClick={setGroupToDelete} + /> + @@ -126,6 +131,6 @@ export function AccessGroupsPage() { }} confirmLoading={deleteMutation.isPending} /> -
+ ); } diff --git a/ui/litellm-dashboard/src/app/(dashboard)/api-keys/ApiKeysDashboard.tsx b/ui/litellm-dashboard/src/app/(dashboard)/api-keys/ApiKeysDashboard.tsx index 376fee72b88..915df2f5ded 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/api-keys/ApiKeysDashboard.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/api-keys/ApiKeysDashboard.tsx @@ -1,5 +1,6 @@ "use client"; +import { Page } from "@/components/shared/Page"; import { teamListCall as v2TeamListCall } from "@/app/(dashboard)/hooks/teams/useTeams"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; import { KeyResponse, Team } from "@/components/key_team_helpers/key_list"; @@ -71,7 +72,7 @@ export default function ApiKeysDashboard() { }, [accessToken, userID, userRole]); return ( -
+ -
+ ); } diff --git a/ui/litellm-dashboard/src/app/(dashboard)/budgets/_components/budget_panel.tsx b/ui/litellm-dashboard/src/app/(dashboard)/budgets/_components/budget_panel.tsx index 7455c252e26..630d90e91b9 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/budgets/_components/budget_panel.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/budgets/_components/budget_panel.tsx @@ -3,13 +3,15 @@ * */ +import { Page, PageTabs, PageTabsList, PageTabsTrigger } from "@/components/shared/Page"; import { Plus, Wallet } from "lucide-react"; import React, { useCallback, useState } from "react"; import { Prism as SyntaxHighlighter } from "react-syntax-highlighter"; import { prism } from "react-syntax-highlighter/dist/esm/styles/prism"; import { useSyntaxTheme } from "@/hooks/useSyntaxTheme"; -import { PageHeader } from "@/components/shared/PageHeader"; +import { PageHeader, PageHeaderControls, PageHeaderDescription, PageHeaderTitle } from "@/components/shared/PageHeader"; +import { ToolbarSeparator } from "@/components/shared/ToolbarSeparator"; import { Button } from "@/components/ui/button"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; import DeleteResourceModal from "@/components/common_components/DeleteResourceModal"; @@ -78,35 +80,30 @@ const BudgetPanel: React.FC = ({ accessToken }) => { }; return ( -
- - } - title="Budgets" - subtitle="Spend, TPM and RPM limits you can assign to customers." - primaryAction={ - canModify ? ( - - ) : undefined - } - tabs={({ leadingControls }) => ( - - {leadingControls} - - Budgets - - - Examples - - - )} - /> + + + + + + Budgets + + Spend, TPM and RPM limits you can assign to customers. + + + {canModify && ( + <> + + + + )} + Budgets + Examples + + +
@@ -174,8 +171,8 @@ const BudgetPanel: React.FC = ({ accessToken }) => {
-
-
+ + ); }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/AutoRouterBenchmarksTab.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/AutoRouterBenchmarksTab.test.tsx index b4b34dbfaf3..081eb7f6e09 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/AutoRouterBenchmarksTab.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/AutoRouterBenchmarksTab.test.tsx @@ -75,7 +75,6 @@ const totals = (overrides: Partial = {}): Totals => ({ saved_spend: 2174.59, baseline_spend: 2534.45, saved_pct: 85.8, - saved_per_session: 23.13, cache: cache(), ...overrides, }); @@ -110,7 +109,6 @@ const zeroTotals: Totals = { saved_spend: 0, baseline_spend: 0, saved_pct: 0, - saved_per_session: 0, cache: zeroCache, }; @@ -173,7 +171,6 @@ describe("AutoRouterBenchmarksTab", () => { saved_spend: saved, baseline_spend: estimatedTurns ? actual + (saved ?? 0) : null, saved_pct: pct, - saved_per_session: null, }; mockHook({ data: response([], totals(comparison)), @@ -204,18 +201,15 @@ describe("AutoRouterBenchmarksTab", () => { } }); - it("leads with total estimated savings, before the four session-shape metrics", () => { + it("leads with total estimated savings, before the three session-shape metrics", () => { mockHook({ data: response([group(), group({ router_name: "gpt-auto" })]) }); renderTab(); const labels = screen - .getAllByText( - /Total estimated savings|Avg saved per session|Avg turns per session|Avg session length|Avg tokens per session/, - ) + .getAllByText(/Total estimated savings|Avg turns per session|Avg session length|Avg tokens per session/) .map((node) => node.textContent); expect(labels).toEqual([ "Total estimated savings", - "Avg saved per session", "Avg turns per session", "Avg session length", "Avg tokens per session", @@ -271,15 +265,40 @@ describe("AutoRouterBenchmarksTab", () => { }, ); - it("pairs the savings with the session count it was earned over, in its own tile", () => { + it("labels selected-day money apart from whole-session metrics, with no savings-per-session tile", () => { mockHook({ data: response([group(), group({ router_name: "gpt-auto" })]) }); renderTab(); - const tile = screen.getByText("Avg saved per session").closest('[data-slot="card"]'); - if (!tile) throw new Error("expected avg saved per session to render as a metric tile"); - - expect(within(tile).getByText("$23.13")).toBeInTheDocument(); + const tile = screen.getByText("Avg turns per session").closest('[data-slot="card"]'); + if (!tile) throw new Error("expected avg turns per session to render as a metric tile"); expect(within(tile).getByText("· 94 sessions")).toBeInTheDocument(); + expect(screen.queryByText("Avg saved per session")).not.toBeInTheDocument(); + expect(screen.getByText(/Savings and spend count requests on the selected UTC days/)).toBeInTheDocument(); + expect(screen.getByText(/Session metrics cover every session that overlaps the range/)).toBeInTheDocument(); + }); + + it("shows session averages as unavailable, not zero, when routed requests have no session rows", () => { + const noSessions = { + sessions: 0, + avg_turns_per_session: null, + avg_session_seconds: null, + avg_tokens_per_session: null, + }; + mockHook({ data: response([], totals(noSessions)) }); + renderTab(); + + expect(screen.getAllByText("Unavailable")).toHaveLength(3); + expect(screen.queryByText("0.0")).not.toBeInTheDocument(); + }); + + it.each([3, -3])("explains a %s gap between router records and recorded savings instead of comparing", (gap) => { + const residual = { saved_spend: 5, unattributed_saved_spend: gap, baseline_spend: null, saved_pct: null }; + mockHook({ data: response([], totals(residual)) }); + renderTab(); + + expect(screen.getByText("$5.00")).toBeInTheDocument(); + expect(screen.getByText(/Per-router records differ from recorded savings by \$3\.00/)).toBeInTheDocument(); + expect(screen.getByText("Estimated baseline spend").nextSibling?.textContent).toBe("Unavailable"); }); it("exposes each spend row as a term and its value, not as loose text", () => { @@ -431,7 +450,7 @@ describe("AutoRouterBenchmarksTab", () => { renderTab(); expect(screen.getByText("Total estimated savings")).toBeInTheDocument(); - expect(screen.getAllByText("$0.00")).toHaveLength(6); + expect(screen.getAllByText("$0.00")).toHaveLength(5); expect(screen.getByText("· 0 sessions")).toBeInTheDocument(); expect(screen.getByText("0s")).toBeInTheDocument(); expect(screen.getByText(/turns measured/)).toBeInTheDocument(); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/AutoRouterBenchmarksTab.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/AutoRouterBenchmarksTab.tsx index 24a97587e32..27e8df6db87 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/AutoRouterBenchmarksTab.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/AutoRouterBenchmarksTab.tsx @@ -105,6 +105,12 @@ const HeroCard: React.FC<{ view: BenchmarkView }> = ({ view }) => { adaptive and quality routers are excluded

)} + {stats.unattributed_saved_spend != null && ( +

+ Per-router records differ from recorded savings by {usd(Math.abs(stats.unattributed_saved_spend))}, for + example history from before per-router tracking, so the baseline comparison is unavailable +

+ )}
@@ -297,22 +303,34 @@ const BenchmarksBody: React.FC = ({ isPending, error, data, -
- - - - -
+

+ Savings and spend count requests on the selected UTC days. Actual spend covers every request on complexity + routers, including LLM classification cost. Baseline is actual spend plus recorded savings, so savings can be + zero or negative. +

- Actual spend covers every request on complexity routers, including LLM classification cost. Baseline is actual - spend plus recorded savings, so savings can be zero or negative. The range counts whole sessions that overlap - it, so totals can differ from savings views that group usage by UTC day. + Session metrics cover every session that overlaps the range, including its turns outside the range.

+
+ + + +
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.tsx index 0aa4f88495a..6a4c49963df 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/CostOptimizationView.tsx @@ -1,12 +1,13 @@ "use client"; +import { Page, PageTabs, PageTabsList, PageTabsTrigger } from "@/components/shared/Page"; import React from "react"; import { Info, PiggyBank } from "lucide-react"; import useCan from "@/app/(dashboard)/hooks/useCan"; import { Alert, AlertDescription } from "@/components/shared/Alert"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { PageHeader } from "@/components/shared/PageHeader"; +import { TabsContent } from "@/components/ui/tabs"; +import { PageHeader, PageHeaderControls, PageHeaderDescription, PageHeaderTitle } from "@/components/shared/PageHeader"; import UsageTab from "./UsageTab"; import PromptCompressionTab from "./PromptCompressionTab"; import PromptCachingTab from "./PromptCachingTab"; @@ -33,37 +34,30 @@ const CostOptimizationView: React.FC = ({ accessToken }; return ( -
- - } - title="Cost Optimization" - subtitle="Track and configure the mechanisms that save you money: prompt compression and prompt caching. Auto routers live under Models + Endpoints, on the Auto-Routers tab" - tabs={({ leadingControls }) => ( - - {leadingControls} - - Overall - + + + + + + Cost Optimization + + + Track and configure the mechanisms that save you money: prompt compression and prompt caching. Auto routers + live under Models + Endpoints, on the Auto-Routers tab + + + + Overall {canViewProxyWideCostData && ( <> - - Prompt Compression - - - Prompt Caching - - - Auto-Router - + Prompt Compression + Prompt Caching + Auto-Router )} - - )} - /> + + +
= ({ accessToken )} - -
+ + ); }; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/TierTurnsChart.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/TierTurnsChart.test.tsx index e4417d77463..42444fd8f06 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/TierTurnsChart.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/TierTurnsChart.test.tsx @@ -12,21 +12,21 @@ vi.mock("@/components/shared/charts", () => ({ })); import TierTurnsChart, { tierDisplayLabel } from "./TierTurnsChart"; -import type { AutoRouterBenchmarkGroup, BenchmarkView } from "./autoRouterBenchmarks"; +import type { AutoRouterBenchmarkGroup, AutoRouterBenchmarkTotals, BenchmarkView } from "./autoRouterBenchmarks"; -const totalsOnly = { +const totalsOnly: AutoRouterBenchmarkTotals = { sessions: 3, turns: 9, avg_turns_per_session: 3, avg_session_seconds: 60, avg_tokens_per_session: 100, spend: 1, + classifier_cost: 0, savings_estimated_turns: 9, savings_estimated_actual_spend: 1, saved_spend: 1, baseline_spend: 2, saved_pct: 50, - saved_per_session: 0.33, cache: { coverage_pct: 0, hit_rate_pct: 0, diff --git a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/autoRouterBenchmarks.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/autoRouterBenchmarks.test.ts index 0586163e77e..62d0c0e4e5e 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/autoRouterBenchmarks.test.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/cost-optimization/_components/autoRouterBenchmarks.test.ts @@ -43,7 +43,6 @@ const totals = (overrides: Partial = {}) => ({ saved_spend: 2174.59, baseline_spend: 2534.45, saved_pct: 85.8, - saved_per_session: 23.13, cache: cache(), ...overrides, }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailsMonitorView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailsMonitorView.tsx index f90a46e19e4..1dd6686d7fe 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailsMonitorView.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailsMonitorView.tsx @@ -1,3 +1,4 @@ +import { Page } from "@/components/shared/Page"; import type { DateRangePickerValue } from "@/components/shared/date_picker_types"; import { parseAsString, useQueryState } from "nuqs"; import React, { useCallback, useMemo, useState } from "react"; @@ -48,7 +49,7 @@ export default function GuardrailsMonitorView({ accessToken = null }: Guardrails ); return ( -
+ {!selectedGuardrailId ? ( )} -
+ ); } diff --git a/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailsOverview.tsx b/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailsOverview.tsx index 468e6967d81..5627e7fc3cb 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailsOverview.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/guardrails-monitor/_components/GuardrailsOverview.tsx @@ -18,7 +18,7 @@ import { type UsageUnits, } from "@/components/GuardrailsMonitor/usageUnits"; import { Button } from "@/components/ui/button"; -import { PageHeader } from "@/components/shared/PageHeader"; +import { PageHeader, PageHeaderControls, PageHeaderDescription, PageHeaderTitle } from "@/components/shared/PageHeader"; import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; import { EvaluationSettingsModal } from "./EvaluationSettingsModal"; import { MetricCard } from "@/components/GuardrailsMonitor/MetricCard"; @@ -282,20 +282,20 @@ export function GuardrailsOverview({ return (
- } - title="Guardrails Monitor" - subtitle="Monitor guardrail performance across all requests" - utilities={ - <> - {dateRangeControl} - - - } - /> + + + + Guardrails Monitor + + Monitor guardrail performance across all requests + + {dateRangeControl} + + +
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModelCostMap.ts b/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModelCostMap.ts index 2d82eedf25c..d9824b4753e 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModelCostMap.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/hooks/models/useModelCostMap.ts @@ -4,8 +4,9 @@ import { createQueryKeys } from "../common/queryKeysFactory"; const modelCostMapKeys = createQueryKeys("modelCostMap"); -export const useModelCostMap = () => { +export const useModelCostMap = (enabled = true) => { return useQuery>({ + enabled, queryKey: modelCostMapKeys.list({}), queryFn: async () => await modelCostMap(), staleTime: 60 * 1000, // 1 minute diff --git a/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx b/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx index 72f26919060..9d8718cb84a 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/layout.tsx @@ -149,7 +149,7 @@ function DashboardShell({ children }: { children: React.ReactNode }) { -
{children}
+
{children}
{!isPlayground && }
diff --git a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensOverview.tsx b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensOverview.tsx index 2e2a0318d85..c6cd8485cbe 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensOverview.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensOverview.tsx @@ -67,64 +67,6 @@ export function InvestigationList({ ); } -export function InvestigationExample({ onClose }: { onClose: () => void }) { - return ( - { - if (!open) onClose(); - }} - > - - - Example investigation - -
-
- - - 3 of 20 conversations -
-

- Failed lookups leave customers without answers -

- - The agent retries the same failed order lookup, then ends the conversation without an answer or a handoff. - -
-
-
-

After three failed lookups, the agent replies:

-
“I will check that for you.”
-
-
- - See the trace - -
    -
  1. - Customer · Where is my order? -
  2. -
  3. - Order lookup · Service unavailable -
  4. -
  5. - Two retries · Same error, no new information -
  6. -
  7. - Agent · I will check that for you. Conversation - ends. -
  8. -
-
-
-
-
- ); -} - export function MonitoringSetup({ settings, ready, diff --git a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensProgress.tsx b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensProgress.tsx index c2b9d227276..3e01671a832 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensProgress.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensProgress.tsx @@ -1,11 +1,31 @@ "use client"; import { useEffect, useState } from "react"; -import { Check, Loader2 } from "lucide-react"; import { Button } from "@/components/ui/button"; -import { analysisElapsed, analysisProgress, nextCheckStatus, type Lens, type Job } from "./lensData"; +import { + analysisElapsed, + analysisFraction, + analysisPace, + analysisProgress, + analysisStages, + durationText, + nextCheckStatus, + remainingLabel, + stageDurations, + type Lens, + type Job, + type ProgressSample, +} from "./lensData"; -const steps = ["Review runs", "Find patterns", "Check evidence"]; +const steps = ["review runs", "find patterns", "check evidence"]; +const markers = { done: "✓", active: "▸", todo: "·" }; +const rowText = { done: "text-foreground", active: "font-medium text-foreground", todo: "text-muted-foreground" }; +const blocks = 32; + +function stageState(index: number, current: number): keyof typeof markers { + if (index < current) return "done"; + return index === current ? "active" : "todo"; +} export function LensProgress({ job, onCancel }: { job: Job; onCancel?: () => void }) { const [now, setNow] = useState(Date.now); @@ -14,57 +34,98 @@ export function LensProgress({ job, onCancel }: { job: Job; onCancel?: () => voi return () => window.clearInterval(timer); }, []); const progress = analysisProgress(job); - const percent = progress.total ? Math.min(100, (progress.done / progress.total) * 100) : undefined; + const fraction = analysisFraction(progress); + const [samples, setSamples] = useState([]); + const latest = samples.at(-1); + if (!latest || latest.step !== progress.step || latest.done !== progress.done) { + setSamples([...samples, { at: now, step: progress.step, done: progress.done, fraction }].slice(-120)); + } + const pace = analysisPace(samples, now); + const percent = Math.round(fraction * 100); + const queued = progress.step < 0; + const counts = analysisStages(job); + const durations = stageDurations(samples, job.created_at, now); + const filled = Math.round(fraction * blocks); + const stats = [ + ["eta", remainingLabel(pace.secondsLeft)], + ["rate", pace.perMinute === null ? "–" : `${Math.round(pace.perMinute)}/min`], + ["elapsed", analysisElapsed(job.created_at, now)], + ]; return ( -
-
-
-
- - {analysisElapsed(job.created_at, now)} elapsed +
+
+ + {progress.title} + {!queued && · {progress.detail}} -
-
    - {steps.map((label, index) => ( -
  1. -
    - - {index < progress.step && } - {label} - -
  2. - ))} -
-
-

{progress.detail}

-
-
-
-
-
- {job.status === "running" && You can leave this page while the investigation runs.} {onCancel && ( - )}
+
+
+ +
+ {Array.from({ length: blocks }, (_, index) => ( + + ))} +
+ + {percent}% +
+
    + {steps.map((label, index) => { + const state = stageState(index, progress.step); + const { done, total } = counts[index]; + const seconds = durations[index]; + return ( +
  1. + + {label} + + {state === "todo" || !total ? "–" : `${Math.min(done, total)}/${total}`} + + {seconds === null ? "" : durationText(seconds)} +
  2. + ); + })} +
+
+ {queued ? ( + {progress.detail} + ) : ( + stats.map(([key, value]) => ( + + {key} {value} + + )) + )} +
+
); } diff --git a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensView.integration.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensView.integration.test.tsx index a3ec7877223..b3d7366bbab 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensView.integration.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensView.integration.test.tsx @@ -231,39 +231,18 @@ it("runs saved settings immediately without opening setup", async () => { expect(screen.queryByRole("dialog")).not.toBeInTheDocument(); }); -it("lets a new user inspect example evidence and return to setup without starting an investigation", async () => { +it("offers the interactive demo without starting an investigation", async () => { window.history.replaceState({}, "", "/lens/"); testQueryClient.clear(); vi.mocked(apiClient.get).mockImplementation(async (path) => { if (path === "/lens") return { lenses: [], workers: [], tracing_enabled: false }; return { traces: false, requests: false }; }); + const onDemo = vi.fn(); const user = userEvent.setup(); - renderWithProviders(); - - await user.click(await screen.findByRole("button", { name: "View an example" })); - const example = within(screen.getByRole("dialog", { name: "Example investigation" })); - expect(example.getByRole("heading", { name: "Failed lookups leave customers without answers" })).toBeVisible(); - const evidence = example.getByText(/Where is my order\?/); - expect(evidence).not.toBeVisible(); - await user.click(example.getByText("See the trace")); - expect(evidence).toBeVisible(); - expect(example.getByText(/Service unavailable/)).toBeVisible(); - await user.click(example.getByText("See the trace")); - expect(evidence).not.toBeVisible(); - - await user.click(example.getByRole("button", { name: "Close" })); - await waitFor(() => expect(screen.queryByRole("dialog")).not.toBeInTheDocument()); - expect(screen.getByRole("link", { name: "Set up traces" })).toBeVisible(); - expect(screen.getByRole("button", { name: "Connect worker" })).toBeDisabled(); - expect(screen.getByRole("button", { name: "New investigation" })).toBeDisabled(); - - await user.click(screen.getByRole("button", { name: "View an example" })); - expect(screen.getByRole("dialog", { name: "Example investigation" })).toBeVisible(); - expect(screen.getByText(/Where is my order\?/)).not.toBeVisible(); - await user.keyboard("{Escape}"); - await waitFor(() => expect(screen.queryByRole("dialog")).not.toBeInTheDocument()); - expect(screen.getByRole("button", { name: "View an example" })).toBeVisible(); + renderWithProviders(); + await user.click(await screen.findByRole("button", { name: "Preview sample" })); + expect(onDemo).toHaveBeenCalledOnce(); expect(apiClient.post).not.toHaveBeenCalled(); }); @@ -276,13 +255,14 @@ it("guides a first-time administrator into worker connection and lens setup", as return { traces: true, requests: false, data: [] }; }); const user = userEvent.setup(); - renderWithProviders(); + renderWithProviders(); const guide = within(await screen.findByRole("region", { name: "Find what needs attention" })); expect(apiClient.get).toHaveBeenCalledWith("/lens/activity/available", { accessToken: "test" }); expect(await guide.findByRole("link", { name: "View traces" })).toHaveAttribute( "href", expect.stringMatching(/^\/ui\/lens\/?\?tab=traces$/), ); + expect(await guide.findByRole("button", { name: "Preview sample" })).toBeVisible(); await user.click(guide.getByRole("button", { name: "Connect worker" })); const connection = within(await screen.findByRole("dialog", { name: "Connect a worker" })); expect(connection.getByRole("button", { name: "Get install command" })).toBeVisible(); @@ -305,6 +285,7 @@ it("guides a first-time administrator into worker connection and lens setup", as }); }); await waitFor(() => expect(guide.getByRole("button", { name: "New investigation" })).toBeEnabled()); + expect(guide.queryByRole("button", { name: "Preview sample" })).not.toBeInTheDocument(); await user.click(guide.getByRole("button", { name: "New investigation" })); expect(await screen.findByRole("dialog", { name: "Which activity should we investigate?" })).toBeVisible(); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensView.tsx index 09a8607813d..cd78effe0f8 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensView.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensView.tsx @@ -1,4 +1,5 @@ "use client"; +import { useLensDemo } from "@/components/lens/LensDemoContext"; import type { components } from "@/lib/http/schema"; import { useEffect, useState } from "react"; @@ -28,7 +29,7 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"; import { Sheet, SheetContent, SheetHeader, SheetTitle, SheetDescription } from "@/components/ui/sheet"; import { Popover, PopoverContent, PopoverTitle, PopoverTrigger } from "@/components/ui/popover"; import { LensFinding } from "./LensFinding"; -import { apiClient } from "@/components/networking"; +import { apiClient as liveApiClient } from "@/components/networking"; import { TracePanel } from "./TracePanel"; import { LensSetup } from "./LensSetup"; import { LensRuns } from "./LensRuns"; @@ -37,13 +38,7 @@ import { WorkerSetup } from "./WorkerSetup"; import { useAnalysisKeyInfo } from "./AnalysisKeyDetails"; import { uiHref } from "@/utils/uiHref"; import { ApiError } from "@/lib/http/client"; -import { - InvestigationList, - InvestigationExample, - MonitoringSetup, - InvestigationSummary, - InvestigationFailure, -} from "./LensOverview"; +import { InvestigationList, MonitoringSetup, InvestigationSummary, InvestigationFailure } from "./LensOverview"; import { LensWelcome } from "./LensWelcome"; import { workerConnected, @@ -70,10 +65,19 @@ function emptyFindingTitle(active: boolean, scanned: boolean, status?: string) { return scanned ? "No matching findings" : "Ready for the first analysis"; } -export function LensView({ accessToken, readOnly = false }: { accessToken: string; readOnly?: boolean }) { +export function LensView({ + accessToken, + readOnly = false, + onDemo, +}: { + accessToken: string; + readOnly?: boolean; + onDemo?: () => void; +}) { + const demo = useLensDemo(); + const apiClient = demo?.client ?? liveApiClient; const client = useQueryClient(); const [workerSetup, setWorkerSetup] = useState(false); - const [showExample, setShowExample] = useState(false); const [monitoring, setMonitoring] = useState(false); const [now, setNow] = useState(Date.now); useEffect(() => { @@ -85,6 +89,7 @@ export function LensView({ accessToken, readOnly = false }: { accessToken: strin queryKey: key, queryFn: () => apiClient.get("/lens", { accessToken }), refetchInterval: (current) => { + if (demo) return false; const running = current.state.data?.lenses.some((item) => item.jobs.some((job) => ["queued", "running"].includes(job.status)), ); @@ -100,7 +105,10 @@ export function LensView({ accessToken, readOnly = false }: { accessToken: strin queryFn: () => apiClient.get<{ data: import("./lensData").AnalysisModelInfo[] }>("/model_group/info", { accessToken }), }); - const [selected, setSelected] = useQueryState("lens", parseAsString.withOptions({ history: "push" })); + const [liveSelected, setLiveSelected] = useQueryState("lens", parseAsString.withOptions({ history: "push" })); + const [demoSelected, setDemoSelected] = useState(null); + const selected = demo ? demoSelected : liveSelected; + const setSelected = demo ? setDemoSelected : setLiveSelected; const [editing, setEditing] = useState<"new" | "edit" | "duplicate" | null>(null); const [batchId, setBatchId] = useState("latest"); const [historyOffset, setHistoryOffset] = useState(0); @@ -146,7 +154,7 @@ export function LensView({ accessToken, readOnly = false }: { accessToken: strin queryKey: ["lens-activity-available", accessToken], queryFn: () => apiClient.get<{ traces: boolean; requests: boolean }>("/lens/activity/available", { accessToken }), enabled: loaded, - refetchInterval: 5000, + refetchInterval: demo ? (false as const) : 5000, }; const activity = useQuery(activityOptions); const tracesReady = activity.data?.traces === true && !activity.error; @@ -157,7 +165,7 @@ export function LensView({ accessToken, readOnly = false }: { accessToken: strin queryKey: ["lens-history", lens?.id, historyOffset, accessToken], enabled: !!lens, queryFn: () => apiClient.get(`/lens/${lens?.id}/runs`, { accessToken, query: { offset: historyOffset } }), - refetchInterval: 10000, + refetchInterval: demo ? (false as const) : 10000, }; const history = useQuery(historyQuery); const historical = useQuery({ @@ -297,7 +305,7 @@ export function LensView({ accessToken, readOnly = false }: { accessToken: strin }} onConnect={() => setWorkerSetup(true)} onCreate={() => setEditing("new")} - onExample={() => setShowExample(true)} + onDemo={activity.isSuccess && !ready ? onDemo : undefined} /> )} {showReadiness && !ready && ( @@ -687,7 +695,6 @@ export function LensView({ accessToken, readOnly = false }: { accessToken: strin } /> )} - {showExample && setShowExample(false)} />} {monitoring && lens && ( void; onCreate: () => void; onRetry: () => void; - onExample: () => void; + onDemo?: () => void; }) { const activityReady = tracesReady || requestsReady; const workerReady = activityReady && connected; @@ -39,16 +40,14 @@ export function LensWelcome({ className: "col-start-2 w-fit sm:col-start-auto", }); return ( -
+
+ {onDemo && }

Find what needs attention

Check how your agents behave. Get findings you can trace back to what happened.

-
  1. diff --git a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensWorkspace.integration.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensWorkspace.integration.test.tsx new file mode 100644 index 00000000000..8b7531c9bc2 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensWorkspace.integration.test.tsx @@ -0,0 +1,137 @@ +import { fireEvent, screen, within } from "@testing-library/react"; +import userEvent from "@testing-library/user-event"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { chooseSelectOption, renderWithProviders, testQueryClient } from "@/../tests/test-utils"; +import { LensWorkspace } from "./LensWorkspace"; +import { createLensDemoData } from "./lensDemoData"; + +const network = vi.fn(); +beforeEach(() => { + testQueryClient.clear(); + vi.stubGlobal("fetch", network); + network.mockReset(); + network.mockImplementation(async (input) => { + const path = new URL(String(input), "http://localhost").pathname; + if (path === "/v1/traces") return Response.json({ detail: "Tracing is not enabled" }, { status: 501 }); + if (path === "/lens") return Response.json({ lenses: [], workers: [], tracing_enabled: false }); + return Response.json({ data: [], traces: false, requests: false }); + }); +}); + +describe("Lens interactive demo", () => { + it("opens without tracing, filters sample runs, and restores the live view without mixing data", async () => { + const user = userEvent.setup(); + const onUrlUpdate = vi.fn(); + renderWithProviders(, { + onUrlUpdate, + }); + expect(await screen.findByText("Enable tracing")).toBeVisible(); + await user.click(screen.getByRole("button", { name: "Preview sample" })); + expect(await screen.findByText("Where is order #1042?")).toBeVisible(); + expect(screen.getByText("You’re viewing demo data")).toBeVisible(); + expect(screen.queryByRole("button", { name: "Set up tracing" })).not.toBeInTheDocument(); + network.mockClear(); + await user.click(screen.getByRole("button", { name: "Refresh", exact: true })); + expect(screen.getByText("Where is order #1042?")).toBeVisible(); + fireEvent.change(screen.getByPlaceholderText("Search input or trace ID"), { target: { value: "headphones" } }); + expect(screen.getByText("Can I return my headphones?")).toBeVisible(); + expect(screen.queryByText("Where is order #1042?")).not.toBeInTheDocument(); + fireEvent.change(screen.getByRole("textbox", { name: "Search runs" }), { target: { value: "" } }); + await chooseSelectOption(user, screen.getByRole("combobox", { name: "Filter by agent" }), "support_agent"); + await chooseSelectOption(user, screen.getByRole("combobox", { name: "Filter by status" }), "Failed"); + expect(within(screen.getByRole("table", { name: "Agent runs" })).getAllByRole("row")).toHaveLength(4); + await user.click(screen.getByRole("tab", { name: "Investigations", exact: true })); + expect(await screen.findByRole("button", { name: /Support quality/ })).toBeVisible(); + expect(screen.queryByRole("button", { name: "New investigation" })).not.toBeInTheDocument(); + expect(network).not.toHaveBeenCalled(); + expect(onUrlUpdate).not.toHaveBeenCalled(); + await user.click(screen.getByRole("button", { name: "Exit demo" })); + expect(await screen.findByText("Enable tracing")).toBeVisible(); + expect(screen.queryByText("You’re viewing demo data")).not.toBeInTheDocument(); + expect(screen.queryByText("Can I return my headphones?")).not.toBeInTheDocument(); + }); + + it("connects findings and history to their original trace without live requests or URL changes", async () => { + const user = userEvent.setup(); + const onUrlUpdate = vi.fn(); + renderWithProviders(, { + searchParams: "?tab=investigations", + onUrlUpdate, + }); + await screen.findByRole("button", { name: "Preview sample" }); + await user.click(screen.getByRole("button", { name: "Preview sample" })); + await user.click(await screen.findByRole("button", { name: /Support quality/ })); + network.mockClear(); + await user.click(screen.getByRole("button", { name: /Repeated lookups leave customers without an answer/ })); + const finding = screen.getByRole("dialog"); + expect(within(finding).getByText(/The support agent retries/)).toBeVisible(); + const summaries = within(finding).getAllByText("support_agent", { exact: true }); + await user.click(summaries[0]); + await user.click(within(finding).getAllByRole("button", { name: /Open original step/ })[0]); + expect(await screen.findByRole("complementary", { name: "Span details" })).toHaveTextContent( + "I will check that for you.", + ); + await user.click(screen.getByRole("button", { name: "Copy for agent" })); + expect(await navigator.clipboard.readText()).toContain("I will check that for you."); + expect(await navigator.clipboard.readText()).not.toContain("Authorization"); + await user.click(screen.getByRole("tab", { name: "Attributes" })); + expect(await screen.findByText("gen_ai.agent.name")).toBeVisible(); + await user.click( + within(screen.getByRole("dialog", { name: "Original run" })).getByRole("button", { name: "Close", exact: true }), + ); + await user.click(within(screen.getByRole("dialog")).getByRole("button", { name: "Close", exact: true })); + await user.click(screen.getByRole("tab", { name: "History" })); + await user.click(screen.getAllByRole("button", { name: /runs reviewed/ })[1]); + expect(await screen.findByText(/1 linked run · high priority/)).toBeVisible(); + expect(network).not.toHaveBeenCalled(); + expect(onUrlUpdate).not.toHaveBeenCalled(); + await user.click(screen.getByRole("button", { name: "Exit demo" })); + expect(await screen.findByRole("heading", { name: "Find what needs attention" })).toBeVisible(); + }); + + it("has no demo entry for existing investigations, populated traces, or connecting another agent", async () => { + const user = userEvent.setup(); + const data = createLensDemoData(); + const saved = data.lenses[0]; + network.mockImplementation(async (input) => { + const path = new URL(String(input), "http://localhost").pathname; + if (path === "/lens") return Response.json({ lenses: [saved], workers: [], tracing_enabled: true }); + if (path.endsWith("/runs")) return Response.json(saved.jobs); + if (path === "/v1/traces") return Response.json({ data: data.runs.map((run) => run.trace.summary) }); + return Response.json({ data: [], traces: true, requests: false }); + }); + renderWithProviders(, { + searchParams: `?tab=investigations&lens=${saved.id}`, + }); + expect(await screen.findByRole("heading", { name: saved.settings.name })).toBeVisible(); + expect(screen.queryByRole("button", { name: "Preview sample" })).not.toBeInTheDocument(); + await user.click(within(screen.getByRole("tablist", { name: "Lens" })).getByRole("tab", { name: "Traces" })); + expect(await screen.findByText("Where is order #1042?")).toBeVisible(); + expect(screen.queryByRole("button", { name: "Preview sample" })).not.toBeInTheDocument(); + await user.click(screen.getByRole("button", { name: "Set up tracing" })); + expect(await screen.findByRole("heading", { name: "Connect another agent" })).toBeVisible(); + expect(screen.queryByRole("button", { name: "Preview sample" })).not.toBeInTheDocument(); + }); + + it("shows the header preview only for the active tab that still needs setup", async () => { + const user = userEvent.setup(); + const saved = createLensDemoData().lenses[0]; + network.mockImplementation(async (input) => { + const path = new URL(String(input), "http://localhost").pathname; + if (path === "/lens") return Response.json({ lenses: [saved], workers: [], tracing_enabled: false }); + if (path.endsWith("/runs")) return Response.json(saved.jobs); + if (path === "/v1/traces") return Response.json({ detail: "Tracing is not enabled" }, { status: 501 }); + return Response.json({ data: [], traces: false, requests: false }); + }); + renderWithProviders(); + expect(await screen.findByRole("button", { name: "Preview sample" })).toBeVisible(); + const tabs = within(screen.getByRole("tablist", { name: "Lens" })); + await user.click(tabs.getByRole("tab", { name: "Investigations" })); + expect(await screen.findByRole("button", { name: new RegExp(saved.settings.name) })).toBeVisible(); + expect(screen.queryByRole("button", { name: "Preview sample" })).not.toBeInTheDocument(); + await user.click(tabs.getByRole("tab", { name: "Traces" })); + await user.click(await screen.findByRole("button", { name: "Preview sample" })); + expect(await screen.findByRole("table", { name: "Agent runs" })).toBeVisible(); + expect(screen.queryByRole("button", { name: "Preview sample" })).not.toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensWorkspace.tsx b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensWorkspace.tsx new file mode 100644 index 00000000000..9c7d2ca3db8 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/LensWorkspace.tsx @@ -0,0 +1,117 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Aperture } from "lucide-react"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { parseAsString, parseAsStringLiteral, useQueryState } from "nuqs"; +import AgentTracesPage from "@/components/view_logs/TraceView/AgentTracesPage"; +import { DemoNotice } from "@/components/shared/DemoNotice"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { LensDemoContext, useLensDemo } from "@/components/lens/LensDemoContext"; +import { LensPreviewTarget } from "@/components/lens/LensPreviewButton"; +import { isProxyAdminRole, isProxyAdminTierRole } from "@/utils/roles"; +import { LensView } from "./LensView"; +import { createLensDemo } from "./lensDemoData"; + +type Tab = "traces" | "investigations"; +type WorkspaceProps = { accessToken: string; userRole: string; readOnly: boolean }; + +export function LensWorkspace(props: WorkspaceProps) { + const [demoTab, setDemoTab] = useState(null); + return demoTab ? ( + setDemoTab(null)} /> + ) : ( + + ); +} + +function DemoSession({ initialTab, onExit }: { initialTab: Tab; onExit: () => void }) { + const [demo] = useState(createLensDemo); + const [client] = useState( + () => new QueryClient({ defaultOptions: { queries: { retry: false, staleTime: Infinity } } }), + ); + useEffect( + () => () => { + client.clear(); + }, + [client], + ); + return ( + + + + + + ); +} + +function LensContent({ + accessToken, + userRole, + readOnly, + initialTab = "traces", + onDemo, + onExit, +}: WorkspaceProps & { initialTab?: Tab; onDemo?: (tab: Tab) => void; onExit?: () => void }) { + const demo = useLensDemo(); + const [tab, setTab] = useQueryState( + "tab", + parseAsStringLiteral(["traces", "investigations"]).withOptions({ history: "push" }), + ); + const [lensId] = useQueryState("lens", parseAsString); + const [demoTab, setDemoTab] = useState(initialTab); + const [previewTarget, setPreviewTarget] = useState(null); + const defaultTab = lensId ? "investigations" : "traces"; + const activeTab = demo ? demoTab : tab ?? defaultTab; + const openDemo = onDemo ? () => onDemo(activeTab) : undefined; + return ( + +
    +
    +

    +

    +
    +
    + {demo && } + (demo ? setDemoTab(value as Tab) : void setTab(value as Tab))} + className="min-h-0 flex-1 gap-4" + > + + + Traces + + + Investigations + + + + + + + {demo || isProxyAdminTierRole(userRole) ? ( + + ) : ( +

    + Investigations require proxy administrator access. You can still view your traces. +

    + )} +
    +
    +
    +
    + ); +} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/WorkerSetup.tsx b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/WorkerSetup.tsx index 4554174f37b..931dd7c17d2 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/WorkerSetup.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/WorkerSetup.tsx @@ -21,7 +21,7 @@ import { AnalysisKey, AnalysisAccessFields, createAnalysisKey, type AnalysisAcce import type { LensList, WorkerCreated } from "./lensData"; export const LENS_WORKER_IMAGE = - "ghcr.io/berriai/litellm-lens-worker@sha256:67eba741c1b97c749975c5c38e2370a603e1105babc908d613c1b79d7b995393"; + "ghcr.io/berriai/litellm-lens-worker@sha256:44f0597c7583dcfef999ece9a8bc02cfeb9f0f5167a1221cee3bd10b1b79271b"; function initialProxyAddress(): string { const url = new URL(proxyBaseUrl || serverRootPath, window.location.origin); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/lensData.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/lensData.test.ts index 84e0fe7e6e6..25276fe5a44 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/lensData.test.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/lensData.test.ts @@ -4,6 +4,10 @@ import { workerConnected, type LensList, analysisProgress, + analysisFraction, + analysisPace, + remainingLabel, + stageDurations, normalizeFilters, sortedFindings, type Finding, @@ -58,6 +62,28 @@ const job: Job = { }; describe("Analysis progress", () => { + it.each(["Collecting executions", "Reading executions"])( + "does not turn an unreported run count into zero runs during %s", + (stage) => { + expect(analysisProgress({ ...job, stage })).toEqual({ + step: -1, + title: "Preparing activity", + done: 0, + total: 0, + detail: "Loading the runs selected for this investigation.", + }); + }, + ); + + it("shows the selected count as soon as the worker reports it", () => { + expect(analysisProgress({ ...job, coverage: { ...coverage, selected: 7 } })).toMatchObject({ + step: 0, + done: 0, + total: 7, + detail: "0 of 7 selected runs reviewed", + }); + }); + it("measures review progress against the sample, not all eligible runs", () => { const expected = { step: 0, done: 7, total: 20, detail: "7 of 20 selected runs reviewed" }; expect( @@ -114,6 +140,96 @@ describe("Analysis progress", () => { }); }); +describe("Analysis pace", () => { + it("fills the bar left to right across stages without jumping backwards at a stage boundary", () => { + const endOfReview = analysisFraction({ step: 0, done: 20, total: 20 }); + const startOfGrouping = analysisFraction({ step: 1, done: 0, total: 4 }); + expect(analysisFraction({ step: -1, done: 0, total: 0 })).toBe(0); + expect(analysisFraction({ step: 0, done: 10, total: 20 })).toBeLessThan(endOfReview); + expect(startOfGrouping).toBeCloseTo(endOfReview); + expect(analysisFraction({ step: 2, done: 5, total: 5 })).toBeCloseTo(1); + }); + + it("measures speed within the current stage and projects time left from overall progress", () => { + const start = Date.parse("2026-09-30T12:00:00Z"); + const pace = analysisPace( + [ + { at: start, step: 0, done: 0, fraction: 0 }, + { at: start + 30000, step: 0, done: 30, fraction: 0.25 }, + ], + start + 30000, + ); + expect(pace.perMinute).toBe(60); + expect(pace.secondsLeft).toBe(90); + }); + + it("waits for enough samples instead of showing a wild first estimate", () => { + const start = Date.parse("2026-09-30T12:00:00Z"); + expect(analysisPace([{ at: start, step: 0, done: 1, fraction: 0.01 }], start + 2000)).toEqual({ + perMinute: null, + secondsLeft: null, + }); + expect(remainingLabel(null)).toBe("estimating"); + }); + + it("lengthens the estimate while progress stalls", () => { + const start = Date.parse("2026-09-30T12:00:00Z"); + const samples = [ + { at: start, step: 0, done: 0, fraction: 0 }, + { at: start + 10000, step: 0, done: 10, fraction: 0.1 }, + ]; + const moving = analysisPace(samples, start + 10000).secondsLeft ?? 0; + const stalled = analysisPace(samples, start + 40000).secondsLeft ?? 0; + expect(stalled).toBeGreaterThan(moving); + }); + + it("still estimates when the worker reports progress less than once a minute", () => { + const start = Date.parse("2026-09-30T12:00:00Z"); + const pace = analysisPace( + [ + { at: start, step: 2, done: 0, fraction: 0.8 }, + { at: start + 90000, step: 2, done: 1, fraction: 0.85 }, + ], + start + 90000, + ); + expect(pace.secondsLeft).toBeCloseTo(270); + expect(pace.perMinute).toBeCloseTo(2 / 3); + }); + + it("measures from the last minute rather than the whole run once updates are frequent", () => { + const start = Date.parse("2026-09-30T12:00:00Z"); + const samples = [ + { at: start, step: 0, done: 0, fraction: 0 }, + { at: start + 120000, step: 0, done: 12, fraction: 0.06 }, + { at: start + 180000, step: 0, done: 72, fraction: 0.36 }, + ]; + expect(analysisPace(samples, start + 180000).perMinute).toBe(60); + }); + + it("rounds remaining time up so the label never promises less than the estimate", () => { + expect(remainingLabel(61)).toBe("~2m"); + expect(remainingLabel(30)).toBe("<1m"); + }); +}); + +describe("Stage durations", () => { + const start = Date.parse("2026-09-30T12:00:00Z"); + const createdAt = "2026-09-30T12:00:00Z"; + + it("times finished stages from the transitions it saw and the active stage up to now", () => { + const samples = [ + { at: start + 5000, step: 0, done: 10, fraction: 0.1 }, + { at: start + 124000, step: 1, done: 0, fraction: 0.6 }, + ]; + expect(stageDurations(samples, createdAt, start + 145000)).toEqual([124, 21, null]); + }); + + it("does not guess when a stage started before the page was opened", () => { + const samples = [{ at: start + 90000, step: 1, done: 2, fraction: 0.7 }]; + expect(stageDurations(samples, createdAt, start + 100000)).toEqual([null, null, null]); + }); +}); + describe("Lens selection and findings", () => { it("preserves literal equals signs in a metadata value", () => { expect(normalizeFilters([{ key: " swarm ", value: " research=v2 " }])).toEqual([ diff --git a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/lensData.ts b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/lensData.ts index c066577817b..7aa2df9edbe 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/lensData.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/lensData.ts @@ -108,6 +108,15 @@ export function analysisProgress(job: Job) { : `${investigated} patterns checked against the original activity`, }; } + if (!selected) { + return { + step: -1, + title: "Preparing activity", + done: 0, + total: 0, + detail: "Loading the runs selected for this investigation.", + }; + } return { step: 0, title: "Reviewing activity", @@ -117,8 +126,90 @@ export function analysisProgress(job: Job) { }; } +export function analysisStages(job: Job): { done: number; total: number }[] { + const { + screened = 0, + selected = 0, + grouped_batches = 0, + grouping_batches = 0, + investigated = 0, + candidates = 0, + } = job.coverage ?? {}; + return [ + { done: screened, total: selected }, + { done: grouped_batches, total: grouping_batches }, + { done: investigated, total: candidates }, + ]; +} + +export interface ProgressSample { + at: number; + step: number; + done: number; + fraction: number; +} + +export const stageWeights = [0.6, 0.2, 0.2]; + +export function analysisFraction({ + step, + done, + total, +}: Pick, "step" | "done" | "total">): number { + if (step < 0) return 0; + const before = stageWeights.slice(0, step).reduce((sum, weight) => sum + weight, 0); + return before + stageWeights[step] * (total ? Math.min(1, done / total) : 0); +} + +function windowStart(samples: readonly ProgressSample[], now: number): ProgressSample | undefined { + return samples.findLast((sample) => now - sample.at >= 60000) ?? samples[0]; +} + +export function analysisPace(samples: readonly ProgressSample[], now: number) { + const latest = samples.at(-1); + if (!latest) return { perMinute: null, secondsLeft: null }; + const first = windowStart( + samples.filter((sample) => sample.step === latest.step), + now, + ); + const anchor = windowStart(samples, now); + if (!first || !anchor) return { perMinute: null, secondsLeft: null }; + const stepMinutes = (now - first.at) / 60000; + const perMinute = stepMinutes >= 1 / 6 ? (latest.done - first.done) / stepMinutes : null; + const spanSeconds = (now - anchor.at) / 1000; + const gained = latest.fraction - anchor.fraction; + const secondsLeft = spanSeconds >= 10 && gained > 0 ? ((1 - latest.fraction) * spanSeconds) / gained : null; + return { perMinute, secondsLeft }; +} + +export function stageDurations(samples: readonly ProgressSample[], createdAt: string, now: number): (number | null)[] { + const current = samples.at(-1)?.step ?? -1; + const starts = [0, 1, 2].map((stage) => { + if (stage === 0) return Date.parse(createdAt); + const entered = samples.findIndex( + (sample, index) => index > 0 && sample.step >= stage && samples[index - 1].step < stage, + ); + return entered < 0 ? null : samples[entered].at; + }); + return starts.map((start, stage) => { + if (start === null || stage > current) return null; + const end = stage === current ? now : starts[stage + 1]; + return end === null ? null : Math.max(0, Math.floor((end - start) / 1000)); + }); +} + +export function remainingLabel(seconds: number | null): string { + if (seconds === null) return "estimating"; + if (seconds < 60) return "<1m"; + if (seconds < 3600) return `~${Math.ceil(seconds / 60)}m`; + return `~${Math.floor(seconds / 3600)}h ${Math.ceil((seconds % 3600) / 60)}m`; +} + export function analysisElapsed(createdAt: string, now: number): string { - const seconds = Math.max(0, Math.floor((now - Date.parse(createdAt)) / 1000)); + return durationText(Math.max(0, Math.floor((now - Date.parse(createdAt)) / 1000))); +} + +export function durationText(seconds: number): string { if (!Number.isFinite(seconds)) return "0s"; if (seconds < 60) return `${seconds}s`; if (seconds < 3600) return `${Math.floor(seconds / 60)}m ${seconds % 60}s`; diff --git a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/lensDemoData.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/lensDemoData.test.ts new file mode 100644 index 00000000000..eabae2928bd --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/lensDemoData.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it, vi } from "vitest"; +import { createLensDemo, createLensDemoData } from "./lensDemoData"; +import type { TracePage } from "@/components/view_logs/TraceView/traceTypes"; +import { evidenceTarget } from "./lensData"; + +describe("Lens demo data", () => { + it("links every finding to the quoted original step and assessed run", () => { + const data = createLensDemoData(); + for (const lens of data.lenses) { + for (const job of lens.jobs) { + for (const finding of job.findings ?? []) { + for (const evidence of finding.evidence) { + const target = evidenceTarget(evidence.execution_id); + const run = data.runs.find(({ trace }) => trace.summary.trace_id === target?.id); + const step = run?.details.find((span) => span.span_id === evidence.span_id); + expect(step?.output).toContain(evidence.quote); + expect(job.sample?.executions.map((execution) => execution.id)).toContain(evidence.execution_id); + expect( + job.assessments.find((assessment) => assessment.execution_id === evidence.execution_id)?.[ + finding.kind === "issue" ? "issue_checks" : "pattern_checks" + ], + ).toContain(finding.check_id); + } + } + } + } + }); + + it("keeps trace totals, timestamps, and agent names consistent", () => { + const data = createLensDemoData(); + for (const { trace } of data.runs) { + expect(trace.summary.span_count).toBe(trace.spans.length); + expect(trace.summary.agent_names).toContain(trace.agents[0].name); + expect(trace.summary.error_count).toBe(trace.spans.filter((span) => span.status === "error").length); + for (const span of trace.spans) { + expect(span.start_offset_ms + span.duration_ms).toBeLessThanOrEqual(trace.summary.duration_ms); + } + } + }); + + it("filters time windows locally and rejects writes or unknown reads without network access", async () => { + const network = vi.spyOn(globalThis, "fetch"); + const now = Date.now(); + const { client } = createLensDemo(now); + const all = await client.get("/v1/traces"); + const recent = await client.get("/v1/traces", { query: { start_ms: now - 3600_000, end_ms: now } }); + expect(recent.data.length).toBeGreaterThan(0); + expect(recent.data.length).toBeLessThan(all.data.length); + expect(recent.data.every((trace) => Date.parse(trace.start_time) >= now - 3600_000)).toBe(true); + await expect(client.post("/lens", { body: {} })).rejects.toMatchObject({ status: 403 }); + await expect(client.get("/lens/real-investigation")).rejects.toMatchObject({ status: 404 }); + expect(network).not.toHaveBeenCalled(); + network.mockRestore(); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/lensDemoData.ts b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/lensDemoData.ts new file mode 100644 index 00000000000..8b4e1645c9e --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/lens/_components/lensDemoData.ts @@ -0,0 +1,483 @@ +import { createApiClient } from "@/lib/http/client"; +import type { LensDemo } from "@/components/lens/LensDemoContext"; +import type { Trace, Span, SpanDetail } from "@/components/view_logs/TraceView/traceTypes"; +import type { Lens, Finding, Job, Settings } from "./lensData"; + +type Scenario = { + agent: string; + question: string; + tool: string; + result: string; + answer: string; + failed?: boolean; +}; + +const scenarios: Scenario[] = [ + { + agent: "support_agent", + question: "Where is order #1042?", + tool: "lookup_order", + result: "Order service timed out", + answer: "I will check that for you.", + failed: true, + }, + { + agent: "research_agent", + question: "Which database should we use for analytics?", + tool: "search_docs", + result: "Column stores are designed for analytical queries. No comparative benchmark is available.", + answer: "The column store is 40% faster than the row store for your workload.", + }, + { + agent: "support_agent", + question: "Can I return my headphones?", + tool: "lookup_return_policy", + result: "Returns are accepted within 30 days. This order was delivered 10 days ago.", + answer: "Yes. Your order is eligible for a return. I can help you start it.", + }, + { + agent: "release_agent", + question: "Is the checkout release ready to ship?", + tool: "read_test_results", + result: "Checkout: 120 passed, 0 failed. Rollback plan approved.", + answer: "All 120 checkout tests passed and the rollback plan is approved. The release is ready.", + }, + { + agent: "support_agent", + question: "My order #1088 has not arrived", + tool: "lookup_order", + result: "Order service timed out", + answer: "I will check that for you.", + failed: true, + }, + { + agent: "research_agent", + question: "How long do we retain audit logs?", + tool: "search_docs", + result: "Audit log retention is 90 days on the current plan.", + answer: "Audit logs are retained for 90 days, according to the plan documentation.", + }, + { + agent: "release_agent", + question: "Review the search release", + tool: "read_test_results", + result: "Search: 86 passed, 1 failed. Unicode query regression remains open.", + answer: "Hold the release. The Unicode query regression is still failing.", + }, + { + agent: "support_agent", + question: "Update the address on order #1091", + tool: "lookup_order", + result: "Order #1091 has not shipped. Address changes are allowed.", + answer: "Your order has not shipped. Please confirm the new delivery address.", + }, + { + agent: "research_agent", + question: "Can this plan store data in Europe?", + tool: "search_docs", + result: "Regional availability is not documented for this plan.", + answer: "I could not verify European data residency for this plan. Please confirm with support before choosing it.", + }, + { + agent: "support_agent", + question: "Check the delivery date for order #1107", + tool: "lookup_order", + result: "Order service timed out", + answer: "The order service is unavailable. I can connect you to support instead.", + failed: true, + }, + { + agent: "release_agent", + question: "Summarize the authentication release checks", + tool: "read_test_results", + result: "Authentication: 214 passed, 0 failed. Security review approved.", + answer: "All 214 tests passed and the security review is approved.", + }, + { + agent: "research_agent", + question: "Does the API support request retries?", + tool: "search_docs", + result: "Retry requests with exponential backoff after a 429 or 503 response.", + answer: "Yes. Use exponential backoff for 429 and 503 responses, as documented.", + }, +]; + +const executionId = (traceId: string) => btoa(JSON.stringify(["traces", "", traceId])); +const iso = (time: number) => new Date(time).toISOString(); + +function makeTrace(scene: Scenario, index: number, now: number) { + const traceId = (index + 1).toString(16).padStart(32, "0"); + const spanId = (step: number) => `${index + 1}${step}`.padStart(16, "0"); + const toolCount = scene.failed && index !== 9 ? 3 : 1; + const duration = toolCount === 3 ? 6340 : 3180 + index * 137; + const base: Span = { + agent: scene.agent, + duration_ms: duration, + error: null, + error_truncated: false, + framework: "", + input_preview: scene.question, + input_tokens: 0, + output_tokens: 0, + litellm_request_id: null, + model: null, + name: scene.agent, + parent_span_id: null, + span_id: spanId(0), + spend: null, + start_offset_ms: 0, + status: "ok", + type: "agent", + }; + const tools: Span[] = Array.from({ length: toolCount }, (_, i) => ({ + ...base, + span_id: spanId(i + 1), + parent_span_id: base.span_id, + name: scene.tool, + type: "tool", + start_offset_ms: 150 + i * 1600, + duration_ms: scene.failed ? 1500 : 340, + status: scene.failed ? "error" : "ok", + error: scene.failed ? scene.result : null, + })); + const model: Span = { + ...base, + span_id: spanId(5), + parent_span_id: base.span_id, + name: "Generate response", + type: "llm", + model: "demo-chat-model", + start_offset_ms: toolCount * 1600, + duration_ms: 1200, + input_tokens: 520 + index * 41, + output_tokens: 48 + index * 7, + spend: 0.003 + index * 0.0002, + }; + const trace: Trace = { + summary: { + agent_count: 1, + agent_invocations: 1, + agent_names: [scene.agent], + duration_ms: duration, + error_count: scene.failed ? toolCount : 0, + frameworks: [], + input_preview: scene.question, + input_tokens: model.input_tokens, + output_tokens: model.output_tokens, + llm_calls: 1, + models: [model.model!], + name: scene.agent, + service: "demo-agents", + span_count: toolCount + 2, + spend: model.spend, + start_time: iso(now - (index + 1) * 35 * 60_000), + status: scene.failed ? "error" : "ok", + tool_calls: toolCount, + trace_id: traceId, + }, + agents: [ + { + name: scene.agent, + parent_agent: null, + duration_ms: duration, + invocations: 1, + llm_calls: 1, + tool_calls: toolCount, + spend: model.spend, + }, + ], + spans: [base, ...tools, model], + }; + const details: SpanDetail[] = trace.spans.map((span) => ({ + span_id: span.span_id, + input: + span.type === "tool" + ? JSON.stringify({ query: scene.question }) + : JSON.stringify([{ role: "user", content: scene.question }]), + output: span.type === "tool" ? scene.result : JSON.stringify([{ role: "assistant", content: scene.answer }]), + attributes: { + "gen_ai.agent.name": scene.agent, + "service.name": "demo-agents", + demo: "true", + ...(span.model ? { "gen_ai.request.model": span.model } : {}), + }, + })); + return { trace, details }; +} + +export function createLensDemoData(now = Date.now()) { + const runs = scenarios.map((scene, index) => makeTrace(scene, index, now)); + const finding = ({ + id, + check, + title, + description, + suggestion, + indices, + kind = "issue", + }: { + id: string; + check: string; + title: string; + description: string; + suggestion: string; + indices: number[]; + kind?: Finding["kind"]; + }): Finding => ({ + id, + check_id: check, + title, + description, + suggestion, + kind, + priority: kind === "issue" ? "high" : "low", + status: "open", + reason: "", + revision: 1, + first_seen: iso(now - 86_400_000), + last_seen: iso(now - 300_000), + limitation: "", + occurrences: indices.map((i) => executionId(runs[i].trace.summary.trace_id)), + evidence: indices.map((i) => ({ + execution_id: executionId(runs[i].trace.summary.trace_id), + span_id: runs[i].trace.spans.at(-1)!.span_id, + quote: scenarios[i].answer, + role: "support", + })), + }); + const findingInputs: Parameters[0][] = [ + { + id: "failed-lookups", + check: "recover", + title: "Repeated lookups leave customers without an answer", + description: + "The support agent retries the same unavailable order service three times, then promises to check without answering or offering a handoff.", + suggestion: "After repeated failures, explain the problem and offer a handoff.", + indices: [0, 4], + }, + { + id: "safe-handoff", + check: "recover", + title: "A clear handoff helps when the order service is unavailable", + description: + "The agent explains the service outage and offers a support handoff instead of promising an answer it cannot provide.", + suggestion: "Keep this fallback for unavailable services.", + indices: [9], + kind: "pattern", + }, + { + id: "unsupported-claim", + check: "grounding", + title: "Performance claim has no supporting benchmark", + description: + "The answer claims a 40% performance improvement, but the retrieved documentation contains no comparative benchmark.", + suggestion: "Require a benchmark source for numeric performance claims, or remove the comparison.", + indices: [1], + }, + { + id: "uncertainty", + check: "grounding", + title: "Missing information is acknowledged", + description: + "When documentation does not establish regional availability, the agent says so and asks the user to verify it.", + suggestion: "Keep stating when a source does not answer the question.", + indices: [8], + kind: "pattern", + }, + { + id: "release-blocked", + check: "release", + title: "Failing checks correctly block the release", + description: "The release agent identifies an unresolved regression and recommends holding the release.", + suggestion: "Continue requiring passing checks before recommending a release.", + indices: [6], + kind: "pattern", + }, + ]; + const findings = findingInputs.map(finding); + const definitions = [ + { + id: "support", + name: "Support quality", + agent: "support_agent", + check: "recover", + context: + "Answer the customer's question using order information. If a tool fails, explain the problem and offer a handoff.", + instruction: "Look for repeated failed calls and conversations that end without an answer or a handoff.", + }, + { + id: "research", + name: "Research accuracy", + agent: "research_agent", + check: "grounding", + context: "Answer questions using verified documentation. Acknowledge missing information.", + instruction: "Find claims that are not supported by the retrieved sources.", + }, + { + id: "release", + name: "Release readiness", + agent: "release_agent", + check: "release", + context: "Review test results and recommend a release only when all required checks pass.", + instruction: "Check whether failed tests are acknowledged before a release recommendation.", + }, + ]; + const lenses: Lens[] = definitions.map((definition) => { + const settings: Settings = { + name: definition.name, + agent_name: definition.agent, + context: definition.context, + checks: [{ id: definition.check, instruction: definition.instruction, enabled: true }], + source: "traces", + service: "", + filters: [], + team_id: "", + execution_ids: [], + lookback_hours: 24, + sample_size: 0, + sample_percent: 100, + concurrency: 8, + monthly_budget: 100, + interval_minutes: 30, + enabled: false, + model: "demo-analysis-model", + }; + const executions = runs + .filter(({ trace }) => trace.summary.name === definition.agent) + .map(({ trace }) => ({ + id: executionId(trace.summary.trace_id), + trace_ref: "", + metadata: [], + root_seen: true, + service: trace.summary.service, + source: "traces" as const, + trace_id: trace.summary.trace_id, + team_id: "", + name: trace.summary.name, + start_time: trace.summary.start_time, + span_count: trace.summary.span_count, + })); + const relevant = findings.filter((f) => f.check_id === definition.check); + const jobs: Job[] = [0, 1].map((day) => { + const sample = day === 0 ? executions : executions.slice(1); + const selectedIds = new Set(sample.map((item) => item.id)); + const snapshot = relevant + .map((f) => ({ + ...f, + occurrences: f.occurrences.filter((id) => selectedIds.has(id)), + evidence: f.evidence.filter((e) => selectedIds.has(e.execution_id)), + })) + .filter((f) => f.occurrences.length > 0); + return { + id: `${definition.id}-scan-${day}`, + findings: snapshot, + settings, + revision: 1, + assessments: sample.map((e) => ({ + execution_id: e.id, + cannot_assess: false, + issue_checks: snapshot.some((f) => f.kind === "issue" && f.occurrences.includes(e.id)) + ? [definition.check] + : [], + pattern_checks: snapshot.some((f) => f.kind === "pattern" && f.occurrences.includes(e.id)) + ? [definition.check] + : [], + })), + attempts: 1, + error: "", + cost: sample.length * 0.012, + coverage: { + eligible: sample.length, + selected: sample.length, + screened: sample.length, + investigated: sample.length, + inconclusive: 0, + grouping_batches: 1, + grouped_batches: 1, + candidates: snapshot.length, + partial: 0, + unassessable: 0, + }, + status: "completed", + stage: "Complete", + created_at: iso(now - 330_000 - day * 60_000), + finished_at: iso(now - 300_000 - day * 60_000), + start: iso(now - 86_400_000), + end: iso(now - 300_000 - day * 60_000), + sample: { eligible: sample.length, selected: sample.length, executions: sample }, + }; + }); + return { + id: definition.id, + version: 1, + revision: 1, + spent: jobs.reduce((sum, job) => sum + job.cost, 0), + scope: { all_teams: true, api_key_hash: "", team_id: "" }, + settings, + created_at: iso(now - 2 * 86_400_000), + next_run_at: iso(now), + budget_month: iso(now).slice(0, 7), + findings: relevant, + jobs, + }; + }); + return { runs, lenses }; +} + +function readDemoTrace(data: ReturnType, url: URL): unknown { + const parts = url.pathname.split("/").filter(Boolean); + if (parts[0] !== "v1" || parts[1] !== "traces") return undefined; + if (!parts[2]) { + const start = Number(url.searchParams.get("start_ms") ?? 0); + const end = Number(url.searchParams.get("end_ms") ?? Infinity); + return { + data: data.runs + .map((run) => run.trace.summary) + .filter((trace) => Date.parse(trace.start_time) >= start && Date.parse(trace.start_time) <= end), + next_cursor: null, + }; + } + const run = data.runs.find(({ trace }) => trace.summary.trace_id === parts[2]); + if (!parts[3]) return run?.trace; + if (parts[3] === "spans" && !parts[5]) return run?.details.find((span) => span.span_id === parts[4]); + if (parts[3] === "spans" && parts[5] === "error") { + const span = run?.trace.spans.find((item) => item.span_id === parts[4]); + return span + ? { span_id: span.span_id, message: span.error ?? "", total_chars: span.error?.length ?? 0, next_cursor: null } + : undefined; + } + return undefined; +} + +export function createLensDemo(now = Date.now()): LensDemo { + const data = createLensDemoData(now); + const read = (url: URL): unknown => { + const parts = url.pathname.split("/").filter(Boolean); + if (url.pathname === "/lens") return { lenses: data.lenses, workers: [], tracing_enabled: true }; + if (url.pathname === "/lens/activity/available") return { traces: true, requests: false }; + if (url.pathname === "/models" || url.pathname === "/model_group/info") return { data: [] }; + if (parts[0] === "lens" && parts[2] === "runs") { + const jobs = data.lenses.find((lens) => lens.id === parts[1])?.jobs; + return parts[3] + ? jobs?.find((job) => job.id === parts[3]) + : jobs?.slice(Number(url.searchParams.get("offset") ?? 0)); + } + return readDemoTrace(data, url); + }; + return { + client: createApiClient({ + getBaseUrl: () => "https://lens-demo.invalid", + fetchImpl: async (input, init) => { + if (init?.method !== "GET") return Response.json({ detail: "Demo data is read-only" }, { status: 403 }); + const result = read(new URL(String(input))); + return result === undefined + ? Response.json({ detail: "This item is not in the demo" }, { status: 404 }) + : Response.json(result); + }, + }), + copyTrace: (traceId, spanId) => { + const run = data.runs.find(({ trace }) => trace.summary.trace_id === traceId); + return JSON.stringify(spanId ? run?.details.find((span) => span.span_id === spanId) : run, null, 2); + }, + }; +} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/lens/page.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/lens/page.test.tsx index 148a916c867..5a6fd9cc68f 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/lens/page.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/lens/page.test.tsx @@ -18,12 +18,11 @@ vi.mock("./_components/LensView", () => ({ describe("Lens navigation", () => { beforeEach(() => auth.mockReturnValue({ accessToken: "test-token", userRole: "Admin", isViewOnly: false })); - it("opens investigations and pauses trace polling when returning from traces", async () => { + it("opens traces by default and pauses polling while viewing investigations", async () => { const user = userEvent.setup(); const onUrlUpdate = vi.fn(); renderWithProviders(, { onUrlUpdate }); - expect(screen.getByRole("tab", { name: "Investigations" })).toHaveAttribute("aria-selected", "true"); - await user.click(screen.getByRole("tab", { name: "Traces" })); + expect(screen.getByRole("tab", { name: "Traces" })).toHaveAttribute("aria-selected", "true"); expect(screen.getByText("Trace polling active")).toBeVisible(); await user.click(screen.getByRole("tab", { name: "Investigations" })); expect(screen.getByText("Manage investigations")).toBeVisible(); @@ -39,6 +38,12 @@ describe("Lens navigation", () => { expect(screen.getByText("Manage investigations")).toBeVisible(); }); + it("honors an explicit traces tab even when a saved investigation is in the URL", () => { + renderWithProviders(, { searchParams: "?tab=traces&lens=saved-lens" }); + expect(screen.getByRole("tab", { name: "Traces" })).toHaveAttribute("aria-selected", "true"); + expect(screen.getByText("Trace polling active")).toBeVisible(); + }); + it.each(["Internal User", "Internal Viewer", "Org Admin"])( "preserves trace access without granting investigations to %s", async (userRole) => { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/lens/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/lens/page.tsx index 01f5126e3ae..da13f48165b 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/lens/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/lens/page.tsx @@ -1,60 +1,10 @@ "use client"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; -import { Aperture } from "lucide-react"; -import { parseAsString, parseAsStringLiteral, useQueryState } from "nuqs"; -import AgentTracesPage from "@/components/view_logs/TraceView/AgentTracesPage"; -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { isProxyAdminRole, isProxyAdminTierRole } from "@/utils/roles"; -import { LensView } from "./_components/LensView"; +import { LensWorkspace } from "./_components/LensWorkspace"; export default function LensPage() { const { accessToken, userRole, isViewOnly } = useAuthorized(); - const [tab, setTab] = useQueryState( - "tab", - parseAsStringLiteral(["traces", "investigations"]).withOptions({ history: "push" }), - ); - const [lensId] = useQueryState("lens", parseAsString); - const defaultTab = isProxyAdminTierRole(userRole ?? "") || lensId ? "investigations" : "traces"; - const activeTab = tab ?? defaultTab; if (!accessToken) return null; - return ( -
    -

    -

    - void setTab(value as "traces" | "investigations")} - className="min-h-0 flex-1 gap-4" - > - - - Traces - - - Investigations - - - - - - - {isProxyAdminTierRole(userRole ?? "") ? ( - - ) : ( -

    - Investigations require proxy administrator access. You can still view your traces. -

    - )} -
    -
    -
    - ); + return ; } diff --git a/ui/litellm-dashboard/src/components/view_logs/index.integration.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/logs/page.integration.test.tsx similarity index 90% rename from ui/litellm-dashboard/src/components/view_logs/index.integration.test.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/logs/page.integration.test.tsx index f2d70b74c96..dc8a57e50ba 100644 --- a/ui/litellm-dashboard/src/components/view_logs/index.integration.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/logs/page.integration.test.tsx @@ -1,8 +1,8 @@ import { screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import SpendLogsTable from "./index"; -import { renderWithProviders, testQueryClient } from "../../../tests/test-utils"; +import LogsPage from "./page"; +import { renderWithProviders, testQueryClient } from "../../../../tests/test-utils"; const { useAuthorizedMock, useOrganizationsMock } = vi.hoisted(() => ({ useAuthorizedMock: vi.fn(), @@ -17,7 +17,7 @@ vi.mock("@/app/(dashboard)/hooks/organizations/useOrganizations", () => ({ useOrganizations: useOrganizationsMock, })); -vi.mock("./RequestLogsPanel", () => ({ +vi.mock("@/components/view_logs/RequestLogsPanel", () => ({ default: function RequestLogsPanelMock() { return
    ; }, @@ -47,17 +47,13 @@ const defaultProps = { const ORG_ADMIN_MEMBERSHIPS = [{ organization_id: "org-1", members: [{ user_id: "user-1", user_role: "org_admin" }] }]; const renderAs = (sessionRole: string, organizations: unknown[] = []) => { - useAuthorizedMock.mockReturnValue({ - accessToken: "sk-test", - userId: "user-1", - userRole: sessionRole, - premiumUser: true, - }); + const session = { ...defaultProps, userId: defaultProps.userID, userRole: sessionRole }; + useAuthorizedMock.mockReturnValue(session); useOrganizationsMock.mockReturnValue({ data: organizations }); - return renderWithProviders(); + return renderWithProviders(); }; -describe("SpendLogsTable network access by role", () => { +describe("LogsPage network access by role", () => { beforeEach(() => { testQueryClient.clear(); vi.clearAllMocks(); diff --git a/ui/litellm-dashboard/src/components/view_logs/index.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/logs/page.test.tsx similarity index 88% rename from ui/litellm-dashboard/src/components/view_logs/index.test.tsx rename to ui/litellm-dashboard/src/app/(dashboard)/logs/page.test.tsx index 7c8c14771bd..c7e0a82a363 100644 --- a/ui/litellm-dashboard/src/components/view_logs/index.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/logs/page.test.tsx @@ -1,8 +1,8 @@ import { screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { beforeEach, describe, expect, it, vi } from "vitest"; -import SpendLogsTable from "./index"; -import { renderWithProviders } from "../../../tests/test-utils"; +import LogsPage from "./page"; +import { renderWithProviders } from "../../../../tests/test-utils"; const { useAuthorizedMock, useOrganizationsMock } = vi.hoisted(() => ({ useAuthorizedMock: vi.fn(), @@ -17,25 +17,25 @@ vi.mock("@/app/(dashboard)/hooks/organizations/useOrganizations", () => ({ useOrganizations: useOrganizationsMock, })); -vi.mock("./RequestLogsPanel", () => ({ +vi.mock("@/components/view_logs/RequestLogsPanel", () => ({ default: function RequestLogsPanelMock({ isActive }: { isActive: boolean }) { return
    {isActive ? "active" : "inactive"}
    ; }, })); -vi.mock("./AuditLogsPanel", () => ({ +vi.mock("@/components/view_logs/AuditLogsPanel", () => ({ default: function AuditLogsPanelMock({ isActive }: { isActive: boolean }) { return
    {isActive ? "active" : "inactive"}
    ; }, })); -vi.mock("../DeletedKeysPage/DeletedKeysPage", () => ({ +vi.mock("@/components/DeletedKeysPage/DeletedKeysPage", () => ({ default: function DeletedKeysPageMock() { return
    ; }, })); -vi.mock("../DeletedTeamsPage/DeletedTeamsPage", () => ({ +vi.mock("@/components/DeletedTeamsPage/DeletedTeamsPage", () => ({ default: function DeletedTeamsPageMock() { return
    ; }, @@ -52,16 +52,16 @@ const defaultProps = { const ORG_ADMIN_MEMBERSHIPS = [{ organization_id: "org-1", members: [{ user_id: "user-1", user_role: "org_admin" }] }]; const renderAs = (sessionRole: string, organizations: unknown[] = []) => { - useAuthorizedMock.mockReturnValue({ userId: "user-1", userRole: sessionRole }); + useAuthorizedMock.mockReturnValue({ ...defaultProps, userId: defaultProps.userID, userRole: sessionRole }); useOrganizationsMock.mockReturnValue({ data: organizations }); - return renderWithProviders(); + return renderWithProviders(); }; const tabNames = () => screen.getAllByRole("tab").map((tab) => tab.textContent); -describe("SpendLogsTable", () => { +describe("LogsPage", () => { beforeEach(() => { - useAuthorizedMock.mockReturnValue({ userId: "user-1", userRole: "Admin" }); + useAuthorizedMock.mockReturnValue({ ...defaultProps, userId: defaultProps.userID }); useOrganizationsMock.mockReturnValue({ data: [] }); }); @@ -182,17 +182,17 @@ describe("SpendLogsTable", () => { describe("auth-not-ready guard", () => { it("shows a loading spinner when credentials are not yet resolved", () => { - useAuthorizedMock.mockReturnValue({ userRole: "Admin" }); - renderWithProviders(); + useAuthorizedMock.mockReturnValue({ ...defaultProps, userId: defaultProps.userID, accessToken: null }); + renderWithProviders(); - expect(document.querySelector('[aria-busy="true"]')).toBeInTheDocument(); + expect(screen.getByRole("status", { name: "Loading" })).toHaveAttribute("aria-busy", "true"); expect(screen.queryByRole("tab", { name: "Request Logs" })).not.toBeInTheDocument(); }); it("renders the tabs (no spinner) once all credentials are present", () => { renderAs("Admin"); - expect(document.querySelector('[aria-busy="true"]')).not.toBeInTheDocument(); + expect(screen.queryByRole("status", { name: "Loading" })).not.toBeInTheDocument(); expect(screen.getByRole("tab", { name: "Request Logs" })).toBeInTheDocument(); }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/logs/page.tsx b/ui/litellm-dashboard/src/app/(dashboard)/logs/page.tsx index 88909e3b87f..51603dec29e 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/logs/page.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/logs/page.tsx @@ -1,17 +1,74 @@ "use client"; -import SpendLogsTable from "@/components/view_logs"; +import { useState } from "react"; import useAuthorized from "@/app/(dashboard)/hooks/useAuthorized"; +import useCan from "@/app/(dashboard)/hooks/useCan"; +import DeletedKeysPage from "@/components/DeletedKeysPage/DeletedKeysPage"; +import DeletedTeamsPage from "@/components/DeletedTeamsPage/DeletedTeamsPage"; +import AuditLogsPanel from "@/components/view_logs/AuditLogsPanel"; +import RequestLogsPanel from "@/components/view_logs/RequestLogsPanel"; +import { Page, PageTabs, PageTabsList, PageTabsTrigger, PageTabsContent } from "@/components/shared/Page"; +import { UiLoadingSpinner } from "@/components/ui/ui-loading-spinner"; -export default function Logs() { +type LogsTab = "request logs" | "audit logs" | "deleted keys" | "deleted teams"; + +export default function LogsPage() { const { accessToken, userRole, userId, token, premiumUser } = useAuthorized(); + const [activeTab, setActiveTab] = useState("request logs"); + const canViewAuditLogs = useCan("viewAuditLogs"); + const canViewDeletedTeams = useCan("viewDeletedTeams"); + + const credentialsPending = !accessToken || !token; + const identityPending = !userRole || !userId; + + if (credentialsPending || identityPending) { + return ( +
    + +
    + ); + } + return ( - + + setActiveTab(value)}> + + Request Logs + {canViewAuditLogs && Audit Logs} + Deleted Keys + {canViewDeletedTeams && Deleted Teams} + + + + + + {canViewAuditLogs && ( + + + + )} + + + + {canViewDeletedTeams && ( + + + + )} + + ); } diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.integration.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.integration.test.tsx index 6ab11d1c9ea..3cf2bcdf238 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.integration.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.integration.test.tsx @@ -1944,6 +1944,29 @@ describe("CreateMCPServer", () => { expect(nameInput).toHaveValue("github_mcp"); }); }); + + const sqlitePrefill = { + name: "sqlite", + title: "SQLite", + description: "Local database", + category: "Databases", + transport: "stdio", + command: "uvx", + args: ["mcp-server-sqlite"], + }; + + it("explains that a catalog stdio server cannot be added while the proxy has stdio off", async () => { + render(); + + expect(await screen.findByText("stdio is disabled on this proxy")).toBeInTheDocument(); + }); + + it("shows no stdio banner for a catalog stdio server once stdio is enabled", async () => { + render(); + + await waitFor(() => expect(getServerNameInput()).toHaveValue("sqlite")); + expect(screen.queryByText("stdio is disabled on this proxy")).not.toBeInTheDocument(); + }); }); describe("with back to discovery button", () => { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.tsx index f656bd2fc60..2d6e83aff9b 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/CreateMCPServer.tsx @@ -48,6 +48,7 @@ import MCPServerCostConfig from "./mcp_server_cost_config"; import MCPConnectionStatus from "./mcp_connection_status"; import MCPToolConfiguration from "./mcp_tool_configuration"; import StdioConfiguration from "./StdioConfiguration"; +import { StdioDisabledBanner, TransportSelectItems } from "./StdioAvailability"; import MCPPermissionManagement from "./MCPPermissionManagement"; import OpenAPIFormSection, { OpenAPIKeyTool } from "./OpenAPIFormSection"; import MCPLogoSelector from "./MCPLogoSelector"; @@ -82,6 +83,7 @@ interface CreateMCPServerProps { existingServers?: MCPServer[]; prefillData?: DiscoverableMCPServer | null; onBackToDiscovery?: () => void; + stdioEnabled?: boolean; } const payloadErrorMessage = (result: Exclude): string => { @@ -113,6 +115,7 @@ const CreateMCPServer: React.FC = ({ existingServers, prefillData, onBackToDiscovery, + stdioEnabled = true, }) => { const form = useForm({ mode: "onChange", defaultValues: CREATE_DEFAULTS }); const registry = useMountRegistry(); @@ -750,11 +753,7 @@ const CreateMCPServer: React.FC = ({ - {TRANSPORT_ITEMS.map((item) => ( - - {item.label} - - ))} + )} @@ -917,6 +916,7 @@ const CreateMCPServer: React.FC = ({ {transportType !== "stdio" && transportType !== "" && isAwsSigV4AuthType && } {/* Stdio Configuration - only show for stdio transport */} + {transportType === "stdio" && !stdioEnabled && }
    diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPServerCard.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPServerCard.test.tsx index d298d9d8145..532424a3d5b 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPServerCard.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPServerCard.test.tsx @@ -138,3 +138,36 @@ describe("MCPServerCard network access", () => { expect(screen.queryByText(/^Hub:/)).not.toBeInTheDocument(); }); }); + +describe("MCPServerCard stdio availability", () => { + const stdioServer = { transport: "stdio", url: undefined, command: "python", args: ["server.py"], auth_type: "none" }; + + it("flags a stdio server with how to enable stdio when the proxy has it off", async () => { + const user = userEvent.setup(); + render( + , + ); + + await user.hover(screen.getByText("stdio disabled")); + + expect( + await screen.findByText( + "stdio MCP servers are disabled on this proxy. Set LITELLM_ENABLE_MCP_STDIO=true on the proxy and restart to enable them", + ), + ).toBeInTheDocument(); + }); + + it("does not flag a stdio server when the proxy has stdio on", () => { + render(); + + expect(screen.getByText("STDIO")).toBeInTheDocument(); + expect(screen.queryByText("stdio disabled")).not.toBeInTheDocument(); + }); + + it("does not flag a non-stdio server when the proxy has stdio off", () => { + render(); + + expect(screen.getByText("HTTP")).toBeInTheDocument(); + expect(screen.queryByText("stdio disabled")).not.toBeInTheDocument(); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPServerCard.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPServerCard.tsx index bb153f94665..a9bb2334372 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPServerCard.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/MCPServerCard.tsx @@ -14,6 +14,7 @@ import { cn } from "@/lib/cva.config"; import { AUTH_TYPE, MCP_REACHABLE_DESCRIPTION, type MCPServer } from "@/components/mcp_tools/types"; import { Logo } from "@/components/molecules/logo/Logo"; import { getMaskedAndFullUrl, getMCPNetworkAccess } from "./utils"; +import { STDIO_DISABLED_MESSAGE } from "./StdioAvailability"; interface MCPServerCardProps { server: MCPServer; @@ -29,6 +30,7 @@ interface MCPServerCardProps { onByokConnect?: () => void; onOpenFillFields?: () => void; onDelete?: () => void; + stdioEnabled?: boolean; } const HEALTH_TONE: Record = { @@ -52,6 +54,7 @@ const MCPServerCard: FC = ({ onByokConnect, onOpenFillFields, onDelete, + stdioEnabled = true, }) => { const alias = server.alias || server.server_name || ""; const name = server.server_name || alias || server.server_id; @@ -220,6 +223,19 @@ const MCPServerCard: FC = ({ /> {displayTransport.toUpperCase()} {authType} + {transport === "stdio" && !stdioEnabled && ( + + + + stdio disabled + + } + /> + {STDIO_DISABLED_MESSAGE} + + )} {oauthFlowUnset && ( + + + + + + + , + ); +} + +const option = (name: RegExp) => screen.getByRole("option", { name }); + +describe("TransportSelectItems", () => { + it("greys out only the stdio option and explains how to enable it when stdio is off", async () => { + const user = userEvent.setup(); + openTransportSelect(false); + + expect(option(/Standard Input\/Output \(stdio\)/)).toHaveAttribute("data-disabled"); + expect(option(/Streamable HTTP/)).not.toHaveAttribute("data-disabled"); + expect(option(/Server-Sent Events/)).not.toHaveAttribute("data-disabled"); + expect(option(/OpenAPI Spec/)).not.toHaveAttribute("data-disabled"); + + await user.hover(screen.getByLabelText("question-circle")); + + expect(await screen.findByText(STDIO_DISABLED_MESSAGE)).toBeInTheDocument(); + }); + + it("offers stdio like any other transport when stdio is on", () => { + openTransportSelect(true); + + expect(option(/Standard Input\/Output \(stdio\)/)).not.toHaveAttribute("data-disabled"); + expect(screen.queryByLabelText("question-circle")).not.toBeInTheDocument(); + }); +}); + +describe("useMcpStdioEnabled", () => { + afterEach(() => { + switchToWorkerUrl(null); + vi.restoreAllMocks(); + }); + + const renderWithConfig = (config: object) => { + const fetchSpy = vi + .spyOn(globalThis, "fetch") + .mockImplementation(async () => new Response(JSON.stringify(config), { status: 200 })); + const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + const wrapper = ({ children }: { children: React.ReactNode }) => ( + {children} + ); + const { result } = renderHook(() => useMcpStdioEnabled(), { wrapper }); + const settled = () => + waitFor(() => + expect( + client + .getQueryCache() + .getAll() + .map((query) => query.state.status), + ).toEqual(["success"]), + ); + return { result, fetchSpy, settled }; + }; + + it("reads the flag from the worker the dashboard is managing", async () => { + switchToWorkerUrl("http://worker-b.example:4000"); + const { result, fetchSpy, settled } = renderWithConfig({ mcp_stdio_enabled: false }); + + await settled(); + expect(result.current).toBe(false); + expect(fetchSpy.mock.calls.map(([request]) => (request as Request).url)).toEqual([ + "http://worker-b.example:4000/.well-known/litellm-ui-config", + ]); + }); + + it.each([ + [{ mcp_stdio_enabled: false }, false], + [{ mcp_stdio_enabled: true }, true], + [{}, true], + ])("treats %o as stdio enabled=%s", async (config, enabled) => { + const { result, settled } = renderWithConfig(config); + + await settled(); + expect(result.current).toBe(enabled); + }); + + it("keeps stdio available while the proxy has not answered yet", async () => { + const fetchSpy = vi.spyOn(globalThis, "fetch").mockImplementation(() => new Promise(() => {})); + const client = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + const { result } = renderHook(() => useMcpStdioEnabled(), { + wrapper: ({ children }: { children: React.ReactNode }) => ( + {children} + ), + }); + + await waitFor(() => expect(fetchSpy).toHaveBeenCalled()); + expect(result.current).toBe(true); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/StdioAvailability.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/StdioAvailability.tsx new file mode 100644 index 00000000000..960f0dbbe5d --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/StdioAvailability.tsx @@ -0,0 +1,37 @@ +import { type FC } from "react"; +import { TriangleAlert } from "lucide-react"; +import { Alert, AlertDescription, AlertTitle } from "@/components/shared/Alert"; +import { SelectItem } from "@/components/ui/select"; +import { SimpleTooltip } from "@/components/ui/tooltip"; +import { TRANSPORT, TRANSPORT_ITEMS } from "@/components/mcp_tools/types"; +import { $api } from "@/lib/http/api"; + +export const STDIO_DISABLED_MESSAGE = + "stdio MCP servers are disabled on this proxy. Set LITELLM_ENABLE_MCP_STDIO=true on the proxy and restart to enable them"; + +export const useMcpStdioEnabled = (): boolean => + $api.useQuery("get", "/.well-known/litellm-ui-config").data?.mcp_stdio_enabled !== false; + +export const TransportSelectItems: FC<{ stdioEnabled: boolean }> = ({ stdioEnabled }) => ( + <> + {TRANSPORT_ITEMS.map((item) => { + const disabled = item.value === TRANSPORT.STDIO && !stdioEnabled; + return ( + + {item.label} + {disabled && } + + ); + })} + +); + +export const StdioDisabledBanner: FC = () => ( + + + stdio is disabled on this proxy + + {STDIO_DISABLED_MESSAGE}. Until then this server cannot start or be saved as stdio. + + +); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_edit.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_edit.test.tsx index 5aec78ba926..f79dd178571 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_edit.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_edit.test.tsx @@ -242,6 +242,56 @@ describe("MCPServerEdit (stdio)", () => { }); }); +describe("MCPServerEdit (stdio disabled on the proxy)", () => { + const stdioServer = { + server_id: "server-1", + server_name: "TestServer", + alias: "test", + transport: "stdio", + url: null, + auth_type: "none", + command: "npx", + args: ["-y", "@circleci/mcp-server-circleci"], + created_at: "2024-01-01T00:00:00Z", + created_by: "user-1", + updated_at: "2024-01-01T00:00:00Z", + updated_by: "user-1", + mcp_access_groups: [], + }; + const renderEdit = (mcpServer: object, stdioEnabled: boolean) => + render( + ["mcpServer"]} + accessToken={null} + onCancel={vi.fn()} + onSuccess={vi.fn()} + availableAccessGroups={[]} + stdioEnabled={stdioEnabled} + />, + ); + + it("explains why an existing stdio server cannot run or be saved as stdio", () => { + renderEdit(stdioServer, false); + + expect(screen.getByText("stdio is disabled on this proxy")).toBeInTheDocument(); + expect(screen.getByText(/Set LITELLM_ENABLE_MCP_STDIO=true on the proxy and restart/)).toBeInTheDocument(); + }); + + it("shows no banner for a stdio server once stdio is enabled", () => { + renderEdit(stdioServer, true); + + expect(screen.getByLabelText("Command")).toBeInTheDocument(); + expect(screen.queryByText("stdio is disabled on this proxy")).not.toBeInTheDocument(); + }); + + it("shows no banner for a non-stdio server while stdio is disabled", () => { + renderEdit({ ...stdioServer, transport: "http", url: "https://mcp.example.com/mcp" }, false); + + expect(screen.getByRole("tab", { name: "Server Configuration" })).toBeInTheDocument(); + expect(screen.queryByText("stdio is disabled on this proxy")).not.toBeInTheDocument(); + }); +}); + describe("MCPServerEdit (delegate auth)", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_edit.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_edit.tsx index d45909445e9..3894cb6cd0b 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_edit.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_edit.tsx @@ -44,6 +44,7 @@ import TruePassthroughWarning from "./TruePassthroughWarning"; import PassthroughAuthorizeSection from "./PassthroughAuthorizeSection"; import MCPToolConfiguration from "./mcp_tool_configuration"; import StdioConfiguration from "./StdioConfiguration"; +import { StdioDisabledBanner, TransportSelectItems } from "./StdioAvailability"; import TokenExchangeFormFields from "./TokenExchangeFormFields"; import IdJagFormFields from "./IdJagFormFields"; import OAuthFormFields from "./OAuthFormFields"; @@ -90,6 +91,7 @@ interface MCPServerEditProps { onSuccess: (server: MCPServer) => void; availableAccessGroups: string[]; existingServers?: MCPServer[]; + stdioEnabled?: boolean; } const AUTH_TYPES_REQUIRING_AUTH_VALUE = [AUTH_TYPE.API_KEY, AUTH_TYPE.BEARER_TOKEN, AUTH_TYPE.TOKEN, AUTH_TYPE.BASIC]; @@ -103,6 +105,7 @@ const MCPServerEdit: React.FC = ({ onSuccess, availableAccessGroups, existingServers, + stdioEnabled = true, }) => { const initialStaticHeaders = React.useMemo(() => { if (!mcpServer.static_headers) { @@ -822,6 +825,7 @@ const MCPServerEdit: React.FC = ({ void submitForm(); }} > + {isStdioTransport && !stdioEnabled && } = ({ - {TRANSPORT_ITEMS.map((item) => ( - - {item.label} - - ))} + )} diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_view.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_view.test.tsx index 7e1eda9143f..784bd61382b 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_view.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_view.test.tsx @@ -222,6 +222,31 @@ describe("MCPServerView", () => { expect(screen.queryByText("edit form")).not.toBeInTheDocument(); }); + it.each([ + { transport: "stdio", stdioEnabled: false, shown: true }, + { transport: "stdio", stdioEnabled: true, shown: false }, + { transport: "http", stdioEnabled: false, shown: false }, + ])( + "explains why a $transport server is inert when stdioEnabled=$stdioEnabled", + ({ transport, stdioEnabled, shown }) => { + renderView({ transport }, { stdioEnabled }); + + expect(screen.getByText("srv-1")).toBeInTheDocument(); + expect(screen.queryByText("stdio is disabled on this proxy") !== null).toBe(shown); + }, + ); + + it("leaves the stdio warning to the edit form once editing starts", async () => { + renderView({ transport: "stdio" }, { stdioEnabled: false }); + await userEvent.click(screen.getByRole("tab", { name: "Settings" })); + expect(screen.getByText("stdio is disabled on this proxy")).toBeInTheDocument(); + + await userEvent.click(screen.getByRole("button", { name: "Edit Settings" })); + + expect(screen.getByText("edit form")).toBeInTheDocument(); + expect(screen.queryByText("stdio is disabled on this proxy")).not.toBeInTheDocument(); + }); + it("opens on the tab named by initialTabIndex", async () => { renderView({}, { initialTabIndex: 1 }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_view.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_view.tsx index 278a98a2fa6..02d961a818d 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_view.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_server_view.tsx @@ -7,7 +7,7 @@ import { Button } from "@/components/ui/button"; import { Card } from "@/components/ui/card"; import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; -import { MCPServer, handleTransport, handleAuth } from "@/components/mcp_tools/types"; +import { MCPServer, TRANSPORT, handleTransport, handleAuth } from "@/components/mcp_tools/types"; // TODO: Move Tools viewer from index file import { MCPToolsViewer } from "."; import MCPServerEdit, { EDIT_OAUTH_UI_STATE_KEY } from "./mcp_server_edit"; @@ -15,6 +15,7 @@ import { MCPServerUserCredentialsPanel } from "./MCPServerUserCredentialsPanel"; import { getSecureItem } from "@/utils/secureStorage"; import { isProxyAdminRole, isProxyAdminTierRole } from "@/utils/roles"; import MCPServerCostDisplay from "./mcp_server_cost_display"; +import { StdioDisabledBanner } from "./StdioAvailability"; import { getMaskedAndFullUrl, getMCPNetworkAccess } from "./utils"; import { copyToClipboard as utilCopyToClipboard } from "@/utils/dataUtils"; import { CheckIcon, CopyIcon } from "lucide-react"; @@ -31,6 +32,7 @@ interface MCPServerViewProps { availableAccessGroups: string[]; existingServers?: MCPServer[]; initialTabIndex?: number; + stdioEnabled?: boolean; } // True when this render is the return from the edit-settings OAuth redirect for this @@ -63,6 +65,7 @@ export const MCPServerView: React.FC = ({ availableAccessGroups, existingServers, initialTabIndex = 0, + stdioEnabled = true, }) => { // Open the editing Settings tab on first render when returning from the edit OAuth // redirect, so the "token fetched" feedback shows where the user left off (Settings=2). @@ -74,6 +77,8 @@ export const MCPServerView: React.FC = ({ const networkAccess = getMCPNetworkAccess(mcpServer); const [copiedStates, setCopiedStates] = useState>({}); const [selectedTabIndex, setSelectedTabIndex] = useState(returningFromEditOAuth ? 2 : initialTabIndex); + const editFormShowsStdioBanner = selectedTabIndex === 2 && editing && canEdit; + const showStdioBanner = mcpServer.transport === TRANSPORT.STDIO && !stdioEnabled && !editFormShowsStdioBanner; const canViewUserCredentials = userRole !== null && isProxyAdminTierRole(userRole); const canRevokeUserCredentials = userRole !== null && isProxyAdminRole(userRole) && !isViewOnly; @@ -144,6 +149,8 @@ export const MCPServerView: React.FC = ({ {mcpServer.description &&

    {mcpServer.description}

    }
    + {showStdioBanner && } + setSelectedTabIndex(Number(v))}> @@ -253,6 +260,7 @@ export const MCPServerView: React.FC = ({ onSuccess={handleSuccess} availableAccessGroups={availableAccessGroups} existingServers={existingServers} + stdioEnabled={stdioEnabled} /> ) : (
    diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_servers.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_servers.test.tsx index 9a3ff0cc6cb..2f4fcfd5f69 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_servers.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_servers.test.tsx @@ -20,8 +20,12 @@ vi.mock("@/components/networking", () => ({ listMCPUserEnvVarStatus: vi.fn().mockResolvedValue([]), fetchMCPGatewaySessions: vi.fn(), terminateMCPGatewaySessions: vi.fn(), + getUiConfig: vi.fn().mockResolvedValue({}), })); +const stubUiConfig = (config: object) => + vi.spyOn(globalThis, "fetch").mockImplementation(async () => new Response(JSON.stringify(config), { status: 200 })); + const createQueryClient = () => new QueryClient({ defaultOptions: { @@ -135,6 +139,7 @@ describe("MCPServers", () => { beforeEach(() => { vi.clearAllMocks(); + stubUiConfig({}); }); it("should render the MCPServers component with title", async () => { @@ -299,6 +304,90 @@ describe("MCPServers", () => { expect(screen.getByText("No servers match the current filters or search.")).toBeVisible(); }); + it.each([ + [false, true], + [true, false], + ])("marks stdio servers as disabled only when the proxy reports stdio off (enabled=%s)", async (enabled, flagged) => { + stubUiConfig({ mcp_stdio_enabled: enabled }); + vi.mocked(networking.fetchMCPServers).mockResolvedValue([ + { + server_id: "stdio-1", + server_name: "local_tools", + alias: "local_tools", + transport: "stdio", + command: "python", + args: ["server.py"], + created_by: "user", + updated_by: "user", + } as MCPServer, + ]); + vi.mocked(networking.fetchMCPServerHealth).mockResolvedValue([]); + + render( + + + , + ); + + const grid = await screen.findByTestId("mcp-servers-grid"); + await waitFor(() => expect(globalThis.fetch).toHaveBeenCalled()); + await waitFor(() => expect(within(grid).queryByText("stdio disabled") !== null).toBe(flagged)); + expect(within(grid).getByText("STDIO")).toBeInTheDocument(); + }); + + const stdioServer = { + server_id: "stdio-1", + server_name: "local_tools", + alias: "local_tools", + transport: "stdio", + command: "python", + args: ["server.py"], + created_by: "user", + updated_by: "user", + } as MCPServer; + + it("greys out stdio in the create form when the proxy reports stdio off", async () => { + stubUiConfig({ mcp_stdio_enabled: false }); + vi.mocked(networking.fetchMCPServers).mockResolvedValue([]); + const user = userEvent.setup(); + render( + + + , + ); + await waitFor(() => expect(globalThis.fetch).toHaveBeenCalled()); + + await user.click(await screen.findByRole("button", { name: "+ Submit MCP Server" })); + await user.click(await screen.findByRole("combobox", { name: /Transport Type/ })); + + expect(await screen.findByRole("option", { name: /Standard Input\/Output \(stdio\)/ })).toHaveAttribute( + "data-disabled", + ); + expect(screen.getByRole("option", { name: /Streamable HTTP/ })).not.toHaveAttribute("data-disabled"); + + await user.keyboard("{Escape}"); + await waitFor(() => expect(screen.queryByRole("listbox")).not.toBeInTheDocument()); + }); + + it("explains on the edit page why an existing stdio server cannot run when the proxy reports stdio off", async () => { + stubUiConfig({ mcp_stdio_enabled: false }); + vi.mocked(networking.fetchMCPServers).mockResolvedValue([stdioServer]); + vi.mocked(networking.fetchMCPServerHealth).mockResolvedValue([]); + const user = userEvent.setup(); + render( + + + , + ); + const grid = await screen.findByTestId("mcp-servers-grid"); + await waitFor(() => expect(within(grid).getByText("stdio disabled")).toBeInTheDocument()); + + await user.click(within(grid).getAllByText("local_tools")[0]); + await user.click(await screen.findByRole("tab", { name: "Settings" })); + + expect(await screen.findByText("stdio is disabled on this proxy")).toBeInTheDocument(); + }); + it("should render mocked MCP servers data in the table", async () => { // Mock MCP servers data const mockServers = [ diff --git a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_servers.tsx b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_servers.tsx index 56a18e0ca4c..13510cadf39 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_servers.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/mcp-servers/_components/mcp_servers.tsx @@ -29,6 +29,7 @@ import CreateMCPServer from "./CreateMCPServer"; import ImportMCPServers from "./ImportMCPServers"; import MCPConnect from "./mcp_connect"; import MCPServerCard from "./MCPServerCard"; +import { useMcpStdioEnabled } from "./StdioAvailability"; import { MCPServerView } from "./mcp_server_view"; import type { DiscoverableMCPServer, @@ -225,6 +226,8 @@ const MCPServers: React.FC = ({ accessToken, userRole, userID, i const [sortKey, setSortKey] = useState("created_desc"); const isInternalUser = userRole === "Internal User"; + const stdioEnabled = useMcpStdioEnabled(); + // Single bulk fetch of this user's per-server env-var status. Drives the // red "N user fields missing" footer on each card with no per-row request. const { data: envVarStatuses, refetch: refetchEnvVarStatus } = useQuery({ @@ -500,6 +503,7 @@ const MCPServers: React.FC = ({ accessToken, userRole, userID, i availableAccessGroups={uniqueMcpAccessGroups} existingServers={mcpServers} prefillData={prefillData} + stdioEnabled={stdioEnabled} onBackToDiscovery={() => { setModalVisible(false); setPrefillData(null); @@ -614,6 +618,7 @@ const MCPServers: React.FC = ({ accessToken, userRole, userID, i availableAccessGroups={uniqueMcpAccessGroups} existingServers={mcpServers} initialTabIndex={selectedServerId === toolsTabServerId ? 1 : 0} + stdioEnabled={stdioEnabled} /> ) : (
    @@ -752,6 +757,7 @@ const MCPServers: React.FC = ({ accessToken, userRole, userID, i onByokConnect={server.is_byok ? () => setByokModalServer(server) : undefined} onOpenFillFields={() => setEnvVarsModalServer(server)} onDelete={isAdminRole(userRole) ? () => handleDelete(server.server_id) : undefined} + stdioEnabled={stdioEnabled} /> ))}
    diff --git a/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/ModelInsightsView.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/ModelInsightsView.test.tsx index 36443e90b63..37d265c4e35 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/ModelInsightsView.test.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/ModelInsightsView.test.tsx @@ -155,16 +155,16 @@ describe("ModelInsightsView", () => { render(); await screen.findByText("fast-chat"); const chart = screen.getByTestId("usage-chart"); - const days = (Date.parse(response.end_date) - Date.parse(response.start_date)) / 86_400_000 + 1; expect(screen.getByRole("tab", { name: "Daily" })).toHaveAttribute("aria-selected", "true"); - expect(chart).toHaveAttribute("data-buckets", String(days)); + expect(chart).toHaveAttribute("data-buckets", "30"); + expect(chart).toHaveAttribute("data-first", "2026-08-30"); expect(screen.getByText("Daily tokens across your gateway")).toBeInTheDocument(); await userEvent.click(screen.getByRole("tab", { name: "Weekly" })); - expect(chart).toHaveAttribute("data-buckets", String(Math.ceil(days / 7))); - expect(chart).toHaveAttribute("data-first", response.start_date); + expect(chart).toHaveAttribute("data-buckets", "12"); + expect(chart).toHaveAttribute("data-first", "2026-07-13"); expect(screen.getByText("Weekly tokens across your gateway")).toBeInTheDocument(); }); }); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/ModelInsightsView.tsx b/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/ModelInsightsView.tsx index 312c469903d..ab406597589 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/ModelInsightsView.tsx +++ b/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/ModelInsightsView.tsx @@ -1,5 +1,6 @@ "use client"; +import { Page } from "@/components/shared/Page"; import React from "react"; import { Bar, BarChart, CartesianGrid, Treemap, XAxis, YAxis } from "recharts"; import { ArrowDownRight, ArrowUpRight, BarChart3, Layers, Minus } from "lucide-react"; @@ -7,7 +8,7 @@ import { ArrowDownRight, ArrowUpRight, BarChart3, Layers, Minus } from "lucide-r import { apiClient } from "@/components/networking"; import { extractErrorMessage } from "@/utils/errorUtils"; import { ProviderLogo } from "@/components/molecules/models/ProviderLogo"; -import { PageHeader } from "@/components/shared/PageHeader"; +import { PageHeader, PageHeaderDescription, PageHeaderTitle } from "@/components/shared/PageHeader"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { ChartConfig, ChartContainer, ChartTooltip, ChartTooltipContent } from "@/components/ui/chart"; @@ -210,12 +211,16 @@ export default function ModelInsightsView({ accessToken }: { accessToken: string ) satisfies ChartConfig; return ( -
    - } - title="Model Leaderboard" - subtitle={`See which models your gateway used from ${data.start_date} through ${data.end_date}`} - /> + + + + + Model Leaderboard + + + See which models your gateway used from {data.start_date} through {data.end_date} + + @@ -257,7 +262,7 @@ export default function ModelInsightsView({ accessToken }: { accessToken: string - + -
    + ); } diff --git a/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/modelInsightsData.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/modelInsightsData.test.ts index 00397dd523c..fe164fb1b5e 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/modelInsightsData.test.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/modelInsightsData.test.ts @@ -57,6 +57,37 @@ describe("buildSeries", () => { { date: "2026-01-03", a: 10 }, ]); }); + + it("starts the chart at the first bucket with usage so bars stay wide on a long range", () => { + const rows = [row({ date: "2026-03-10", requests: 1 }), row({ date: "2026-03-20", requests: 2 })]; + const daily = buildSeries(rows, ["a"], "requests", { start: "2025-03-21", end: "2026-03-20", granularity: "day" }); + expect(daily[0]).toEqual({ date: "2026-02-19", a: 0 }); + expect(daily).toHaveLength(30); + expect(daily.at(-1)).toEqual({ date: "2026-03-20", a: 2 }); + + const early = [row({ date: "2025-12-01", requests: 1 }), ...rows]; + const fromFirstUse = buildSeries(early, ["a"], "requests", { + start: "2025-03-21", + end: "2026-03-20", + granularity: "day", + }); + expect(fromFirstUse[0]).toEqual({ date: "2025-12-01", a: 1 }); + expect(fromFirstUse.at(-1)?.date).toBe("2026-03-20"); + }); + + it("keeps weekly buckets on the original grid when trimming idle weeks", () => { + const rows = [row({ date: "2026-03-20", requests: 3 })]; + const weekly = buildSeries(rows, ["a"], "requests", { + start: "2025-03-21", + end: "2026-03-20", + granularity: "week", + }); + expect(weekly).toHaveLength(12); + expect(weekly.map((week) => (Date.parse(String(week.date)) - Date.parse("2025-03-21")) % (7 * 86_400_000))).toEqual( + Array(12).fill(0), + ); + expect(weekly.at(-1)).toEqual({ date: "2026-03-20", a: 3 }); + }); }); describe("buildBucketTotals", () => { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/modelInsightsData.ts b/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/modelInsightsData.ts index e4e83d24362..0e1a5fa7aac 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/modelInsightsData.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/model-insights/_components/modelInsightsData.ts @@ -37,6 +37,7 @@ export type Granularity = "day" | "week"; const DAY_MS = 86_400_000; const BUCKET_DAYS: Record = { day: 1, week: 7 }; +const MIN_VISIBLE_BUCKETS: Record = { day: 30, week: 12 }; export const metricValue = (row: Usage, metric: Metric) => { if (metric === "requests") return row.requests; @@ -85,7 +86,12 @@ export const buildSeries = (rows: DailyMetric[], models: string[], metric: Metri const bucket = buckets[Math.floor((toDay(row.date) - origin) / bucketMs)]; if (bucket) bucket[row.model_group] = Number(bucket[row.model_group] ?? 0) + metricValue(row, metric); } - return buckets; + const firstActive = buckets.findIndex((bucket) => models.some((model) => Number(bucket[model]) > 0)); + const visibleFrom = Math.min( + firstActive === -1 ? buckets.length : firstActive, + buckets.length - MIN_VISIBLE_BUCKETS[window.granularity], + ); + return buckets.slice(Math.max(0, visibleFrom)); }; export const buildBucketTotals = (totals: DailyTotal[], metric: Metric, window: SeriesWindow) => { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AutoRouters/autoRouterRows.test.ts b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AutoRouters/autoRouterRows.test.ts index 79c4243271e..153b77666a7 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AutoRouters/autoRouterRows.test.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AutoRouters/autoRouterRows.test.ts @@ -85,7 +85,8 @@ describe("autoRouterRows", () => { it.each([ ["llm", "LLM Classifier"], - ["jev", "JEV Classifier"], + ["jev", "OSS Classifier"], + ["oss_classifier", "OSS Classifier"], ])("labels a router using the %s classifier", (classifierType, label) => { const row = toAutoRouterRow( { diff --git a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AutoRouters/autoRouterRows.ts b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AutoRouters/autoRouterRows.ts index 1faf3408c23..3c3366a767d 100644 --- a/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AutoRouters/autoRouterRows.ts +++ b/ui/litellm-dashboard/src/app/(dashboard)/models-and-endpoints/components/AutoRouters/autoRouterRows.ts @@ -57,7 +57,8 @@ const dedupe = (models: string[]): string[] => Array.from(new Set(models)); const COMPLEXITY_TYPE_LABELS: Record = { llm: "LLM Classifier", - jev: "JEV Classifier", + jev: "OSS Classifier", + oss_classifier: "OSS Classifier", capability: "Capability", llm_v2: "Fuse v2", heuristic_first: "Heuristic first", diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/JsonEditor.test.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/JsonEditor.test.tsx new file mode 100644 index 00000000000..c861c1d466d --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/JsonEditor.test.tsx @@ -0,0 +1,70 @@ +import { fireEvent, render, screen } from "@testing-library/react"; +import { describe, expect, it, vi } from "vitest"; +import JsonEditor from "./JsonEditor"; +import { validateSystemOnePayload } from "./lib/validatePayload"; + +const validPayload = JSON.stringify( + { state: "Hi", questions: { escalate: { type: "noul", instructions: "Escalate?" } } }, + null, + 2, +); + +describe("JsonEditor", () => { + it("marks a valid payload as ready to send and counts its lines", () => { + render(); + + expect(screen.getByText("Valid payload")).toBeInTheDocument(); + expect(screen.getByRole("status")).toHaveTextContent("Ready to send"); + expect(screen.getByText(`${validPayload.split("\n").length} lines`)).toBeInTheDocument(); + expect(screen.getByRole("textbox", { name: "System One JSON payload" })).toHaveAttribute("aria-invalid", "false"); + }); + + it("lists each issue with its path and counts only errors in the status badge", () => { + const payload = JSON.stringify({ + state: "Hi", + questions: { + category: { type: "choice", instructions: 1, criteria: { support: "Help" } }, + urgency: { type: "score", instructions: "Rate", criteria: ["Low"] }, + }, + }); + render(); + + expect(screen.getByText("2 issues")).toBeInTheDocument(); + const issues = screen.getByRole("list", { name: "Payload validation issues" }); + expect(issues).toHaveTextContent("questions.category.instructionsInstructions must be a string."); + expect(issues).toHaveTextContent("questions.urgency.criteriaScore criteria must contain at least 2 levels."); + expect(screen.getByRole("textbox", { name: "System One JSON payload" })).toHaveAttribute("aria-invalid", "true"); + }); + + it("shows warnings without counting them as issues", () => { + const payload = JSON.stringify({ + state: "Hi", + questions: { urgency: { type: "score", instructions: "Rate", criteria: Array.from({ length: 11 }, () => "L") } }, + }); + render(); + + expect(screen.getByText("Valid payload")).toBeInTheDocument(); + expect(screen.getByRole("list", { name: "Payload validation issues" })).toHaveTextContent( + "More than 10 score levels may reduce result quality.", + ); + }); + + it("numbers every line, including a trailing empty one, so wrapped lines keep their number", () => { + const value = `${validPayload}\n`; + render(); + + const lineCount = value.split("\n").length; + expect(screen.getByText(`${lineCount} lines`)).toBeInTheDocument(); + expect(screen.getByText(String(lineCount))).toBeInTheDocument(); + expect(screen.queryByText(String(lineCount + 1))).not.toBeInTheDocument(); + }); + + it("reports edits to the caller", () => { + const onChange = vi.fn(); + render(); + + fireEvent.change(screen.getByRole("textbox", { name: "System One JSON payload" }), { target: { value: "{}" } }); + + expect(onChange).toHaveBeenCalledWith("{}"); + }); +}); diff --git a/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/JsonEditor.tsx b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/JsonEditor.tsx new file mode 100644 index 00000000000..1effe55ef41 --- /dev/null +++ b/ui/litellm-dashboard/src/app/(dashboard)/playground/components/systemOneUI/JsonEditor.tsx @@ -0,0 +1,167 @@ +import { Badge } from "@/components/ui/badge"; +import { cn } from "@/lib/cva.config"; +import { CircleAlert, CircleCheck, TriangleAlert } from "lucide-react"; +import { useId, useMemo, useRef } from "react"; +import { createElement, PrismLight as SyntaxHighlighter } from "react-syntax-highlighter"; +import type { SyntaxHighlighterProps } from "react-syntax-highlighter"; +import json from "react-syntax-highlighter/dist/esm/languages/prism/json"; +import { findRootBlocks, ROOT_BLOCK_STYLES, type RootBlock } from "./lib/rootBlocks"; +import type { SystemOnePayloadValidation } from "./lib/validatePayload"; + +SyntaxHighlighter.registerLanguage("json", json); + +const EDITOR_TEXT = "m-0 whitespace-pre-wrap wrap-anywhere py-3 font-mono text-xs leading-5 [scrollbar-gutter:stable]"; +const GUTTER_WIDTH = "w-11"; +type LineRendererProps = Parameters>[0]; +const CONTENT_INSET = "pl-14 pr-3"; +const CODE_TAG_PROPS = { className: "language-json", style: { whiteSpace: "pre-wrap" } } as const; + +const TOKEN_COLORS = [ + "[&_.token.property]:text-sky-700 dark:[&_.token.property]:text-sky-300", + "[&_.token.string]:text-emerald-700 dark:[&_.token.string]:text-emerald-300", + "[&_.token.number]:text-amber-700 dark:[&_.token.number]:text-amber-300", + "[&_.token.boolean]:text-violet-700 dark:[&_.token.boolean]:text-violet-300", + "[&_.token.null]:text-violet-700 dark:[&_.token.null]:text-violet-300", + "[&_.token.punctuation]:text-muted-foreground [&_.token.operator]:text-muted-foreground", +].join(" "); + +interface JsonEditorProps { + value: string; + onChange: (value: string) => void; + validation: SystemOnePayloadValidation; +} + +function ValidationStatus({ validation }: { validation: SystemOnePayloadValidation }) { + const errorCount = validation.issues.filter((issue) => issue.severity === "error").length; + if (errorCount > 0) { + return ( + + {errorCount} {errorCount === 1 ? "issue" : "issues"} + + ); + } + return Valid payload; +} + +function IssueList({ id, validation }: { id: string; validation: SystemOnePayloadValidation }) { + if (validation.issues.length === 0) { + return ( +

    + + Ready to send +

    + ); + } + return ( +
      + {validation.issues.map((issue, index) => ( +
    • + {issue.severity === "error" ? ( + + ) : ( + + )} + {issue.path} + {issue.message} +
    • + ))} +
    + ); +} + +function renderLines(rootBlocks: readonly RootBlock[]) { + return function LineRows({ rows, stylesheet, useInlineStyles }: LineRendererProps) { + return rows.map((row, line) => { + const lineElement = { node: row, stylesheet, useInlineStyles, key: line }; + const block = rootBlocks.find(({ startLine, endLine }) => line >= startLine && line <= endLine); + return ( +
    + + {line + 1} + + + {createElement(lineElement)} + +
    + ); + }); + }; +} + +export default function JsonEditor({ value, onChange, validation }: JsonEditorProps) { + const issuesId = useId(); + const highlightRef = useRef(null); + const renderer = useMemo(() => renderLines(findRootBlocks(value)), [value]); + const lineCount = value.split("\n").length; + const hasErrors = !validation.isValid; + + return ( +
    +
    +
    + Request JSON + +
    + + {lineCount} {lineCount === 1 ? "line" : "lines"} + +
    +
    +