From 50862275f52ecd4841e8b342c79d4b6f13032628 Mon Sep 17 00:00:00 2001 From: Kevin Burke Date: Wed, 1 Oct 2025 14:53:05 -0700 Subject: [PATCH] llm_openai: use previous_response_id if available Also add test cases. Fixes #11 --- llm_openai.py | 151 ++++++++----- ...est_chained_response_stored_correctly.yaml | 212 +++++++++++++++++ .../test_conversation_chaining.yaml | 213 ++++++++++++++++++ ...onversation_chaining_with_store_false.yaml | 213 ++++++++++++++++++ tests/test_openai.py | 74 ++++++ 5 files changed, 810 insertions(+), 53 deletions(-) create mode 100644 tests/cassettes/test_openai/test_chained_response_stored_correctly.yaml create mode 100644 tests/cassettes/test_openai/test_conversation_chaining.yaml create mode 100644 tests/cassettes/test_openai/test_conversation_chaining_with_store_false.yaml diff --git a/llm_openai.py b/llm_openai.py index 29c8e1f..1c6ddbb 100644 --- a/llm_openai.py +++ b/llm_openai.py @@ -193,6 +193,49 @@ def set_usage(self, response, usage): input=input_tokens, output=output_tokens, details=simplify_usage_dict(usage) ) + def _build_prompt_messages(self, prompt, image_detail, prev_system=None): + """ + Build messages for a single prompt, including system, user message, and tool results. + + Args: + prompt: The prompt to build messages for + image_detail: Image detail setting for attachments + prev_system: Previous system prompt (to avoid duplicates) + + Returns: + List of messages to append + """ + messages = [] + + # Add system prompt if present and different from previous + if prompt.system and prompt.system != prev_system: + messages.append({"role": "system", "content": prompt.system}) + + # Add user message (with or without attachments) + if not prompt.attachments: + messages.append({"role": "user", "content": prompt.prompt or ""}) + else: + attachment_message = [] + if prompt.prompt: + attachment_message.append({"type": "input_text", "text": prompt.prompt}) + for attachment in prompt.attachments: + attachment_message.append(_attachment(attachment, image_detail)) + messages.append({"role": "user", "content": attachment_message}) + + # Add tool results if present + for tool_result in getattr(prompt, "tool_results", []): + if not tool_result.tool_call_id: + continue + messages.append( + { + "type": "function_call_output", + "call_id": tool_result.tool_call_id, + "output": tool_result.output, + } + ) + + return messages + def _build_messages(self, prompt, conversation): messages = [] current_system = None @@ -201,37 +244,16 @@ def _build_messages(self, prompt, conversation): image_detail = prompt.options.image_detail or "low" if conversation is not None: for prev_response in conversation.responses: - if ( - prev_response.prompt.system - and prev_response.prompt.system != current_system - ): - messages.append( - {"role": "system", "content": prev_response.prompt.system} - ) + # Build messages for the previous prompt + prev_messages = self._build_prompt_messages( + prev_response.prompt, image_detail, current_system + ) + messages.extend(prev_messages) + # Update current_system if it changed + if prev_response.prompt.system: current_system = prev_response.prompt.system - if prev_response.attachments: - attachment_message = [] - if prev_response.prompt.prompt: - attachment_message.append( - {"type": "input_text", "text": prev_response.prompt.prompt} - ) - for attachment in prev_response.attachments: - attachment_message.append(_attachment(attachment, image_detail)) - messages.append({"role": "user", "content": attachment_message}) - else: - messages.append( - {"role": "user", "content": prev_response.prompt.prompt} - ) - for tool_result in getattr(prev_response.prompt, "tool_results", []): - if not tool_result.tool_call_id: - continue - messages.append( - { - "type": "function_call_output", - "call_id": tool_result.tool_call_id, - "output": tool_result.output, - } - ) + + # Add assistant response prev_text = prev_response.text_or_raise() if prev_text: messages.append({"role": "assistant", "content": prev_text}) @@ -246,32 +268,55 @@ def _build_messages(self, prompt, conversation): "arguments": json.dumps(tool_call.arguments), } ) - if prompt.system and prompt.system != current_system: - messages.append({"role": "system", "content": prompt.system}) - if not prompt.attachments: - messages.append({"role": "user", "content": prompt.prompt or ""}) - else: - attachment_message = [] - if prompt.prompt: - attachment_message.append({"type": "input_text", "text": prompt.prompt}) - for attachment in prompt.attachments: - attachment_message.append(_attachment(attachment, image_detail)) - messages.append({"role": "user", "content": attachment_message}) - for tool_result in getattr(prompt, "tool_results", []): - if not tool_result.tool_call_id: - continue - messages.append( - { - "type": "function_call_output", - "call_id": tool_result.tool_call_id, - "output": tool_result.output, - } - ) + + # Build messages for the current prompt + current_messages = self._build_prompt_messages(prompt, image_detail, current_system) + messages.extend(current_messages) + return messages def _build_kwargs(self, prompt, conversation): - messages = self._build_messages(prompt, conversation) - kwargs = {"model": self.model_name, "input": messages} + kwargs = {"model": self.model_name} + + # Determine if we should use chaining via previous_response_id + store_option = getattr(prompt.options, "store", None) + use_chaining = ( + store_option is not False # store is True or None (default) + and conversation is not None + and len(conversation.responses) > 0 + ) + + if use_chaining: + # Try to extract previous_response_id from last response + last_response = conversation.responses[-1] + previous_response_id = None + if hasattr(last_response, 'response_json') and last_response.response_json: + previous_response_id = last_response.response_json.get('id') + + if previous_response_id: + # Use chaining: only send current message + previous_response_id + kwargs["previous_response_id"] = previous_response_id + + # Build messages for just the current prompt + image_detail = None + if self.vision: + image_detail = prompt.options.image_detail or "low" + + # Check if system changed from last response + last_system = getattr(last_response.prompt, 'system', None) + current_messages = self._build_prompt_messages(prompt, image_detail, last_system) + + kwargs["input"] = current_messages + else: + # Couldn't find previous_response_id, fall back to full history + use_chaining = False + + if not use_chaining: + # Fall back to sending full conversation history + messages = self._build_messages(prompt, conversation) + kwargs["input"] = messages + + # Add other options for option in ( "max_output_tokens", "temperature", diff --git a/tests/cassettes/test_openai/test_chained_response_stored_correctly.yaml b/tests/cassettes/test_openai/test_chained_response_stored_correctly.yaml new file mode 100644 index 0000000..1d09a12 --- /dev/null +++ b/tests/cassettes/test_openai/test_chained_response_stored_correctly.yaml @@ -0,0 +1,212 @@ +interactions: +- request: + body: '{"input":[{"role":"user","content":"Say ''first''"}],"model":"gpt-4o-mini","stream":false}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '88' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - OpenAI/Python 2.0.0 + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 2.0.0 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - PyPy + x-stainless-runtime-version: + - 3.9.15 + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: !!binary | + H4sIAAAAAAAAA3xT0W6jMBB8z1cgPzcVkLSBfMD9RHVCi72kvhivZa+jRlX+/YQJBK7tvSU7u8Ps + zPpzk2VCK3HMhMfgmrzN605V5QGkwq4+5PlrpWSrdmrfHfKqqHfVvsWXbldAW+3LXVmJp4GC2j8o + eaIhG3CsS4/AqBoYsOLwUpf1a1XWCQsMHMMwI6l3BhnVONSCPJ88RTvo6sAEHMvaGG1P4ph9brIs + y4SDK/phXuEFDTn0YpNlt9SM3tOA2WhMKmg7faVRyKBNWKOBfZSsya7qPXw0FNlFbpjO+BVkItNI + MGu6nhSaQdnJ8XZP215bvS3zcr/ND9ti8izximP2ltYZl5rj6MPpP2nArq1SGrnqykIVBSqVQzca + mFj46jDxYAhwwgfwk+0JlGQZ7UPUUtiKdjIFP3ieTg1gLTFMRr79XoGGTs5T+w2SiI6Z+KV94Gcx + Q7f7r7lbeDJJAYSgA4PlsXloTE3CgQdj0KyzYR/HM3IeL5piaKZLbZLhc3bOU++4kSDfsTnjdYl5 + hEB2dYTYdeR50TRYHPse/DQ532SADvnaaIWWdadxdZ8B/UVLbFhPN91BNKO5IjB5XC7B2Dv0wDGV + i+f8Xk0m3pV15Ht4/F+El/pG1+6KL+hbCpqv48koHfvHWxp9fCctR+Mjk5iBR5aCyTWLhPO56JYa + fbQy3UfaUgdozfTwY7rUeQFtV++uKJ6+1hePeV4zRaceg/lq1X+f8+67+ne0c/g/MTMxmIXe/exg + DOuwe2RQwDDQ3za3vwAAAP//AwDGEMxNhgUAAA== + headers: + CF-RAY: + - 9879965a4dae67dd-SJC + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Wed, 01 Oct 2025 05:33:49 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=4JxMZpfw0H4_p0saYiQi8t2riBZk2TuG15LO0nK9Foo-1759296829-1.0.1.1-U67o0QZBx8iWIPWRb5PQasGZwtd87EP7xFeWzfaLwgzAJS_W8DX1rjlr2J9ePGaniktPDDOUogcGH9STxBHk8DgebVkEyBKvA8vRbc.5b.4; + path=/; expires=Wed, 01-Oct-25 06:03:49 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=__cbHl_oB.bNBTKC5VxDgHli51yafpffPsyT5PfCIoo-1759296829801-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-processing-ms: + - '471' + openai-project: + - proj_HFFKG5Gf9Vuh44Vyp11iM5lW + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + x-envoy-upstream-service-time: + - '473' + x-ratelimit-limit-requests: + - '30000' + x-ratelimit-limit-tokens: + - '150000000' + x-ratelimit-remaining-requests: + - '29999' + x-ratelimit-remaining-tokens: + - '149999970' + x-ratelimit-reset-requests: + - 2ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_42578d0dcd0fd06bdddedc9361705001 + status: + code: 200 + message: OK +- request: + body: '{"input":[{"role":"user","content":"Say ''second''"}],"model":"gpt-4o-mini","previous_response_id":"resp_0b09fd827acdef970068dcbd3d4f70819384be5f31ab842328","stream":false}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '170' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - OpenAI/Python 2.0.0 + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 2.0.0 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - PyPy + x-stainless-runtime-version: + - 3.9.15 + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: !!binary | + H4sIAAAAAAAAA4xUy27jMAy85ysMnZvCj6Rx8ht7LBYGLdGptrIoSFTQoMi/LyzHjr3tLvaWcMgx + yRnqc5NlQitxyoTH4Jq8zY+dqssDSIXd8ZDnL7WSraowx2JXF8eqhVq2uC937UEWrVTiaaCg9hdK + nmjIBhzj0iMwqgYGrDjsj+Xxpa7yhAUGjmGokdQ7g4x3shbk+9lTtENfHZiAY1gbo+1ZnLLPTZZl + mXBwRT/UK7ygIYdebLLslpLRexowG41JAW2nrzQKGbQJazSwj5I12VW8h4+GIrvIDdM7fgWZyDQS + zJquJ4Vm6OzseLujba+t3pZ5udvmh21R33eWeMUpe03jjEPNcvTh/A81DlUuBzUAcyhQyiPs1Uub + l4k5sfDVYeLBEOCMD+Bva0+gJMtoH00tG1vRTkvBD56rUwJYSwzTIl9/rkBDZ+ep/QZJRKdM/EBJ + Vj2LGbvdf83pwpNJLUAIOjBYHpOHxJQkHHgwBs1aHPZx9JHzeNEUQzNZtfmfA1C77pAPK693Le67 + qoC23pVVeRfTeeodNxLkGzbveF26wSMEsivrYteR50XSIEzse/BT5ezkAB3ytdEKLetO48rVAf1F + S2xYT5fQQTSjJCIweVxOztg79MAxhYvn/B5Nq7931pHv4fF/IXnKG1d97/iCvqWg+ToaTenYPy5w + XP4baTmqFZnEDDwcIJhcs/BFPgfdskcfrUyuSlPqAK2ZnouY/D0PoO3qWsv909f44gmYx0zSqUdh + vhr1z0eg+i7+He0s/t+YmRjMot963mAMa7F7ZFDAMNDfNrffAAAA//8DAAolxM+8BQAA + headers: + CF-RAY: + - 987996637aa26804-SJC + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Wed, 01 Oct 2025 05:33:50 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=xYwi4LHJFiIjlzTHwNoKMPlSCba78nKZEzxJfLoSbfg-1759296830-1.0.1.1-qFcMaqXZdKVmY30gNcp47gY7Z0w5_HPv91c08KUiyjaCbsHLzQFMp36W7wTUxbNCb016dIQpxeLaEr56Ntfe8cWQCqUZXOOkMb4KXN_5dvo; + path=/; expires=Wed, 01-Oct-25 06:03:50 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=3weMslqFEM8QPGSWaBAfEGTLLMIekLw3C6iDfFCFqPo-1759296830835-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-processing-ms: + - '586' + openai-project: + - proj_HFFKG5Gf9Vuh44Vyp11iM5lW + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + x-envoy-upstream-service-time: + - '592' + x-ratelimit-limit-requests: + - '30000' + x-ratelimit-limit-tokens: + - '150000000' + x-ratelimit-remaining-requests: + - '29999' + x-ratelimit-remaining-tokens: + - '149999957' + x-ratelimit-reset-requests: + - 2ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_b257d1b800ffec5d194336699d5d1c85 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/test_openai/test_conversation_chaining.yaml b/tests/cassettes/test_openai/test_conversation_chaining.yaml new file mode 100644 index 0000000..e0f523d --- /dev/null +++ b/tests/cassettes/test_openai/test_conversation_chaining.yaml @@ -0,0 +1,213 @@ +interactions: +- request: + body: '{"input":[{"role":"user","content":"What is 7+7?"}],"model":"gpt-4o-mini","stream":false}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '89' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - OpenAI/Python 2.0.0 + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 2.0.0 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - PyPy + x-stainless-runtime-version: + - 3.9.15 + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: !!binary | + H4sIAAAAAAAAA3xU0W7bMAx871cIel0z2E7ixPmVYjBomU61yqImUUGDIv8+WI4de233lvDIC3l3 + yseTEFK38iSkx+DqbKuKfa6OLWTd/lh2WVYeW9Uo7HZVuTvmVddkVXksyl1ebRsAOMjngYKa36h4 + oiEbcKwrj8DY1jBg+WFfFVV52OcJCwwcwzCjqHcGGdtxqAH1dvYU7bBXBybgWNbGaHuWJ/HxJIQQ + 0sEV/TDf4gUNOfTySYhbakbvacBsNCYVtJ1+pW6RQZuwRgP7qFiTXdV7eK8psotcM73hZ5CJTK3A + rOl6atEMm50db3a06bXVmyIrdpvssMmPd80SrzyJl3TOeNRsRx/O/3FDlXs1uAFYbVVZqbwoDqVq + R9UTC18dJh4MAc4L4DvZE6jIMtrHUsvFVrSTKPjO83RqAGuJYRLy5dcKNHR2npovkER0EvIgfoiD + wD8RTBD57qecm273T/Oc9GTSLhCCDgyWx+ahMTVJBx6MQbN2iX0cA+U8XjTFUE+ZrZP0s4vOU++4 + VqBesX7D6xLzCIHsKo7YdeR50TSIHfse/DQ5pzNAh3ytdYuWdadxldSA/qIV1qyndHcQzSizDEwe + l0cw9g49cEzl/Gd2ryY575t15Ht4fF/YmPpG1e4bX9A3FDRfx/C0OvaPVzXq+EpajcJHJjkDD1cl + k6sXXmdz0S139NGqlJR0pQ7QmOkvIKbMzgdou3qB+e75c33xrOczk3XtYzBbnfrvw66+qn9FO5v/ + HTMTg3mAxXZWMIa12T0ytMAw0N+ebn8BAAD//wMAJRy4IZAFAAA= + headers: + CF-RAY: + - 987994758da26899-SJC + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Wed, 01 Oct 2025 05:32:32 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=iNBuGFk1H.Eg_NSRcIYhxZWeYVoNOfw1WWPMKPTYPNg-1759296752-1.0.1.1-LTNdsYgBIwrQmpZzSOVHOHxJn_RveT3kH0qk9MEOGsm5b2OG_PE4r.wgKyrxrlTDjyDDX5WGfXPqgFApF4dIZLTxBPmOdG_D6vbryH3srxM; + path=/; expires=Wed, 01-Oct-25 06:02:32 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=pWqQDpeCVo85iejCibA.qArZH5B_YrDFfILVGHcuRHg-1759296752159-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-processing-ms: + - '843' + openai-project: + - proj_HFFKG5Gf9Vuh44Vyp11iM5lW + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + x-envoy-upstream-service-time: + - '845' + x-ratelimit-limit-requests: + - '30000' + x-ratelimit-limit-tokens: + - '150000000' + x-ratelimit-remaining-requests: + - '29999' + x-ratelimit-remaining-tokens: + - '149999967' + x-ratelimit-reset-requests: + - 2ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_ea910ee9603afa4187751aaed43ddb95 + status: + code: 200 + message: OK +- request: + body: '{"input":[{"role":"user","content":"Add 3 to that"}],"model":"gpt-4o-mini","previous_response_id":"resp_03c251c8da0f586f0068dcbcef4964819fb0968264193baaa7","stream":false}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '171' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - OpenAI/Python 2.0.0 + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 2.0.0 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - PyPy + x-stainless-runtime-version: + - 3.9.15 + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: !!binary | + H4sIAAAAAAAAA4xU227bMAx9z1cIel1T2I7j2PmVYjBoiU61ypInUUGDIv8+WI5vazvsLeEhj0me + Q33sGONK8jPjDn1fJweRHVNRSkjaY1m0SVKUUjSiTQpIRZlWLYg0yQ5VlqZ5emyqjD8NFLb5hYIm + Gms8jnHhEAhlDQOWno5VVhWnYxYxT0DBDzXCdr1GQjkWNSDeLs4GM/TVgvY4hpXWylz4mX3sGGOM + 93BDN9RLvKK2PTq+Y+wek9E5O2AmaB0DykxfqSUSKO23qCcXBClrNvEO3msbqA9Uk33DzyBZq2sB + ekvXWYl66OzS0z63+04Ztc+SLN8np31aPnYWefmZvcRxxqFmOTp/+YcaKE9lVOMkq7xJyvZQZBVI + iMyRhW49Rh70Hi64AN+tPYLCGkKzNLVubEM7LQXfaa6OCWCMJZgW+fJzA2p76Z1tvkAi0ZnxNGc/ + 2IHh7wDas/T0zOes++PXXMid1bEZ8F55AkNj8pAYk3gPDrRGvZWJXBgd1Tu8Kht8PZm2/o9TwDav + inxYfpNURZkVeVodGgA48Qep7XqqBYhXrN/wtvaFQ/DWbEyMbWsdrZIGiULXgZsqZ097aJFutZJo + SLUKN/726K5KYE1quokWgh7F4Z6sw/XkhF2PDijEcPqcPKJRhEdnrXUdLP9X4se8cdWPjq/oGusV + 3UbLSRW65RbH5b9aJUa1Alk+A4sXONm+XjkkmYP9ukcXjIj+ilMqD42eHo4QnT4PoMzmbg/Hp8/x + 1WMwjxmlk0thshn17+eg+ir+Fe0s/nfMZAn0Aub5vMHgt2J3SCCBYKC/7+5/AAAA//8DAPGKpg7G + BQAA + headers: + CF-RAY: + - 9879947e3ad4ebe9-SJC + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Wed, 01 Oct 2025 05:32:33 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=MIGz8u7wlesR6FUKtH33LsHRE0exNtWWLsW.hhhDWWY-1759296753-1.0.1.1-7.GYjcdqaVY8kjmQDhjBx...ik3VMfufE_hm7SkFkskaMVTBI5yk6ZWFa4Xi1HAAWuC81ILeiVK_4THqDHFg9uWEltq0f6S_G8fSezS1rbQ; + path=/; expires=Wed, 01-Oct-25 06:02:33 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=VrKMcHPBL0Yg2VTFr7VvfwONpv.3AldbKpIwyU40ffA-1759296753266-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-processing-ms: + - '825' + openai-project: + - proj_HFFKG5Gf9Vuh44Vyp11iM5lW + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + x-envoy-upstream-service-time: + - '827' + x-ratelimit-limit-requests: + - '30000' + x-ratelimit-limit-tokens: + - '150000000' + x-ratelimit-remaining-requests: + - '29999' + x-ratelimit-remaining-tokens: + - '149999945' + x-ratelimit-reset-requests: + - 2ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_7b7db55d8934fd50b379cdba561dff48 + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/test_openai/test_conversation_chaining_with_store_false.yaml b/tests/cassettes/test_openai/test_conversation_chaining_with_store_false.yaml new file mode 100644 index 0000000..f2999bd --- /dev/null +++ b/tests/cassettes/test_openai/test_conversation_chaining_with_store_false.yaml @@ -0,0 +1,213 @@ +interactions: +- request: + body: '{"input":[{"role":"user","content":"What is 8+8?"}],"model":"gpt-4o-mini","store":true,"stream":false}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '102' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - OpenAI/Python 2.0.0 + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 2.0.0 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - PyPy + x-stainless-runtime-version: + - 3.9.15 + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: !!binary | + H4sIAAAAAAAAA3RU0W7iMBB85yusvF6pkhRC4FeqU7SxN9RXx+uz16io4t9PcUhIru0b7OwOuzNj + PjdCZFplJ5F5DK7J1b6qW8T2WCIe9oc8r2olW9lJpYpdXUAJUB9eZNW+VHXdyl2bPQ0U1P5ByRMN + 2YBjXXoERtXAgBWH/bE8Vodql7DAwDEMM5J6Z5BRjUMtyPezp2iHvTowAceyNkbbc3YSnxshhMgc + XNEP8wovaMihzzZC3FIzek8DZqMxqaDt9CuNQgZtwhoN7KNkTXZV7+GjocgucsP0jl9BJjKNBLOm + 60mhGTY7O97uaNtrq7dlXu62+WFb1HfNEm92Eq/pnPGo2Y4+nH92Q+2Luh7caOuiqwBxMKPuujIx + Jxa+Okw8GAKc8QH8JHsCJVlG+1hqudiKdhIFP3ieTg1gLTFMQr7+XoGGzs5T+w2SiE4iq8UvUQv8 + G8EEUVTP2dx0u3+a5zJPJu0CIejAYHlsHhpTU+bAgzFo1i6xj2OgnMeLphiaKbNNkn520XnqHTcS + 5Bs273hdYh4hkF3FEbuOPC+aBrFj34OfJud0BuiQr41WaFl3GldJDegvWmLDekp3B9GMMmeByePy + CMbeoQeOqVw85/dqkvO+WUe+h8f3hY2pb1TtvvEFfUtB83UMj9Kxf7yqUcc30nIUPjJlM/BwNWNy + zcLrfC665Y4+WpmSkq7UAVoz/QXElNn5AG1XL7DYPX2tL571fGayTj0G89Wp/z/s43f172hn839i + ZmIwD7B8mRWMYW12jwwKGAb62+b2DwAA//8DAAYcRe2QBQAA + headers: + CF-RAY: + - 987994cbdfae17d8-SJC + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Wed, 01 Oct 2025 05:32:45 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=nuu67LS6Jite6V5_zUTUQBwUrsABiKHBvzvWR5ndTI8-1759296765-1.0.1.1-69u8cj9hOW5fVcfCWapkJLN9vAGdDMdJLb5l10fNemdECbWcmdNYSJ4cwenGSmb.FpoKVzvGlR9g8iqezSI8XcgJj15CJ25G9IREwDYI7vg; + path=/; expires=Wed, 01-Oct-25 06:02:45 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=KufQUrVyYGACjjO8H0S56YX.Owe3ODIQ7nz0t3Usi30-1759296765610-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-processing-ms: + - '724' + openai-project: + - proj_HFFKG5Gf9Vuh44Vyp11iM5lW + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + x-envoy-upstream-service-time: + - '729' + x-ratelimit-limit-requests: + - '30000' + x-ratelimit-limit-tokens: + - '150000000' + x-ratelimit-remaining-requests: + - '29999' + x-ratelimit-remaining-tokens: + - '149999967' + x-ratelimit-reset-requests: + - 2ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_5c341e7f8fa3e4da5a000f52a7bf7dfd + status: + code: 200 + message: OK +- request: + body: '{"input":[{"role":"user","content":"What is 8+8?"},{"role":"assistant","content":"8 + + 8 equals 16."},{"role":"user","content":"Subtract 5"}],"model":"gpt-4o-mini","store":false,"stream":false}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '192' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - OpenAI/Python 2.0.0 + x-stainless-arch: + - arm64 + x-stainless-async: + - 'false' + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 2.0.0 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - PyPy + x-stainless-runtime-version: + - 3.9.15 + method: POST + uri: https://api.openai.com/v1/responses + response: + body: + string: !!binary | + H4sIAAAAAAAAA3RU246jMAx971dEeZ6OgLb08iujFTKJ6WQnxNnEqaYa9d9XhELL7swb+NgH+xyb + r5UQ0mh5EjJg9E1R7nR5VPv9od4e6hqKsj5o1apO6+5wOJSwaatug1Add6pSRaWUfBkoqP2Niica + chHHuAoIjLqBASv3u2N1rPf1LmORgVMcahT13iKjHotaUB/nQMkNfXVgI45hY61xZ3kSXyshhJAe + rhiGeo0XtOQxyJUQt5yMIdCAuWRtDhg3faXRyGBsXKKRQ1JsyC3iPXw2lNgnbpg+8H+QiWyjwC7p + etJoh87OntdbWvfGmXVVVNt1sV+Xh7tmmVeexFseZxxqtqOP55/dwKrGYnDjqMtjB7tWb7Z73JVt + Zs4sfPWYeTBGOOMD+En2DCpyjO7R1HNjC9pJFPzkuTongHPEMAn59msBWjr7QO03SCY6CVnWYi12 + Av8ksFGU5aucs273p7lQBrK5GYjRRAbHY/KQmJOkhwDWol3axCGNG+UDXgyl2ExL22TtZxt9oN5z + o0C9Y/OB12csIERyi33ErqPAT0mD2qnvIUyV83pG6JCvjdHo2HQGF6saMVyMwobNtN4dJDvqLCNT + wMVdMPYeA3DK8fK1uEezoPfWOgo9PN6fjMx5o2z3li8YWoqGr+P6aJP6x12NQr6TUaPyiUnOwMNX + yeSbJ7eLOeifewzJqbwreUwTobXTTyDlrZ0HMG5xg5vNy//xp8Oex8ze6UdhsRj139M+fhf/jnZ2 + /ydmJgb7ALfVrGCKS7d7ZNDAMNDfVre/AAAA//8DAM0M0peSBQAA + headers: + CF-RAY: + - 987994d25da5faf0-SJC + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Wed, 01 Oct 2025 05:32:46 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=tgzcAoB2Z7A1EBAhdWjTn5VAGuE.4o_dguFgsgsh2MA-1759296766-1.0.1.1-qEcLr6VQp_yTzw7ki6Kj14BAULW4923EjbSL5nexTEe_ThFAr5iNYYbIhKXPxgyB54PvNtxyTwGa3_NMIkKiRbZXD3bmKQ.RAoESv_crx_A; + path=/; expires=Wed, 01-Oct-25 06:02:46 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=_ra7h_CrLz1mi9c7.chZSyzNAipeI2PAWKpir4OZOdw-1759296766316-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-processing-ms: + - '414' + openai-project: + - proj_HFFKG5Gf9Vuh44Vyp11iM5lW + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + x-envoy-upstream-service-time: + - '420' + x-ratelimit-limit-requests: + - '30000' + x-ratelimit-limit-tokens: + - '150000000' + x-ratelimit-remaining-requests: + - '29999' + x-ratelimit-remaining-tokens: + - '149999947' + x-ratelimit-reset-requests: + - 2ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_45b175a4eab5ac6ad8355a6fa98fc82a + status: + code: 200 + message: OK +version: 1 diff --git a/tests/test_openai.py b/tests/test_openai.py index bb83185..b9dad1b 100644 --- a/tests/test_openai.py +++ b/tests/test_openai.py @@ -76,3 +76,77 @@ def simple_tool(number): ) output = chain_response.text() assert output == snapshot + + +@pytest.mark.vcr +def test_conversation_chaining(vcr): + """Test that conversations use previous_response_id for chaining""" + model = llm.get_model("openai/gpt-4o-mini") + conversation = model.conversation() + + # First prompt - no chaining + response1 = conversation.prompt("What is 7+7?", key=API_KEY, stream=False) + text1 = response1.text() + assert "14" in text1 + + # Check first request didn't use previous_response_id + first_request = json.loads(vcr.requests[0].body) + assert "previous_response_id" not in first_request + assert "input" in first_request + + # Second prompt - should use chaining + response2 = conversation.prompt("Add 3 to that", key=API_KEY, stream=False) + text2 = response2.text() + assert "17" in text2 + + # Check second request used previous_response_id + second_request = json.loads(vcr.requests[1].body) + assert "previous_response_id" in second_request + # Should have previous_response_id from first response + assert second_request["previous_response_id"] == response1.response_json["id"] + # Input should only contain the new message (not full history) + assert len(second_request["input"]) == 1 + assert second_request["input"][0]["content"] == "Add 3 to that" + + +@pytest.mark.vcr +def test_conversation_chaining_with_store_false(vcr): + """Test that store=False falls back to full conversation history""" + model = llm.get_model("openai/gpt-4o-mini") + conversation = model.conversation() + + # First prompt with store=True + response1 = conversation.prompt("What is 8+8?", key=API_KEY, stream=False, store=True) + text1 = response1.text() + assert "16" in text1 + + # Second prompt with store=False - should NOT use chaining + response2 = conversation.prompt("Subtract 5", key=API_KEY, stream=False, store=False) + text2 = response2.text() + assert "11" in text2 + + # Check second request did NOT use previous_response_id + second_request = json.loads(vcr.requests[1].body) + assert "previous_response_id" not in second_request + # Input should contain full conversation history + assert len(second_request["input"]) >= 2 + + +@pytest.mark.vcr +def test_chained_response_stored_correctly(): + """Test that chained responses have previous_response_id in response_json""" + model = llm.get_model("openai/gpt-4o-mini") + conversation = model.conversation() + + # First response + response1 = conversation.prompt("Say 'first'", key=API_KEY, stream=False) + response1.text() + + # Second response - chained + response2 = conversation.prompt("Say 'second'", key=API_KEY, stream=False) + response2.text() + + # Verify response_json contains previous_response_id + assert response2.response_json is not None + assert "previous_response_id" in response2.response_json + assert response2.response_json["previous_response_id"] == response1.response_json["id"]