Parameter Precedence in Pay-i Instrumentation
Overview
This document summarizes how parameters are handled across different instrumentation methods in Pay-i, particularly when they are nested or combined.
Pay-i's Context Management Foundation: payi_instrument()
payi_instrument()
The foundation of Pay-i's context management and automatic instrumentation is the payi_instrument()
function.
- Initialization: This function initializes the Pay-i SDK, typically called once at application startup.
- Enables Context: It enables the context stack mechanism required for
@track
decorators andtrack_context
managers to function and inherit parameters. - Global Defaults: It establishes the initial Global Context, setting application-wide default parameters.
For full details on its usage and configuration, see the dedicated payi_instrument()
documentation.
Context Types in Pay-i Instrumentation
When initialized via payi_instrument()
, the Pay-i SDK maintains a Context Stack that allows parameters to be sourced and combined from up to four different levels:
-
Global Context
- Created when you call
payi_instrument()
during initialization - Sets default values like the default Use Case (named after your Python module)
- Can be configured with options like
payi_instrument(config={"use_case_name": "MyApp"})
- Initialized once at application startup
- Created when you call
-
@track decorator
- Applied to functions to provide instrumentation for all API calls made within them
- Inherits values from global context and parent decorated functions
- Applies to the entire call tree of the decorated function, including nested function calls (unless overridden)
-
track_context
- A context manager that applies instrumentation to all API calls within its scope
- Inherits from global context and any active decorated function context
- Limited to a specific block of code
-
custom_headers
- Direct parameters passed to individual API calls
- Highest precedence, overrides values from all other contexts
These different methods form a conceptual Context Stack, where inner contexts can inherit from or override parameters set by outer contexts, with custom_headers
having the final say.
Note: For more details, see the Auto-Instrumentation documentation for global context, and the Custom Instrumentation documentation for
@track
,track_context
, andcustom_headers
.
Parameter Behavior Patterns
When contexts are nested (e.g., track_context
inside @track
), parameters are determined through inheritance or override:
- Inheritance: If a nested context doesn't specify a parameter (or uses
None
), it inherits the value from its parent context. - Override: If a nested context does specify a parameter, its value takes precedence. The exact override behavior depends on the parameter's data type and the value provided (non-empty, empty string
""
, or empty list[]
), as detailed below.
Note: The
custom_headers
column below summarizes how headers interact with active decorator/context scopes. This interaction is explained in detail with examples in the "Headers with Decorated Functions" section later in this document.
The following table summarizes how different value types are handled across the instrumentation methods:
Value Type | For @track & track_context | For custom_headers |
---|---|---|
None | Inherits from parent scope | Uses decorator/context value (if scope active) |
Parameter not specified | Same as None (inherits from parent) | Uses decorator/context value (if scope active) |
Empty values ("" , [] ) | Overrides parent values | Sets value to empty |
Non-empty values | Overrides parent values | Overrides decorator/context values |
Non-empty lists | Combines with parent values | Combines with decorator/context values |
Name-ID Pairs | ||
ID only specified | Uses name from parent with specified ID | Uses name from decorator/context with specified ID |
Name only specified | New name with new/specified ID | Overrides both name and ID from decorator/context |
Both specified | Both override parent values | Both override decorator/context values |
Note: Using
@track()
without parameters is optional sincepayi_instrument()
already enables automatic instrumentation. However, explicitly adding it can be useful for making code more self-documenting by visibly marking a function as instrumented while still inheriting all parameter values from parent context.
When methods are nested (like @track
inside @track
, or track_context
inside track_context
), the inner method's parameters interact with the outer method according to these rules.
Generalized Parameter Inheritance Rules by Data Type
Parameter Type | When Value is None or Not Specified | When Value is Empty ("" , [] ) | When Value is Non-Empty |
---|---|---|---|
String (e.g., user_id ) | Inherits from parent scope | Overrides parent value with empty string | Overrides parent value |
List (e.g., limit_ids ) | Inherits from parent scope | Overrides parent values with empty list | Combines with parent values |
Integer (e.g., use_case_version ) | Inherits from parent scope | N/A (no empty state for integers) | Overrides parent value |
These rules are consistent whether you're using nested decorators, nested context managers, or combinations of decorators and context managers.
Key Takeaway on Override Behavior: When a nested context specifies a parameter value, it overrides the inherited value. The behavior depends consistently on whether the parameter is a Scalar (String, Integer) or a List:
- Scalars (Strings, Integers): The parent value is always replaced by the new value.
- Note: For Strings specifically, an empty string
""
also acts as a replacement. Integers lack an equivalent "empty" replacement state.
- Note: For Strings specifically, an empty string
- Lists: The parent value is either combined with the new list items (if the new list is non-empty) or replaced entirely (if the new value is an empty list
[]
or an empty string""
in headers).
Method-Specific Differences
While parameter behavior patterns are consistent across all methods, there are important differences in how each method is applied:
Characteristic | Global Context | @track | track_context | custom_headers |
---|---|---|---|---|
Precedence | Lowest | Medium | High | Highest |
Runtime Values | Fixed at initialization | Functions/globals available at module load | Values available when context created | Values available at API call time |
Scope Duration | All application requests | All calls in decorated function and its calltree | Only within context block | Single API call |
Example | payi_instrument(config={...}) | @track(use_case_name="feature") | with track_context(user_id=current_user.id) | extra_headers={"x-payi-user-id": user_response.id} |
Use Cases for Empty Values
Empty values (""
, []
) provide a way to explicitly override parameters from parent scopes. Some practical reasons to do this:
-
Temporarily Disabling Limits: Setting
limit_ids
to[]
removes all limit constraints from a specific operation. This is useful for:- Admin operations that should bypass spending restrictions
- Critical system messages in chatbots that shouldn't count toward user quotas
- Fallback paths during error handling
-
Redacting User Information: Setting
user_id
to""
can be used when:- Processing shared content that shouldn't be attributed to a specific user
- Handling public/anonymous API endpoints within an otherwise authenticated app
- Running system maintenance tasks that are user-agnostic
Nesting Behavior Examples
Nested @track Decorators
This example shows how parameters behave when decorated functions contain other decorated functions. The key points demonstrated are:
- Parameters from outer decorated functions automatically flow to inner functions
- Inner functions can override inherited parameters with either non-empty values or empty values (
[]
,""
) - List values combine across nested function decorators
@track(limit_ids=["limit1"])
def outer_function():
# A function with no decorator at all
def inner_function0():
# No instrumentation unless the call comes through decorated parent
# When called through outer_function:
# - limit_ids = ["limit1"] (inherits from parent function's decorator)
pass
# Function with empty decorator - equivalent to no parameters
@track()
def inner_function1():
# Result: limit_ids = ["limit1"]
# (Inherits from parent decorator)
pass
@track(limit_ids=[])
def inner_function2():
# Result: limit_ids = []
# (Empty list overrides parent values)
pass
@track(limit_ids=["limit2"])
def inner_function3():
# Result: limit_ids = ["limit1", "limit2"]
# (Lists combine from both decorators)
pass
Note: Functions with no decorator and functions with
@track()
or@track(limit_ids=None)
behave the same way - they inherit parameter values from parent decorated functions when called through the parent's call tree.
Nested track_context Calls
Context managers can be nested to create layers of instrumentation. This pattern provides practical benefits like:
- Adding tenant-specific limits to global app limits for multi-tenant applications
- Temporarily applying user identity to a block of code that handles their requests
- Associating multiple API calls with the same use case for better analytics grouping
- Selectively clearing user attribution with empty values for system operations
These examples show how nested context managers help you track and control resource usage at different levels of your application.
with track_context(limit_ids=["limit1"], user_id="user1"):
# First level context with specific values
with track_context(limit_ids=[], user_id=""):
# Result:
# - limit_ids = [] (Empty list overrides outer context)
# - user_id = "" (Empty string overrides outer context)
pass
with track_context(limit_ids=["limit2"]):
# Result: limit_ids = ["limit1", "limit2"] (Lists combine)
with track_context(limit_ids=["limit3"]):
# Result: limit_ids = ["limit1", "limit2", "limit3"]
# (Lists combine through multiple levels)
pass
Combined @track and track_context
When a function has both decorator and context manager instrumentation, the context manager takes precedence following these rules:
String Parameters (user_id, use_case_name, etc.)
String values in instrumentation parameters follow standard override behavior. This predictable pattern makes it easy to set defaults at the function level via decorators, then override them selectively with context managers where needed:
- Both empty and non-empty strings in the context manager override decorator values
- None values in the context manager inherit from the decorator
@track(user_id="decorator_user")
def function_with_context():
# Default user_id = "decorator_user"
with track_context(user_id="context_user"):
# Result: user_id = "context_user" (context overrides decorator)
client.chat.completions.create(...)
with track_context(user_id=None):
# Result: user_id = "decorator_user" (None inherits from decorator)
client.chat.completions.create(...)
with track_context(user_id=""):
# Result: user_id = "" (Empty string overrides decorator value)
client.chat.completions.create(...)
List Parameters (limit_ids, request_tags)
List parameters interact differently with parent values than string parameters do:
- Lists in the context manager modify parent values in two ways:
- Non-empty lists combine with decorator values (additive behavior)
- Empty lists (
[]
) replace all decorator values (override behavior)
- None values in the context manager inherit the entire list from the decorator
This combining behavior is especially useful for accumulating limits or tags across different levels of your application.
@track(limit_ids=["decorator_limit"])
def function_with_context():
# Default limit_ids = ["decorator_limit"]
with track_context(limit_ids=["context_limit"]):
# Result: limit_ids = ["decorator_limit", "context_limit"] (values combine)
client.chat.completions.create(...)
with track_context(limit_ids=[]):
# Result: limit_ids = [] (Empty list overrides decorator values)
client.chat.completions.create(...)
Numeric Parameters (use_case_version)
Numeric parameters like use_case_version
follow a simple pattern:
- Non-null values override the parent value
- None values inherit from the parent
@track(use_case_version=1)
def function_with_numeric():
# Default: use_case_version = 1 (from decorator)
client.chat.completions.create(...)
with track_context(use_case_version=2):
# Result: use_case_version = 2 (Numeric value overrides decorator)
client.chat.completions.create(...)
Name-ID Pairs (use_case_name/id, experience_name/id)
Name-ID pairs like use_case_name
/use_case_id
or experience_name
/experience_id
have special handling because they're related parameters:
- When only a name is specified, a new ID is automatically generated (unless explicitly provided)
- When only an ID is specified, the name from the parent scope is used with the new ID
- When both are specified, they completely override the parent values
- None values inherit the parent values
- Empty string values for names override inheritance and set to empty
This provides flexibility when working with related parameter pairs that need to be kept in sync.
@track(use_case_name="decorator_case", use_case_id="decorator_id")
def function_with_context():
# Default use_case_name = "decorator_case", use_case_id = "decorator_id"
with track_context(use_case_name="context_case"):
# Result:
# - use_case_name = "context_case"
# - use_case_id = new UUID
# (New name gets new ID unless explicitly provided)
client.chat.completions.create(...)
with track_context(use_case_id="context_id"):
# Result:
# - use_case_name = "decorator_case"
# - use_case_id = "context_id"
# (Decorator name used with context ID)
client.chat.completions.create(...)
with track_context(use_case_name="context_case", use_case_id="context_id"):
# Result:
# - use_case_name = "context_case"
# - use_case_id = "context_id"
# (Context values override decorator completely)
client.chat.completions.create(...)
Headers with Decorated Functions
Custom headers provide direct parameter control at the individual API call level. They can be used standalone on any API call or in combination with active @track
decorators or track_context
scopes. When used standalone (i.e., no active decorator or context scope), there are no decorator/context values to inherit from or combine with. When used in combination, they interact with inherited values as follows:
- String Parameters (e.g.,
x-payi-user-id
): Both empty (""
) and non-empty string values in headers override inherited values for that specific API call. - List Parameters (e.g.,
x-payi-limit-ids
):- An empty string (
""
) in the header overrides the inherited list entirely, resulting in an empty list ([]
) for that call. - A non-empty string in the header is treated as a single element and combined with the inherited list values.
- An empty string (
- Numeric Types (e.g.,
x-payi-use-case-version
): Values override inherited values. - Unspecified Parameters: If a parameter is not included in
extra_headers
, its value is determined by the active decorator/context scope. - Scope: Header parameters only affect the specific API call they are attached to.
This makes headers ideal for one-off parameter overrides without changing the surrounding context.
@track(limit_ids=["limit1"], user_id="default_user")
def function_with_headers():
# Headers with empty values:
client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": "Hello"}],
extra_headers={
"x-payi-limit-ids": "", # Empty string for list parameter
"x-payi-user-id": "" # Empty string for string parameter
}
)
# Result:
# - limit_ids = [] (Empty string in header overrides decorator)
# - user_id = "" (Empty string in header overrides decorator)
# Headers with non-empty values:
client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": "Hello"}],
extra_headers={
"x-payi-limit-ids": "header_limit", # Non-empty list value
"x-payi-use-case-version": "5" # Numeric parameter
}
)
# Result:
# - limit_ids = ["limit1", "header_limit"] (Combined)
# - use_case_version = 5 (Header overrides any inherited value)
# - user_id = "default_user" (Inherited from decorator as not in headers)
Context Manager Calling a Decorated Function
When a decorated function is called from within a context manager:
- The context manager's values take precedence over the decorator's values
- String values from the context manager override the decorator's values
- List values from both sources are combined
This is a particularly important pattern as it allows you to apply function-specific defaults via decorators, then override or enhance them for specific execution contexts.
@track(limit_ids=["decorator_limit"], user_id="decorator_user")
def decorated_function():
# When called directly:
# - limit_ids = ["decorator_limit"]
# - user_id = "decorator_user"
client.chat.completions.create(...)
# When called from within a track_context:
with track_context(limit_ids=["context_limit"], user_id="context_user"):
# Context manager values have higher precedence
decorated_function()
# Result inside the function's API calls:
# - limit_ids = ["decorator_limit", "context_limit"] (combined)
# - user_id = "context_user" (context overrides decorator)
Triple Combination Example
This example demonstrates the most complex nesting scenario combining all three instrumentation methods: decorator, context manager, and custom headers. It also shows nested context managers with different parameter values to illustrate:
- How parameters flow through multiple levels of instrumentation
- The consistent behavior of parameter inheritance rules at each level
- How empty values can override inherited values at any level
- How list values combine across all active scopes
- The effect of adding custom headers at the API call level
@track(limit_ids=["limit1"], user_id="default_user")
def complex_function():
# Starting parameters from decorator:
# - limit_ids = ["limit1"]
# - user_id = "default_user"
with track_context(limit_ids=["limit2"], use_case_name="reporting"):
# First-level context adds:
# - limit_ids = ["limit1", "limit2"] (combined from decorator and context)
# - user_id = "default_user" (from decorator, not overridden)
# - use_case_name = "reporting" (from context)
# Example 1: Direct API call with headers
client.chat.completions.create(
model="gpt-4",
messages=[{"role": "user", "content": "Hello"}],
extra_headers={
"x-payi-limit-ids": "limit3",
"x-payi-user-id": "specific_user"
}
)
# Parameters for this call:
# - limit_ids = ["limit1", "limit2", "limit3"] (combined from all three)
# - user_id = "specific_user" (headers override both decorator and context)
# - use_case_name = "reporting" (from context, not overridden by headers)
# Example 2: Nested context with empty values (overriding values)
with track_context(limit_ids=[], user_id=""):
# Parameters in this nested context:
# - limit_ids = [] (empty list overrides inherited values)
# - user_id = "" (empty string overrides inherited value)
# - use_case_name = "reporting" (still inherited as not overridden)
client.chat.completions.create(...)
# Example 3: Nested context with new values
with track_context(limit_ids=["limit4"], user_id="nested_user"):
# Parameters in this nested context:
# - limit_ids = ["limit1", "limit2", "limit4"] (combines through all levels)
# - user_id = "nested_user" (overrides previously inherited value)
# - use_case_name = "reporting" (still inherited as not overridden)
client.chat.completions.create(...)
This confirms that parameter behavior is consistent regardless of nesting depth. The same rules apply whether you're working with a simple decorator or a complex multi-level combination of all instrumentation methods.
Updated 5 days ago