refactor(otel/v2): drop per-key destination assignment; keys inherit from team and org

Destinations now bind at tenancy granularity only. The per-key logging_exporters
surface was half-shipped (edit-form picker but no create flow) and unrequested,
so it goes: the column leaves the key tables and the migration, /key/generate,
/key/update, and /key/regenerate stop accepting the field, the resolver's union
reads team and org columns only, and the key pages drop the picker and exporter
badges. A key's traces route by its team and org, which the live check confirms:
a brand-new team key exports to the team's destinations with no assignment.

Subset targeting below a whole scope remains available at team granularity
(a multi-team scope with enable-for-entire-scope off, named on specific teams).
Re-adding key granularity later is a purely additive column and field.
This commit is contained in:
Yucheng Zhu 2026-07-28 14:41:13 -07:00
parent 69ba1840d5
commit 36faa5de5a
12 changed files with 13 additions and 150 deletions

View file

@ -4,11 +4,5 @@ ALTER TABLE "LiteLLM_TeamTable" ADD COLUMN IF NOT EXISTS "logging_exporters" TEX
-- AlterTable
ALTER TABLE "LiteLLM_DeletedTeamTable" ADD COLUMN IF NOT EXISTS "logging_exporters" TEXT[] DEFAULT ARRAY[]::TEXT[];
-- AlterTable
ALTER TABLE "LiteLLM_VerificationToken" ADD COLUMN IF NOT EXISTS "logging_exporters" TEXT[] DEFAULT ARRAY[]::TEXT[];
-- AlterTable
ALTER TABLE "LiteLLM_DeletedVerificationToken" ADD COLUMN IF NOT EXISTS "logging_exporters" TEXT[] DEFAULT ARRAY[]::TEXT[];
-- AlterTable
ALTER TABLE "LiteLLM_OrganizationTable" ADD COLUMN IF NOT EXISTS "logging_exporters" TEXT[] DEFAULT ARRAY[]::TEXT[];

View file

