Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/marsys/models/adapters/openai.py
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,9 @@ def convert_content_types(content):
"effort": self._served_effort(reasoning_effort.lower(), model_lower)
}

if kwargs.get("prompt_cache_key") is not None:
payload["prompt_cache_key"] = kwargs["prompt_cache_key"]

# Only accept known OpenAI Responses API parameters - warn about unknown ones
# Based on: https://platform.openai.com/docs/api-reference/responses/create
valid_openai_params = {
Expand Down
5 changes: 3 additions & 2 deletions src/marsys/models/adapters/openai_oauth.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
import logging
import os
import time
import uuid
from pathlib import Path
from typing import Any, Dict, List, Optional, Tuple

Expand Down Expand Up @@ -361,9 +360,11 @@ def format_request_payload(
"store": False,
"stream": True, # REQUIRED for ChatGPT backend
"include": ["reasoning.encrypted_content"],
"prompt_cache_key": str(uuid.uuid4()),
}

if kwargs.get("prompt_cache_key") is not None:
payload["prompt_cache_key"] = kwargs["prompt_cache_key"]

# Handle structured output — text.format (Responses API). Strict mode here
# demands BOTH additionalProperties:false AND required==every property; a
# Pydantic schema satisfies neither out of the box, so compose both schema
Expand Down
44 changes: 44 additions & 0 deletions tests/models/test_prompt_cache_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""Caller-selected cache routing survives each OpenAI-family payload builder."""

import pytest

from marsys.models.adapters.azure import AsyncAzureOpenAIAdapter, AzureOpenAIAdapter
from marsys.models.adapters.openai import AsyncOpenAIAdapter, OpenAIAdapter
from marsys.models.adapters.openai_oauth import AsyncOpenAIOAuthAdapter, OpenAIOAuthAdapter


@pytest.fixture(params=[
OpenAIAdapter, AsyncOpenAIAdapter, AzureOpenAIAdapter, AsyncAzureOpenAIAdapter,
OpenAIOAuthAdapter, AsyncOpenAIOAuthAdapter,
])
def adapter(request):
adapter_type = request.param
if issubclass(adapter_type, OpenAIOAuthAdapter):
# Payload construction needs no credential discovery or network client.
result = object.__new__(adapter_type)
result.model_name = "gpt-5"
return result
return adapter_type(
model_name="gpt-5", api_key="not-a-real-key",
base_url="https://example.invalid/openai/v1", max_tokens=1024,
)


def test_caller_key_reaches_payload_and_stays_stable(adapter):
messages = [{"role": "user", "content": "Continue the work."}]
key = "installation:instance"
first = adapter.format_request_payload(messages, prompt_cache_key=key)
second = adapter.format_request_payload(messages, prompt_cache_key=key)
assert first["prompt_cache_key"] == second["prompt_cache_key"] == key
other = adapter.format_request_payload(messages, prompt_cache_key="installation:other")
assert other["prompt_cache_key"] == "installation:other"


@pytest.mark.parametrize("kwargs", [{}, {"prompt_cache_key": None}])
def test_absent_key_is_omitted_without_minting_one(adapter, kwargs):
messages = [{"role": "user", "content": "Continue the work."}]
first = adapter.format_request_payload(messages, **kwargs)
second = adapter.format_request_payload(messages, **kwargs)
assert "prompt_cache_key" not in first
assert "prompt_cache_key" not in second
assert first == second
Loading