Skip to content

Commit 23dae56

Browse files
authored
fix(ai): bind prompt reads to project token (#433)
* fix(ai): bind prompt reads to project token * chore(release): bump version to 7.8.7
1 parent 73bec04 commit 23dae56

File tree

4 files changed

+67
-19
lines changed

4 files changed

+67
-19
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
# 7.9.1 - 2026-02-17
2+
3+
fix(llma): make prompt fetches deterministic by requiring project_api_key and sending it as token query param
4+
15
# 7.9.0 - 2026-02-17
26

37
feat: Support device_id as bucketing identifier for local evaluation

posthog/ai/prompts.py

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,11 @@ class Prompts:
5454
prompts = Prompts(posthog)
5555
5656
# Or with direct options (no PostHog client needed)
57-
prompts = Prompts(personal_api_key='phx_xxx', host='https://us.posthog.com')
57+
prompts = Prompts(
58+
personal_api_key='phx_xxx',
59+
project_api_key='phc_xxx',
60+
host='https://us.posthog.com',
61+
)
5862
5963
# Fetch with caching and fallback
6064
template = prompts.get('support-system-prompt', fallback='You are a helpful assistant.')
@@ -72,6 +76,7 @@ def __init__(
7276
posthog: Optional[Any] = None,
7377
*,
7478
personal_api_key: Optional[str] = None,
79+
project_api_key: Optional[str] = None,
7580
host: Optional[str] = None,
7681
default_cache_ttl_seconds: Optional[int] = None,
7782
):
@@ -80,7 +85,8 @@ def __init__(
8085
8186
Args:
8287
posthog: PostHog client instance (optional if personal_api_key provided)
83-
personal_api_key: Direct API key (optional if posthog provided)
88+
personal_api_key: Direct personal API key (optional if posthog provided)
89+
project_api_key: Direct project API key (optional if posthog provided)
8490
host: PostHog host (defaults to app endpoint)
8591
default_cache_ttl_seconds: Default cache TTL (defaults to 300)
8692
"""
@@ -91,11 +97,13 @@ def __init__(
9197

9298
if posthog is not None:
9399
self._personal_api_key = getattr(posthog, "personal_api_key", None) or ""
100+
self._project_api_key = getattr(posthog, "api_key", None) or ""
94101
self._host = remove_trailing_slash(
95102
getattr(posthog, "raw_host", None) or APP_ENDPOINT
96103
)
97104
else:
98105
self._personal_api_key = personal_api_key or ""
106+
self._project_api_key = project_api_key or ""
99107
self._host = remove_trailing_slash(host or APP_ENDPOINT)
100108

101109
def get(
@@ -215,7 +223,7 @@ def _fetch_prompt_from_api(self, name: str) -> str:
215223
"""
216224
Fetch prompt from PostHog API.
217225
218-
Endpoint: {host}/api/environments/@current/llm_prompts/name/{encoded_name}/
226+
Endpoint: {host}/api/environments/@current/llm_prompts/name/{encoded_name}/?token={encoded_project_api_key}
219227
Auth: Bearer {personal_api_key}
220228
221229
Args:
@@ -232,9 +240,15 @@ def _fetch_prompt_from_api(self, name: str) -> str:
232240
"[PostHog Prompts] personal_api_key is required to fetch prompts. "
233241
"Please provide it when initializing the Prompts instance."
234242
)
243+
if not self._project_api_key:
244+
raise Exception(
245+
"[PostHog Prompts] project_api_key is required to fetch prompts. "
246+
"Please provide it when initializing the Prompts instance."
247+
)
235248

236249
encoded_name = urllib.parse.quote(name, safe="")
237-
url = f"{self._host}/api/environments/@current/llm_prompts/name/{encoded_name}/"
250+
encoded_project_api_key = urllib.parse.quote(self._project_api_key, safe="")
251+
url = f"{self._host}/api/environments/@current/llm_prompts/name/{encoded_name}/?token={encoded_project_api_key}"
238252

239253
headers = {
240254
"Authorization": f"Bearer {self._personal_api_key}",

posthog/test/ai/test_prompts.py

Lines changed: 44 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,15 @@ class TestPrompts(unittest.TestCase):
3333
}
3434

3535
def create_mock_posthog(
36-
self, personal_api_key="phx_test_key", host="https://us.posthog.com"
36+
self,
37+
personal_api_key="phx_test_key",
38+
project_api_key="phc_test_key",
39+
host="https://us.posthog.com",
3740
):
3841
"""Create a mock PostHog client."""
3942
mock = MagicMock()
4043
mock.personal_api_key = personal_api_key
44+
mock.api_key = project_api_key
4145
mock.raw_host = host
4246
return mock
4347

@@ -61,7 +65,7 @@ def test_successfully_fetch_a_prompt(self, mock_get_session):
6165
call_args = mock_get.call_args
6266
self.assertEqual(
6367
call_args[0][0],
64-
"https://us.posthog.com/api/environments/@current/llm_prompts/name/test-prompt/",
68+
"https://us.posthog.com/api/environments/@current/llm_prompts/name/test-prompt/?token=phc_test_key",
6569
)
6670
self.assertIn("Authorization", call_args[1]["headers"])
6771
self.assertEqual(
@@ -235,6 +239,18 @@ def test_throw_when_no_personal_api_key_configured(self):
235239
"personal_api_key is required to fetch prompts", str(context.exception)
236240
)
237241

242+
def test_throw_when_no_project_api_key_configured(self):
243+
"""Should throw when no project_api_key is configured."""
244+
posthog = self.create_mock_posthog(project_api_key=None)
245+
prompts = Prompts(posthog)
246+
247+
with self.assertRaises(Exception) as context:
248+
prompts.get("test-prompt")
249+
250+
self.assertIn(
251+
"project_api_key is required to fetch prompts", str(context.exception)
252+
)
253+
238254
@patch("posthog.ai.prompts._get_session")
239255
def test_throw_when_api_returns_invalid_response_format(self, mock_get_session):
240256
"""Should throw when API returns invalid response format."""
@@ -255,15 +271,17 @@ def test_use_custom_host_from_posthog_options(self, mock_get_session):
255271
mock_get = mock_get_session.return_value.get
256272
mock_get.return_value = MockResponse(json_data=self.mock_prompt_response)
257273

258-
posthog = self.create_mock_posthog(host="https://eu.i.posthog.com")
274+
posthog = self.create_mock_posthog(host="https://eu.posthog.com")
259275
prompts = Prompts(posthog)
260276

261277
prompts.get("test-prompt")
262278

263279
call_args = mock_get.call_args
264280
self.assertTrue(
265-
call_args[0][0].startswith("https://eu.i.posthog.com/"),
266-
f"Expected URL to start with 'https://eu.i.posthog.com/', got {call_args[0][0]}",
281+
call_args[0][0].startswith(
282+
"https://eu.posthog.com/api/environments/@current/llm_prompts/name/test-prompt/?token=phc_test_key"
283+
),
284+
f"Expected URL to start with 'https://eu.posthog.com/api/environments/@current/llm_prompts/name/test-prompt/?token=phc_test_key', got {call_args[0][0]}",
267285
)
268286

269287
@patch("posthog.ai.prompts._get_session")
@@ -333,7 +351,7 @@ def test_url_encode_prompt_names_with_special_characters(self, mock_get_session)
333351
call_args = mock_get.call_args
334352
self.assertEqual(
335353
call_args[0][0],
336-
"https://us.posthog.com/api/environments/@current/llm_prompts/name/prompt%20with%20spaces%2Fand%2Fslashes/",
354+
"https://us.posthog.com/api/environments/@current/llm_prompts/name/prompt%20with%20spaces%2Fand%2Fslashes/?token=phc_test_key",
337355
)
338356

339357
@patch("posthog.ai.prompts._get_session")
@@ -342,15 +360,17 @@ def test_work_with_direct_options_no_posthog_client(self, mock_get_session):
342360
mock_get = mock_get_session.return_value.get
343361
mock_get.return_value = MockResponse(json_data=self.mock_prompt_response)
344362

345-
prompts = Prompts(personal_api_key="phx_direct_key")
363+
prompts = Prompts(
364+
personal_api_key="phx_direct_key", project_api_key="phc_direct_key"
365+
)
346366

347367
result = prompts.get("test-prompt")
348368

349369
self.assertEqual(result, self.mock_prompt_response["prompt"])
350370
call_args = mock_get.call_args
351371
self.assertEqual(
352372
call_args[0][0],
353-
"https://us.posthog.com/api/environments/@current/llm_prompts/name/test-prompt/",
373+
"https://us.posthog.com/api/environments/@current/llm_prompts/name/test-prompt/?token=phc_direct_key",
354374
)
355375
self.assertEqual(
356376
call_args[1]["headers"]["Authorization"], "Bearer phx_direct_key"
@@ -363,15 +383,17 @@ def test_use_custom_host_from_direct_options(self, mock_get_session):
363383
mock_get.return_value = MockResponse(json_data=self.mock_prompt_response)
364384

365385
prompts = Prompts(
366-
personal_api_key="phx_direct_key", host="https://eu.posthog.com"
386+
personal_api_key="phx_direct_key",
387+
project_api_key="phc_direct_key",
388+
host="https://eu.posthog.com",
367389
)
368390

369391
prompts.get("test-prompt")
370392

371393
call_args = mock_get.call_args
372394
self.assertEqual(
373395
call_args[0][0],
374-
"https://eu.posthog.com/api/environments/@current/llm_prompts/name/test-prompt/",
396+
"https://eu.posthog.com/api/environments/@current/llm_prompts/name/test-prompt/?token=phc_direct_key",
375397
)
376398

377399
@patch("posthog.ai.prompts._get_session")
@@ -385,7 +407,9 @@ def test_use_custom_default_cache_ttl_from_direct_options(
385407
mock_time.return_value = 1000.0
386408

387409
prompts = Prompts(
388-
personal_api_key="phx_direct_key", default_cache_ttl_seconds=60
410+
personal_api_key="phx_direct_key",
411+
project_api_key="phc_direct_key",
412+
default_cache_ttl_seconds=60,
389413
)
390414

391415
# First call
@@ -486,23 +510,29 @@ def test_handle_multiple_occurrences_of_same_variable(self):
486510

487511
def test_work_with_direct_options_initialization(self):
488512
"""Should work with direct options initialization."""
489-
prompts = Prompts(personal_api_key="phx_test_key")
513+
prompts = Prompts(
514+
personal_api_key="phx_test_key", project_api_key="phc_test_key"
515+
)
490516

491517
result = prompts.compile("Hello, {{name}}!", {"name": "World"})
492518

493519
self.assertEqual(result, "Hello, World!")
494520

495521
def test_handle_variables_with_hyphens(self):
496522
"""Should handle variables with hyphens."""
497-
prompts = Prompts(personal_api_key="phx_test_key")
523+
prompts = Prompts(
524+
personal_api_key="phx_test_key", project_api_key="phc_test_key"
525+
)
498526

499527
result = prompts.compile("User ID: {{user-id}}", {"user-id": "12345"})
500528

501529
self.assertEqual(result, "User ID: 12345")
502530

503531
def test_handle_variables_with_dots(self):
504532
"""Should handle variables with dots."""
505-
prompts = Prompts(personal_api_key="phx_test_key")
533+
prompts = Prompts(
534+
personal_api_key="phx_test_key", project_api_key="phc_test_key"
535+
)
506536

507537
result = prompts.compile("Company: {{company.name}}", {"company.name": "Acme"})
508538

posthog/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
VERSION = "7.9.0"
1+
VERSION = "7.9.1"
22

33
if __name__ == "__main__":
44
print(VERSION, end="") # noqa: T201

0 commit comments

Comments
 (0)