@ -445,7 +445,6 @@ model LiteLLM_VerificationToken {
key_type String?
policies String[] @default([])
access_group_ids String[] @default([])
logging_exporters String[] @default([]) // admin-owned OTEL trace destinations assigned to this key (credential names)
model_spend Json @default("{}")
model_max_budget Json @default("{}")
budget_fallbacks Json @default("{}")
@ -541,7 +540,6 @@ model LiteLLM_DeletedVerificationToken {
key_type String?
policies String[] @default([])
access_group_ids String[] @default([])
logging_exporters String[] @default([])
model_spend Json @default("{}")
model_max_budget Json @default("{}")
budget_fallbacks Json @default("{}")

View file

@ -5,7 +5,6 @@ Canonical definition for ``litellm_verificationtoken``. Re-exported from
``litellm.proxy._types`` for backwards compatibility.
"""
from collections.abc import Sequence
from datetime import datetime
from typing import Dict, List, Optional, Union
@ -55,7 +54,6 @@ class LiteLLM_VerificationToken(LiteLLMPydanticObjectBase):
object_permission_id: Optional[str] = None
object_permission: Optional[LiteLLM_ObjectPermissionTable] = None
access_group_ids: Optional[List[str]] = None
logging_exporters: Sequence[str] | None = None
rotation_count: Optional[int] = 0
auto_rotate: Optional[bool] = False
rotation_interval: Optional[str] = None

View file

@ -1084,7 +1084,6 @@ class AllowedVectorStoreIndexItem(LiteLLMPydanticObjectBase):
class KeyRequestBase(GenerateRequestBase):
key: Optional[str] = None
budget_id: Optional[str] = None
logging_exporters: Sequence[str] | None = None
tags: Optional[List[str]] = None
disable_global_guardrails: Optional[bool] = None
throttle_on_budget_exceeded: Optional[bool] = None

View file

@ -625,10 +625,9 @@ async def _effective_org_id(user_api_key_dict: UserAPIKeyAuth) -> str | None:
async def _union_logging_exporter_names(user_api_key_dict: UserAPIKeyAuth, org_id: str | None) -> frozenset[str]:
"""The union of admin-assigned exporter names across the request's identity chain.
Each level is read from its own ``logging_exporters`` column: the key via
``get_key_object`` (the auth object's fields are the team's shadow, so it is
fetched fresh), the team via ``get_team_object``, the org via ``get_org_object``
on the effective ``org_id`` (token org or team fallback). Internal-user is
Each level is read from its own ``logging_exporters`` column: the team via
``get_team_object``, the org via ``get_org_object`` on the effective ``org_id``
(token org or team fallback). Keys inherit from their team and org. Internal-user is
intentionally not a routing dimension. The assignment is an admin-owned column;
the request never supplies it. Needs a DB connection: in SDK mode there is no
identity to resolve against, so this is empty (admin-owned destinations do not
@ -636,7 +635,6 @@ async def _union_logging_exporter_names(user_api_key_dict: UserAPIKeyAuth, org_i
"""
from litellm.proxy import proxy_server
from litellm.proxy.auth.auth_checks import (
get_key_object,
get_org_object,
get_team_object,
)
@ -659,19 +657,6 @@ async def _union_logging_exporter_names(user_api_key_dict: UserAPIKeyAuth, org_i
except Exception: # noqa: BLE001 # best-effort identity enrichment; a failed lookup must not block the request
return ()
key_names = (
await _level(
get_key_object(
hashed_token=user_api_key_dict.token,
prisma_client=prisma_client,
user_api_key_cache=cache,
parent_otel_span=span,
proxy_logging_obj=proxy_server.proxy_logging_obj,
)
)
if user_api_key_dict.token
else ()
)
team_names = (
await _level(
get_team_object(
@ -698,7 +683,7 @@ async def _union_logging_exporter_names(user_api_key_dict: UserAPIKeyAuth, org_i
if org_id
else ()
)
return frozenset((*key_names, *team_names, *org_names))
return frozenset((*team_names, *org_names))
async def _resolve_logging_exporters(

View file

@ -18,7 +18,7 @@ import os
import re
import secrets
import traceback
from collections.abc import Mapping, Sequence
from collections.abc import Mapping
from datetime import datetime, timedelta, timezone
from typing import Any, Callable, Dict, List, Literal, Optional, Tuple, cast
@ -1504,7 +1504,6 @@ async def generate_key_fn(
- metadata: Optional[dict] - Metadata for key, store information for key. Example metadata = {"team": "core-infra", "app": "app2", "email": "ishaan@berri.ai" }
- guardrails: Optional[List[str]] - List of active guardrails for the key
- policies: Optional[List[str]] - List of policy names to apply to the key. Policies define guardrails, conditions, and inheritance rules.
- logging_exporters: Optional[List[str]] - Names of admin-owned logging destinations (credential names) this key exports its traces to.
- disable_global_guardrails: Optional[bool] - Whether to disable global guardrails for the key.
- throttle_on_budget_exceeded: Optional[bool] - When the key exceeds its max_budget, throttle its tpm/rpm to the global budget_exceeded_throttle_percentage instead of blocking the key entirely.
- permissions: Optional[dict] - key-specific permissions. Currently just used for turning off pii masking (if connected). Example - {"pii": false}
@ -1557,9 +1556,6 @@ async def generate_key_fn(
"""
try:
from litellm.proxy._types import CommonProxyErrors
from litellm.proxy.management_endpoints.logging_exporter_validation import (
validate_logging_exporter_field,
)
from litellm.proxy.proxy_server import (
prisma_client,
user_api_key_cache,
@ -1648,11 +1644,6 @@ async def generate_key_fn(
route=KeyManagementRoutes.KEY_GENERATE,
)
# logging_exporters on a key is proxy-admin only. Skip the check when the
# field isn't in the payload to keep /key/generate cheap for the common case.
if data.logging_exporters is not None:
validate_logging_exporter_field(data.logging_exporters, user_api_key_dict)
if team_table is not None:
await _check_team_key_limits(
team_table=team_table,
@ -1830,15 +1821,6 @@ async def generate_service_account_key_fn(
route=KeyManagementRoutes.KEY_GENERATE_SERVICE_ACCOUNT,
)
# Same logging_exporters gate as /key/generate: proxy-admin only. Skip the
# check unless the field is being written.
from litellm.proxy.management_endpoints.logging_exporter_validation import (
validate_logging_exporter_field,
)
if data.logging_exporters is not None:
validate_logging_exporter_field(data.logging_exporters, user_api_key_dict)
data.user_id = None # do not allow user_id to be set for service account keys
return await _common_key_generation_helper(
@ -2559,7 +2541,6 @@ async def update_key_fn( # noqa: C901 # single endpoint handling many optional
- send_invite_email: Optional[bool] - Send invite email to user_id
- guardrails: Optional[List[str]] - List of active guardrails for the key
- policies: Optional[List[str]] - List of policy names to apply to the key. Policies define guardrails, conditions, and inheritance rules.
- logging_exporters: Optional[List[str]] - Names of admin-owned logging destinations (credential names) this key exports its traces to.
- disable_global_guardrails: Optional[bool] - Whether to disable global guardrails for the key.
- throttle_on_budget_exceeded: Optional[bool] - When the key exceeds its max_budget, throttle its tpm/rpm to the global budget_exceeded_throttle_percentage instead of blocking the key entirely.
- prompts: Optional[List[str]] - List of prompts that the key is allowed to use.
@ -2594,9 +2575,6 @@ async def update_key_fn( # noqa: C901 # single endpoint handling many optional
}'
```
"""
from litellm.proxy.management_endpoints.logging_exporter_validation import (
validate_logging_exporter_field,
)
from litellm.proxy.proxy_server import (
llm_router,
premium_user,
@ -2623,16 +2601,6 @@ async def update_key_fn( # noqa: C901 # single endpoint handling many optional
prisma_client=prisma_client,
)
# logging_exporters is proxy-admin only. The validator no-ops when the
# effective value doesn't change; pass the stored column value so a
# non-admin cannot clear an admin-assigned one.
if data.logging_exporters is not None:
validate_logging_exporter_field(
data.logging_exporters,
user_api_key_dict,
existing_exporters=getattr(existing_key_row, "logging_exporters", None),
)
await _validate_update_key_data(
data=data,
existing_key_row=existing_key_row,
@ -3621,7 +3589,6 @@ async def generate_key_helper_fn(
rotation_interval: Optional[str] = None,
router_settings: Optional[dict] = None,
access_group_ids: Optional[list] = None,
logging_exporters: Sequence[str] | None = None, # admin-owned OTEL destinations (credential names)
budget_limits: Optional[list] = None, # multiple concurrent budget windows
):
from litellm.proxy.proxy_server import premium_user, prisma_client
@ -3759,7 +3726,6 @@ async def generate_key_helper_fn(
"object_permission_id": object_permission_id,
"router_settings": router_settings_json,
"access_group_ids": access_group_ids or [],
"logging_exporters": logging_exporters or [],
}
# Add rotation fields if auto_rotate is enabled
@ -4826,23 +4792,6 @@ async def regenerate_key_fn( # noqa: C901 # single endpoint handling many opti
is_proxy_admin=_regen_is_proxy_admin,
)
# logging_exporters gate on regenerate matches /key/generate and
# /key/update. Without this, a key owner could set logging_exporters on
# /key/{id}/regenerate and route future traces to a destination they
# aren't allowed to assign (Veria F3). The validator no-ops when the
# effective value doesn't change; pass the stored column value so a
# non-admin cannot clear an admin-assigned one.
if data is not None and data.logging_exporters is not None:
from litellm.proxy.management_endpoints.logging_exporter_validation import (
validate_logging_exporter_field,
)
validate_logging_exporter_field(
data.logging_exporters,
user_api_key_dict,
existing_exporters=getattr(_key_in_db, "logging_exporters", None),
)
verbose_proxy_logger.info(
"Key regeneration requested: key_alias=%s",
getattr(_key_in_db, "key_alias", None),

View file

@ -445,7 +445,6 @@ model LiteLLM_VerificationToken {
key_type String?
policies String[] @default([])
access_group_ids String[] @default([])
logging_exporters String[] @default([]) // admin-owned OTEL trace destinations assigned to this key (credential names)
model_spend Json @default("{}")
model_max_budget Json @default("{}")
budget_fallbacks Json @default("{}")
@ -541,7 +540,6 @@ model LiteLLM_DeletedVerificationToken {
key_type String?
policies String[] @default([])
access_group_ids String[] @default([])
logging_exporters String[] @default([])
model_spend Json @default("{}")
model_max_budget Json @default("{}")
budget_fallbacks Json @default("{}")

View file

@ -445,7 +445,6 @@ model LiteLLM_VerificationToken {
key_type String?
policies String[] @default([])
access_group_ids String[] @default([])
logging_exporters String[] @default([]) // admin-owned OTEL trace destinations assigned to this key (credential names)
model_spend Json @default("{}")
model_max_budget Json @default("{}")
budget_fallbacks Json @default("{}")
@ -541,7 +540,6 @@ model LiteLLM_DeletedVerificationToken {
key_type String?
policies String[] @default([])
access_group_ids String[] @default([])
logging_exporters String[] @default([])
model_spend Json @default("{}")
model_max_budget Json @default("{}")
budget_fallbacks Json @default("{}")

View file

@ -5232,11 +5232,12 @@ async def test_resolve_logging_exporters_team_level(_seeded_logging_credentials,
@pytest.mark.asyncio
async def test_resolve_logging_exporters_unions_key_team_org(
async def test_resolve_logging_exporters_unions_team_org_keys_inherit(
_seeded_logging_credentials, monkeypatch
):
# key, team, and org are each read from their OWN logging_exporters column.
# All three union, deduped.
# team and org are each read from their OWN logging_exporters column and union,
# deduped. Keys have no assignment column: they inherit from team/org, so a
# key-level value (patched below) must contribute nothing.
from litellm.proxy.litellm_pre_call_utils import _resolve_logging_exporters
_patch_identity(monkeypatch, key=["arize-prod"], team=["langfuse-eu"], org=["langfuse-eu"])
@ -5245,9 +5246,8 @@ async def test_resolve_logging_exporters_unions_key_team_org(
assert {d["endpoint"] for d in destinations} == {
"https://cloud.langfuse.com/api/public/otel", # team + org (deduped)
"https://otlp.arize.com/v1", # key
}
assert set(backends) == {"langfuse_otel", "arize"}
assert set(backends) == {"langfuse_otel"}
@pytest.mark.asyncio

View file

@ -36,8 +36,6 @@ import { fetchTeamModels } from "../organisms/create_key_button";
import NumericalInput from "../shared/numerical_input";
import { Tag } from "../tag_management/types";
import EditLoggingSettings from "../team/EditLoggingSettings";
import { LoggingExportersFormItem } from "../logging_credentials/LoggingExportersSelect";
import { loggingExportersOf } from "../logging_credentials/loggingExportersOf";
import VectorStoreSelector from "../vector_store_management/VectorStoreSelector";
interface KeyEditViewProps {
@ -210,7 +208,6 @@ export function KeyEditView({
accessGroups: keyData.object_permission?.agent_access_groups || [],
},
logging_settings: extractLoggingSettings(keyData.metadata),
logging_exporters: loggingExportersOf(keyData),
disabled_callbacks: Array.isArray(keyData.metadata?.litellm_disabled_callbacks)
? mapInternalToDisplayNames(keyData.metadata.litellm_disabled_callbacks)
: [],
@ -242,8 +239,7 @@ export function KeyEditView({
mcp_tool_permissions: keyData.object_permission?.mcp_tool_permissions || {},
throttle_on_budget_exceeded: keyData.metadata?.throttle_on_budget_exceeded || false,
logging_settings: extractLoggingSettings(keyData.metadata),
logging_exporters: loggingExportersOf(keyData),
disabled_callbacks: Array.isArray(keyData.metadata?.litellm_disabled_callbacks)
disabled_callbacks: Array.isArray(keyData.metadata?.litellm_disabled_callbacks)
? mapInternalToDisplayNames(keyData.metadata.litellm_disabled_callbacks)
: [],
access_group_ids: keyData.access_group_ids || [],
@ -834,7 +830,6 @@ export function KeyEditView({
<Input value={projectDisplay ?? ""} disabled />
</Form.Item>
)}
<LoggingExportersFormItem tooltip="Trace destinations this key exports to." />
<Form.Item label="Logging Settings" name="logging_settings">
<EditLoggingSettings

View file

@ -8,22 +8,14 @@ import { ArrowLeftIcon } from "@heroicons/react/outline";
import { Badge, Button, Card, Grid, Tab, TabGroup, TabList, TabPanel, TabPanels, Text, Title } from "@tremor/react";
import { Form, Modal, Tag } from "antd";
import { KeyInfoHeader } from "./KeyInfoHeader";
import { useEffect, useMemo, useState } from "react";
import {
canReadCredentialsRole,
isProxyAdminRole,
isUserTeamAdminForSingleTeam,
rolesWithWriteAccess,
} from "../../utils/roles";
import { useEffect, useState } from "react";
import { isProxyAdminRole, isUserTeamAdminForSingleTeam, rolesWithWriteAccess } from "../../utils/roles";
import { mapDisplayToInternalNames, mapInternalToDisplayNames } from "../callback_info_helpers";
import AutoRotationView from "../common_components/AutoRotationView";
import DeleteResourceModal from "../common_components/DeleteResourceModal";
import { extractLoggingSettings, formatMetadataForDisplay, stripTagsFromMetadata } from "../key_info_utils";
import { KeyResponse } from "../key_team_helpers/key_list";
import LoggingSettingsView from "../logging_settings_view";
import { loggingExportersOf } from "../logging_credentials/loggingExportersOf";
import { useCredentials } from "@/app/(dashboard)/hooks/credentials/useCredentials";
import { useOrganizations } from "@/app/(dashboard)/hooks/organizations/useOrganizations";
import NotificationManager from "../molecules/notifications_manager";
import { getPolicyInfoWithGuardrails, keyDeleteCall, keyUpdateCall } from "../networking";
import { useResetKeySpend } from "@/app/(dashboard)/hooks/keys/useResetKeySpend";
@ -78,8 +70,6 @@ export default function KeyInfoView({
const queryClient = useQueryClient();
const canEditGuardrails = premiumUser || (userRole != null && rolesWithWriteAccess.includes(userRole));
const { teams: teamsData } = useTeams();
const { data: keyCredentialsData } = useCredentials(canReadCredentialsRole(userRole));
const { data: keyOrganizationsData } = useOrganizations();
const { data: projects } = useProjects();
const { data: uiSettingsData } = useUISettings();
const enableProjectsUI = Boolean(uiSettingsData?.values?.enable_projects_ui);
@ -96,25 +86,6 @@ export default function KeyInfoView({
// Add local state to maintain key data and track regeneration
const [currentKeyData, setCurrentKeyData] = useState<KeyResponse | undefined>(keyData);
// Destinations whose credential_info.access targets THIS key (via its team_id,
// its team's organization_id, or global). Rendered alongside the key's own
// metadata.logging_exporters so the Logging Exporters section reflects BOTH
// routing directions, matching the resolver's union at request time.
const scopedExportersForKey = useMemo<string[]>(() => {
const keyTeamId = (currentKeyData as { team_id?: string | null } | undefined)?.team_id ?? null;
const team = (teamsData ?? []).find((t) => t.team_id === keyTeamId);
const teamOrgId = (team as { organization_id?: string | null } | undefined)?.organization_id ?? null;
return (keyCredentialsData?.credentials ?? [])
.filter((c) => c.credential_info?.credential_type === "logging")
.filter((c) => {
const access = c.credential_info?.access;
if (!access) return false;
if (access.global === true) return true;
if (Array.isArray(access.teams) && keyTeamId && access.teams.includes(keyTeamId)) return true;
return Array.isArray(access.orgs) && teamOrgId != null && access.orgs.includes(teamOrgId);
})
.map((c) => c.credential_name);
}, [keyCredentialsData?.credentials, currentKeyData, teamsData, keyOrganizationsData]);
const [lastRegeneratedAt, setLastRegeneratedAt] = useState<Date | null>(null);
const [isRecentlyRegenerated, setIsRecentlyRegenerated] = useState(false);
const [policyGuardrails, setPolicyGuardrails] = useState<Record<string, string[]>>({});
@ -711,8 +682,6 @@ export default function KeyInfoView({
<LoggingSettingsView
loggingConfigs={extractLoggingSettings(currentKeyData.metadata)}
loggingExporters={loggingExportersOf(currentKeyData)}
scopedExporters={scopedExportersForKey}
disabledCallbacks={
Array.isArray(currentKeyData.metadata?.litellm_disabled_callbacks)
? mapInternalToDisplayNames(currentKeyData.metadata.litellm_disabled_callbacks)
@ -992,8 +961,6 @@ export default function KeyInfoView({
<LoggingSettingsView
loggingConfigs={extractLoggingSettings(currentKeyData.metadata)}
loggingExporters={loggingExportersOf(currentKeyData)}
scopedExporters={scopedExportersForKey}
disabledCallbacks={
Array.isArray(currentKeyData.metadata?.litellm_disabled_callbacks)
? mapInternalToDisplayNames(currentKeyData.metadata.litellm_disabled_callbacks)

View file

@ -6570,7 +6570,6 @@ export interface paths {
* - metadata: Optional[dict] - Metadata for key, store information for key. Example metadata = {"team": "core-infra", "app": "app2", "email": "ishaan@berri.ai" }
* - guardrails: Optional[List[str]] - List of active guardrails for the key
* - policies: Optional[List[str]] - List of policy names to apply to the key. Policies define guardrails, conditions, and inheritance rules.
* - logging_exporters: Optional[List[str]] - Names of admin-owned logging destinations (credential names) this key exports its traces to.
* - disable_global_guardrails: Optional[bool] - Whether to disable global guardrails for the key.
* - throttle_on_budget_exceeded: Optional[bool] - When the key exceeds its max_budget, throttle its tpm/rpm to the global budget_exceeded_throttle_percentage instead of blocking the key entirely.
* - permissions: Optional[dict] - key-specific permissions. Currently just used for turning off pii masking (if connected). Example - {"pii": false}
@ -6977,7 +6976,6 @@ export interface paths {
* - send_invite_email: Optional[bool] - Send invite email to user_id
* - guardrails: Optional[List[str]] - List of active guardrails for the key
* - policies: Optional[List[str]] - List of policy names to apply to the key. Policies define guardrails, conditions, and inheritance rules.
* - logging_exporters: Optional[List[str]] - Names of admin-owned logging destinations (credential names) this key exports its traces to.
* - disable_global_guardrails: Optional[bool] - Whether to disable global guardrails for the key.
* - throttle_on_budget_exceeded: Optional[bool] - When the key exceeds its max_budget, throttle its tpm/rpm to the global budget_exceeded_throttle_percentage instead of blocking the key entirely.
* - prompts: Optional[List[str]] - List of prompts that the key is allowed to use.
@ -24095,8 +24093,6 @@ export interface components {
* @default default
*/
key_type: components["schemas"]["LiteLLMKeyType"] | null;
/** Logging Exporters */
logging_exporters?: string[] | null;
/** Max Budget */
max_budget?: number | null;
/** Max Parallel Requests */
@ -24254,8 +24250,6 @@ export interface components {
key_type?: string | null;
/** Litellm Budget Table */
litellm_budget_table?: unknown | null;
/** Logging Exporters */
logging_exporters?: string[] | null;
/** Max Budget */
max_budget?: number | null;
/** Max Parallel Requests */
@ -25268,8 +25262,6 @@ export interface components {
} | null;
/** Litellm Changed By */
litellm_changed_by?: string | null;
/** Logging Exporters */
logging_exporters?: string[] | null;
/** Max Budget */
max_budget?: number | null;
/** Max Parallel Requests */
@ -26668,8 +26660,6 @@ export interface components {
litellm_budget_table?: {
[key: string]: unknown;
} | null;
/** Logging Exporters */
logging_exporters?: string[] | null;
/** Max Budget */
max_budget?: number | null;
/** Max Parallel Requests */
@ -28634,8 +28624,6 @@ export interface components {
key_type?: string | null;
/** Litellm Budget Table */
litellm_budget_table?: unknown | null;
/** Logging Exporters */
logging_exporters?: string[] | null;
/** Max Budget */
max_budget?: number | null;
/** Max Parallel Requests */
@ -30420,8 +30408,6 @@ export interface components {
* @default default
*/
key_type: components["schemas"]["LiteLLMKeyType"] | null;
/** Logging Exporters */
logging_exporters?: string[] | null;
/** Max Budget */
max_budget?: number | null;
/** Max Parallel Requests */
@ -32508,8 +32494,6 @@ export interface components {
key: string;
/** Key Alias */
key_alias?: string | null;
/** Logging Exporters */
logging_exporters?: string[] | null;
/** Max Budget */
max_budget?: number | null;
/** Max Parallel Requests */
@ -33371,8 +33355,6 @@ export interface components {
litellm_budget_table?: {
[key: string]: unknown;
} | null;
/** Logging Exporters */
logging_exporters?: string[] | null;
/** Max Budget */
max_budget?: number | null;
/** Max Parallel Requests */