diff --git a/README.md b/README.md index 0b93cba..81eed51 100644 --- a/README.md +++ b/README.md @@ -468,6 +468,19 @@ print(response.output_text) for item in response.output: print(item.type) +# Structured input uses the Responses API part types +response = client.responses.create( + model="gpt-5.4", + input=[{ + "type": "message", + "role": "user", + "content": [ + {"type": "input_text", "text": "What is in this image?"}, + {"type": "input_image", "image_url": "https://example.com/cat.png"}, + ], + }], +) + # With reasoning response = client.responses.create( model="gpt-5.4", @@ -606,6 +619,12 @@ the `publish` confirmation value. ## Changelog +### Unreleased +- Type Responses API input parts: `ResponseInputText` (`input_text`), + `ResponseInputImage` (`input_image`) and `ResponseInputFile` (`input_file`). The old + `text` / `image` part types still type-check. +- Add `budget_tokens` to `ThinkingConfig`. + ### v0.4.0 - Add `client.audio`: `speech.create()` (binary audio with `write_to_file()`), `transcriptions.create()` and `translations.create()` (multipart uploads). diff --git a/apertis/types/__init__.py b/apertis/types/__init__.py index 7772b49..34915e1 100644 --- a/apertis/types/__init__.py +++ b/apertis/types/__init__.py @@ -59,6 +59,9 @@ ResponseReasoningContent, ResponseUsage, ResponseInputItem, + ResponseInputText, + ResponseInputImage, + ResponseInputFile, ResponseOutputItem, ResponseOutputMessage, ResponseOutputText, @@ -143,6 +146,9 @@ "ResponseReasoningContent", "ResponseUsage", "ResponseInputItem", + "ResponseInputText", + "ResponseInputImage", + "ResponseInputFile", "ResponseOutputItem", "ResponseOutputMessage", "ResponseOutputText", diff --git a/apertis/types/chat.py b/apertis/types/chat.py index 15275a9..14cb401 100644 --- a/apertis/types/chat.py +++ b/apertis/types/chat.py @@ -106,9 +106,13 @@ class ReasoningConfig(TypedDict, total=False): class ThinkingConfig(TypedDict, total=False): - """Extended thinking configuration for Gemini models.""" + """Extended thinking configuration. + + ``budget_tokens`` is read when the request goes to a Claude model. + """ type: Literal["enabled", "disabled", "auto"] + budget_tokens: int class GeminiThinkingConfig(TypedDict, total=False): diff --git a/apertis/types/responses.py b/apertis/types/responses.py index 65a1bfb..e0090a7 100644 --- a/apertis/types/responses.py +++ b/apertis/types/responses.py @@ -166,12 +166,46 @@ class ResponseInputImageContent(TypedDict, total=False): source: Required[Dict[str, Any]] # {"type": "base64", "media_type": "...", "data": "..."} -ResponseInputContent = Union[ResponseInputTextContent, ResponseInputImageContent] +class ResponseInputText(TypedDict): + """Text part of a Responses API input message.""" + + type: Required[Literal["input_text"]] + text: Required[str] + + +class ResponseInputImage(TypedDict, total=False): + """Image part of a Responses API input message: an ``image_url`` or a ``file_id``.""" + + type: Required[Literal["input_image"]] + image_url: str # URL or data URL + file_id: str + detail: Literal["low", "high", "auto"] + + +class ResponseInputFile(TypedDict, total=False): + """File part of a Responses API input message.""" + + type: Required[Literal["input_file"]] + file_id: str + file_data: str # data URL, e.g. "data:application/pdf;base64,..." + file_url: str + filename: str + + +# The first three are the Responses API part types; the last two are kept for +# code written against SDK 0.4.0 and earlier. +ResponseInputContent = Union[ + ResponseInputText, + ResponseInputImage, + ResponseInputFile, + ResponseInputTextContent, + ResponseInputImageContent, +] class ResponseInputItem(TypedDict, total=False): """Input item for responses.""" type: Required[Literal["message"]] - role: Required[Literal["user", "assistant"]] + role: Required[Literal["user", "assistant", "system", "developer"]] content: Required[Union[str, List[ResponseInputContent]]] diff --git a/tests/test_responses.py b/tests/test_responses.py index b020612..135d702 100644 --- a/tests/test_responses.py +++ b/tests/test_responses.py @@ -2,14 +2,20 @@ from __future__ import annotations +import json +from typing import List + import httpx import pytest import respx from pydantic import ValidationError from apertis import Apertis, AsyncApertis +from apertis.types.chat import ThinkingConfig from apertis.types.responses import ( Response, + ResponseInputContent, + ResponseInputItem, ResponseFunctionToolCall, ResponseOutputMessage, ResponseOutputRefusal, @@ -295,3 +301,28 @@ def test_message_with_bad_content_still_fails(self) -> None: "output": [{"type": "message", "id": "m", "role": "assistant"}], } ) + + +class TestTypedInputParts: + """Spec part types are accepted and sent unchanged.""" + + @respx.mock + def test_input_parts_sent_unchanged(self, client: Apertis) -> None: + route = respx.post("https://api.apertis.ai/v1/responses").mock( + return_value=httpx.Response(200, json=_REAL_SHAPE) + ) + content: List[ResponseInputContent] = [ + {"type": "input_text", "text": "Describe both."}, + {"type": "input_image", "image_url": "https://example.com/cat.png", "detail": "low"}, + {"type": "input_file", "file_data": "data:application/pdf;base64,JVBERi0=", "filename": "a.pdf"}, + {"type": "text", "text": "legacy part"}, + ] + item: ResponseInputItem = {"type": "message", "role": "user", "content": content} + + client.responses.create(model="gpt-5.4", input=[item]) + + assert json.loads(route.calls[0].request.content)["input"] == [item] + + def test_thinking_budget_tokens_type_checks(self) -> None: + thinking: ThinkingConfig = {"type": "enabled", "budget_tokens": 2048} + assert thinking["budget_tokens"] == 2048