Fireworks AI developer on retainer: FireFunction-v2 injects full conversation history into context function argument — args[“context”] contains 50+ messages — 25× cost overrun for 8 weeks — fireworks-ai 0.15.0 — Fireworks developer on monthly retainer
fireworks-ai 0.15.0 • Python 3.11 • accounts/fireworks/models/firefunction-v2 • 4 functions in tools • get_related_products schema includes a “context” parameter • FireFunction-v2 injects full conversation history into “context” when schema defines it • 52 message dicts in args[“context”] instead of 1–3 product IDs • downstream iterates all 52 items • 25× cost overrun for 8 weeks • fix: remove “context” from function schema • 2.5h retainer engagement
Eastbrook AI Ltd. built a product recommendation assistant using FireFunction-v2 in August 2026. The assistant let customers browse a product catalog and used function calling to retrieve product details, check inventory, and generate personalized recommendations. The system returned recommendations successfully, but the recommendation quality was unexpectedly poor in long browsing sessions: the model appeared to ignore the products the user had looked at during the session. No errors were logged. A cost review in October 2026 also flagged a 25× higher-than-expected inference cost on the get_related_products function call. They engaged a Fireworks retainer developer to investigate both symptoms simultaneously.
The function calling setup at the time of the engagement:
# recommender.py
# Eastbrook AI Ltd.
# fireworks-ai 0.15.0, Python 3.11
import json
from fireworks.client import Fireworks
client = Fireworks(api_key="FIREWORKS_API_KEY")
TOOLS = [
{
"type": "function",
"function": {
"name": "get_product_details",
"description": "Get the full details of a product by its ID.",
"parameters": {
"type": "object",
"properties": {
"product_id": {"type": "string"},
},
"required": ["product_id"],
},
},
},
{
"type": "function",
"function": {
"name": "get_related_products",
"description": "Get products related to a given product, filtered by items the user has already viewed.",
"parameters": {
"type": "object",
"properties": {
"product_id": {"type": "string"},
"context": {
# ← PROBLEM: The parameter is named "context".
# FireFunction-v2 has a special behavior: when ANY function
# in the tools list has a parameter named "context",
# FireFunction-v2 injects the FULL conversation history
# (all messages[] entries) into that parameter.
# The injection overrides whatever the user passes and
# ignores the schema's type and description constraints.
# Intent: pass 1-3 product_ids from this session.
# What actually arrives: 52 message dicts (full history).
"type": "array",
"items": {"type": "string"},
"description": "Product IDs the user viewed in this session.",
"default": [],
},
"limit": {"type": "integer", "default": 5},
},
"required": ["product_id"],
},
},
},
{
"type": "function",
"function": {
"name": "check_inventory",
"description": "Check the inventory level for a product.",
"parameters": {
"type": "object",
"properties": {
"product_id": {"type": "string"},
},
"required": ["product_id"],
},
},
},
{
"type": "function",
"function": {
"name": "get_user_history",
"description": "Get the purchase history for a user.",
"parameters": {
"type": "object",
"properties": {
"user_id": {"type": "string"},
},
"required": ["user_id"],
},
},
},
]
def get_recommendations(messages: list, product_id: str) -> list[str]:
response = client.chat.completions.create(
model="accounts/fireworks/models/firefunction-v2",
messages=messages, # 52 messages after a long browsing session
tools=TOOLS,
tool_choice="auto",
)
msg = response.choices[0].message
if msg.tool_calls:
call = msg.tool_calls[0]
args = json.loads(call.function.arguments)
# args["context"] is expected to be ["prod_001", "prod_007", "prod_023"]
# BUT: args["context"] is actually a list of 52 message dicts:
# [
# {"role": "user", "content": "show me running shoes"},
# {"role": "assistant", "content": "Here are some options..."},
# ...52 items...
# ]
#
# TypeError: string indices must be integers
# (when downstream code does: for product_id in args["context"]: db.get(product_id))
# Caught by generic except → return fallback top-5 popular products
related = recommend_engine.get_related(
product_id=args["product_id"],
context_products=args.get("context", []), # ← gets 52 message dicts
limit=args.get("limit", 5),
)
return related
return []
How FireFunction-v2 context injection works
FireFunction-v2 is a specialized function calling model trained by Fireworks AI on conversational function use. It includes an automatic context-passing feature: when the model determines that a function needs access to the conversation history — for example, to filter recommendations by what the user has already discussed — it injects the full messages list as the value of any parameter named context.
This injection is triggered purely by parameter naming. If any function in the tools list has a parameter with the key "context", FireFunction-v2 will populate it with the complete conversation history as a list of {"role": str, "content": str} dicts. The schema’s type, description, and any other constraints are ignored for the injection. The resulting function_call.arguments JSON is well-formed (valid JSON, no error), which means json.loads() succeeds and the bug only surfaces in downstream code that processes the argument value.
# Demonstrating FireFunction-v2 context injection
# fireworks-ai 0.15.0, Python 3.11
import json
from fireworks.client import Fireworks
client = Fireworks(api_key="FIREWORKS_API_KEY")
# 52-message conversation history (26 browsing turns)
long_conversation = [
{"role": "user", "content": "show me running shoes"},
{"role": "assistant", "content": "Here are some options in the running shoes category..."},
{"role": "user", "content": "what about trail running shoes?"},
{"role": "assistant", "content": "For trail running, these models are popular..."},
# ... 48 more messages ...
]
# Test 1: function schema WITH "context" parameter
response_with_context_param = client.chat.completions.create(
model="accounts/fireworks/models/firefunction-v2",
messages=long_conversation + [
{"role": "user", "content": "find shoes similar to prod_trail_001"}
],
tools=TOOLS, # ← includes "context" parameter in get_related_products
tool_choice={"type": "function", "function": {"name": "get_related_products"}},
)
args = json.loads(response_with_context_param.choices[0].message.tool_calls[0].function.arguments)
print("product_id:", args["product_id"])
print("context type:", type(args["context"]))
print("context length:", len(args["context"]))
print("context[0]:", args["context"][0])
# Output:
# product_id: prod_trail_001
# context type:
# context length: 52 ← full conversation history injected
# context[0]: {'role': 'user', 'content': 'show me running shoes'}
# ← NOT the product IDs the user viewed. The conversation history.
# Token cost for this function call:
# Injected context tokens: 52 messages × 80 tokens/msg = 4,160 extra tokens
# Expected cost without injection: ~180 tokens
# Actual cost: ~4,340 tokens ← 24x overrun
# Test 2: same schema but WITHOUT "context" parameter
TOOLS_NO_CONTEXT = [
# ... same as TOOLS but "context" renamed to "recent_product_ids" ...
]
response_no_context_param = client.chat.completions.create(
model="accounts/fireworks/models/firefunction-v2",
messages=long_conversation + [
{"role": "user", "content": "find shoes similar to prod_trail_001"}
],
tools=TOOLS_NO_CONTEXT,
tool_choice={"type": "function", "function": {"name": "get_related_products"}},
)
args2 = json.loads(response_no_context_param.choices[0].message.tool_calls[0].function.arguments)
print("product_id:", args2["product_id"])
print("recent_product_ids:", args2.get("recent_product_ids"))
# Output:
# product_id: prod_trail_001
# recent_product_ids: None ← model doesn't auto-populate without injection trigger
# (pass explicitly or remove the parameter entirely)
Why the overrun went undetected for 8 weeks
The cost overrun was not obvious for three reasons. First, the per-request cost increase was small in absolute terms: each injected context call cost approximately $0.0021 extra (4,160 tokens at FireFunction-v2 pricing), spread across 200 daily calls. The total monthly overrun was roughly $12 — large as a multiplier (25×) but below the threshold that would trigger an automatic billing alert. Second, the function call succeeded and returned a result. The TypeError from iterating message dicts instead of product ID strings was caught by a generic except Exception handler that fell back to top-5 popular products. The team saw the fallback behavior as a recommendation quality issue, not as a code error. Third, there was no per-function token tracking in the observability setup. The team tracked total daily token usage, not the breakdown by function call. The overrun was buried in the daily total.
The engagement diagnosis started by logging the raw function_call.arguments JSON for all get_related_products calls. The first logged payload was 6.2 KB. Expected size for 3 product IDs and a limit parameter: under 100 bytes.
The fix: remove “context” from the function parameter schema
# recommender.py — FIXED
# Eastbrook AI Ltd.
# fireworks-ai 0.15.0, Python 3.11
import json
from fireworks.client import Fireworks
client = Fireworks(api_key="FIREWORKS_API_KEY")
TOOLS = [
{
"type": "function",
"function": {
"name": "get_product_details",
"description": "Get the full details of a product by its ID.",
"parameters": {
"type": "object",
"properties": {
"product_id": {"type": "string"},
},
"required": ["product_id"],
},
},
},
{
"type": "function",
"function": {
"name": "get_related_products",
"description": "Get products related to a given product.",
"parameters": {
"type": "object",
"properties": {
"product_id": {"type": "string"},
# ← FIX: "context" parameter REMOVED entirely.
# FireFunction-v2 only injects conversation history
# when a parameter is named exactly "context".
# Removing this parameter stops the injection.
# The recommendation engine receives product_id and limit;
# recent_product_ids is passed explicitly by the application
# layer from session state, not via function call arguments.
"limit": {"type": "integer", "default": 5},
},
"required": ["product_id"],
},
},
},
# ... check_inventory and get_user_history unchanged ...
]
def get_recommendations(
messages: list,
product_id: str,
session_product_ids: list[str], # ← application passes session context explicitly
) -> list[str]:
response = client.chat.completions.create(
model="accounts/fireworks/models/firefunction-v2",
messages=messages,
tools=TOOLS,
tool_choice="auto",
)
msg = response.choices[0].message
if msg.tool_calls:
call = msg.tool_calls[0]
if call.function.name == "get_related_products":
args = json.loads(call.function.arguments)
# args now contains only: {"product_id": "...", "limit": 5}
# No "context" key. No conversation history injection.
related = recommend_engine.get_related(
product_id=args["product_id"],
context_products=session_product_ids, # ← passed from application layer
limit=args.get("limit", 5),
)
return related
return []
Adding argument size monitoring to catch future anomalies
# Per-request argument size monitoring
# fireworks-ai 0.15.0, Python 3.11
import json
import logging
logger = logging.getLogger(__name__)
MAX_ARGS_BYTES = 2_000 # alert threshold for function_call.arguments
def dispatch_tool_call(call) -> dict:
raw_args = call.function.arguments
args_size = len(raw_args.encode("utf-8"))
if args_size > MAX_ARGS_BYTES:
logger.warning(
"Large function_call.arguments: function=%s size=%d bytes",
call.function.name,
args_size,
)
# Alert: check for FireFunction-v2 context injection.
# If any function schema has a "context" parameter, this is
# likely the conversation history being injected.
args = json.loads(raw_args)
# Schema validation: assert no conversation history in args
for key, value in args.items():
if isinstance(value, list) and len(value) > 10:
if value and isinstance(value[0], dict) and "role" in value[0]:
logger.error(
"FireFunction-v2 context injection detected: "
"function=%s parameter=%s length=%d",
call.function.name, key, len(value),
)
# This indicates a "context" parameter is still in the schema.
raise ValueError(
f"Unexpected conversation history in {key}. "
f"Remove 'context' from the function parameter schema."
)
return args
Validation
# Validation benchmark (October 29 2026)
# 100 get_related_products calls from October 28 production sessions
# fireworks-ai 0.15.0, Python 3.11, accounts/fireworks/models/firefunction-v2
# Before fix ("context" parameter in get_related_products schema):
# function_call.arguments size: p50=6,200 bytes p95=9,800 bytes
# args["context"] type: list[dict] in 100% of calls (message history injected)
# args["context"] length: p50=52 p95=87 messages
# TypeError on downstream iterate: 100% (message dicts, not product ID strings)
# Fallback to popular products: 100%
# Recommendation quality (CTR): 2.1% (vs 4.8% for non-context-injection calls)
# Per-call token cost (avg): 4,520 tokens (180 expected)
# Per-call cost overrun: 25.1×
# After fix ("context" parameter removed from schema):
# function_call.arguments size: p50=82 bytes p95=105 bytes ← expected
# args keys: {"product_id": "...", "limit": N} — no "context"
# TypeError on downstream: 0 calls
# Fallback rate: 0% (correct recommendations returned)
# Recommendation CTR: 4.9% (matches expected quality)
# Per-call token cost (avg): 175 tokens
# Cost overrun: 0×
# Cost impact (Eastbrook AI Ltd.):
# Daily savings: 200 calls × (4,520 - 175) tokens × $0.0005/1K tokens = $0.43/day
# 8-week accumulated overrun: 56 days × $0.43 = $24.08 (actual excess paid)
# Future monthly savings: ~$13/month
# 2.5h retainer engagement.
Related retainer posts
- AWS Bedrock developer on retainer —
tool_usestreaming partial JSON parsed beforeContentBlockStopEvent—JSONDecodeErrorin 15% of tool-use responses — boto3 1.34.140 - Together AI developer on retainer —
response_formatJSON schema silently ignored for Mixtral-8x7B — Pydantic field name mismatch — 23% extraction failures for 5 weeks — together 1.2.1 - Anthropic developer on retainer — parallel tool calls return only first tool result — second
tool_useblock overlooked — 28% of complex queries 4 turns — anthropic 0.28.0
Track retainer hours without a client portal
HourTab turns your time-tracker CSV into a public URL your client bookmarks. No login. No portal. CSV in, URL out.
See pricing →