From c9102e018dfa2b567bc2c4c319638cf3fa3e171d Mon Sep 17 00:00:00 2001 From: Ives van Hoorne Date: Fri, 7 Mar 2025 16:17:02 -0800 Subject: [PATCH 1/7] feat: code interpreter --- examples/code_interpreter_demo.py | 64 +++++++++ src/together/client.py | 5 + src/together/constants.py | 2 +- src/together/resources/audio/speech.py | 4 +- src/together/resources/chat/completions.py | 4 +- src/together/resources/code_interpreter.py | 58 +++++++++ src/together/resources/completions.py | 4 +- src/together/resources/embeddings.py | 4 +- src/together/resources/endpoints.py | 24 ++-- src/together/resources/files.py | 12 +- src/together/resources/finetune.py | 24 ++-- src/together/resources/images.py | 4 +- src/together/resources/models.py | 4 +- src/together/resources/rerank.py | 4 +- src/together/types/code_interpreter.py | 48 +++++++ tests/unit/test_code_interpreter.py | 145 +++++++++++++++++++++ 16 files changed, 365 insertions(+), 45 deletions(-) create mode 100644 examples/code_interpreter_demo.py create mode 100644 src/together/resources/code_interpreter.py create mode 100644 src/together/types/code_interpreter.py create mode 100644 tests/unit/test_code_interpreter.py diff --git a/examples/code_interpreter_demo.py b/examples/code_interpreter_demo.py new file mode 100644 index 0000000..191b2f1 --- /dev/null +++ b/examples/code_interpreter_demo.py @@ -0,0 +1,64 @@ +from together import Together + +client = Together() + +# Create a code interpreter instance +code_interpreter = client.code_interpreter + +# Example 1: Simple print statement +print("Example 1: Simple print") +response = code_interpreter.run( + code='print("Hello from Together!")', + language="python" +) +print(f"Status: {response.data.status}") +for output in response.data.outputs: + print(f"{output.type}: {output.data}") +if response.data.errors: + print(f"Errors: {response.data.errors}") +print("\n") + +# Example 2: Using session for maintaining state +print("Example 2: Using session for state") +response1 = code_interpreter.run( + code='x = 42', + language="python" +) +session_id = response1.data.session_id + +response2 = code_interpreter.run( + code='print(f"The value of x is {x}")', + language="python", + session_id=session_id +) +for output in response2.data.outputs: + print(f"{output.type}: {output.data}") +if response2.data.errors: + print(f"Errors: {response2.data.errors}") +print("\n") + +# Example 3: More complex computation +print("Example 3: Complex computation") +code = ''' +!pip install numpy +import numpy as np + +# Create a random matrix +matrix = np.random.rand(3, 3) +print("Random matrix:") +print(matrix) + +# Calculate eigenvalues +eigenvalues = np.linalg.eigvals(matrix) +print("\\nEigenvalues:") +print(eigenvalues) +''' + +response = code_interpreter.run( + code=code, + language="python" +) +for output in response.data.outputs: + print(f"{output.type}: {output.data}") +if response.data.errors: + print(f"Errors: {response.data.errors}") diff --git a/src/together/client.py b/src/together/client.py index ea5359a..dce6d30 100644 --- a/src/together/client.py +++ b/src/together/client.py @@ -6,6 +6,7 @@ from together import resources from together.constants import BASE_URL, MAX_RETRIES, TIMEOUT_SECS from together.error import AuthenticationError +from together.resources.code_interpreter import CodeInterpreter from together.types import TogetherClient from together.utils import enforce_trailing_slash @@ -20,6 +21,7 @@ class Together: fine_tuning: resources.FineTuning rerank: resources.Rerank audio: resources.Audio + code_interpreter: CodeInterpreter # client options client: TogetherClient @@ -82,6 +84,7 @@ def __init__( self.rerank = resources.Rerank(self.client) self.audio = resources.Audio(self.client) self.endpoints = resources.Endpoints(self.client) + self.code_interpreter = CodeInterpreter(self.client) class AsyncTogether: @@ -93,6 +96,7 @@ class AsyncTogether: models: resources.AsyncModels fine_tuning: resources.AsyncFineTuning rerank: resources.AsyncRerank + code_interpreter: CodeInterpreter # client options client: TogetherClient @@ -153,6 +157,7 @@ def __init__( self.models = resources.AsyncModels(self.client) self.fine_tuning = resources.AsyncFineTuning(self.client) self.rerank = resources.AsyncRerank(self.client) + self.code_interpreter = CodeInterpreter(self.client) Client = Together diff --git a/src/together/constants.py b/src/together/constants.py index c64af32..067a676 100644 --- a/src/together/constants.py +++ b/src/together/constants.py @@ -9,7 +9,7 @@ MAX_RETRY_DELAY = 8.0 # API defaults -BASE_URL = "https://api.together.xyz/v1" +BASE_URL = "https://api.together.xyz" # Download defaults DOWNLOAD_BLOCK_SIZE = 10 * 1024 * 1024 # 10 MB diff --git a/src/together/resources/audio/speech.py b/src/together/resources/audio/speech.py index da01586..2aecd10 100644 --- a/src/together/resources/audio/speech.py +++ b/src/together/resources/audio/speech.py @@ -76,7 +76,7 @@ def create( response, streamed, _ = requestor.request( options=TogetherRequest( method="POST", - url="audio/speech", + url="/v1/audio/speech", params=parameter_payload, ), stream=stream, @@ -144,7 +144,7 @@ async def create( response, _, _ = await requestor.arequest( options=TogetherRequest( method="POST", - url="audio/speech", + url="/v1/audio/speech", params=parameter_payload, ), stream=stream, diff --git a/src/together/resources/chat/completions.py b/src/together/resources/chat/completions.py index 5e4b44b..f28205d 100644 --- a/src/together/resources/chat/completions.py +++ b/src/together/resources/chat/completions.py @@ -141,7 +141,7 @@ def create( response, _, _ = requestor.request( options=TogetherRequest( method="POST", - url="chat/completions", + url="/v1/chat/completions", params=parameter_payload, ), stream=stream, @@ -283,7 +283,7 @@ async def create( response, _, _ = await requestor.arequest( options=TogetherRequest( method="POST", - url="chat/completions", + url="/v1/chat/completions", params=parameter_payload, ), stream=stream, diff --git a/src/together/resources/code_interpreter.py b/src/together/resources/code_interpreter.py new file mode 100644 index 0000000..c37a834 --- /dev/null +++ b/src/together/resources/code_interpreter.py @@ -0,0 +1,58 @@ +from __future__ import annotations + +from typing import Dict, Literal, Optional + +from together.abstract import api_requestor +from together.together_response import TogetherResponse +from together.types import TogetherClient, TogetherRequest +from together.types.code_interpreter import ExecuteResponse + + +class CodeInterpreter: + """Code Interpreter resource for executing code snippets.""" + + def __init__(self, client: TogetherClient) -> None: + self._client = client + + def run( + self, + code: str, + language: Literal["python"], + session_id: Optional[str] = None, + ) -> ExecuteResponse: + """Execute a code snippet. + + Args: + code (str): Code snippet to execute + language (str): Programming language for the code to execute. Currently only supports Python. + session_id (str, optional): Identifier of the current session. Used to make follow-up calls. + + Returns: + ExecuteResponse: Object containing execution results and outputs + """ + requestor = api_requestor.APIRequestor( + client=self._client, + ) + + data: Dict[str, str] = { + "code": code, + "language": language, + } + + if session_id is not None: + data["session_id"] = session_id + + # Use absolute URL to bypass the /v1 prefix + response, _, _ = requestor.request( + options=TogetherRequest( + method="POST", + url="/tci/execute", + params=data, + ), + stream=False, + ) + + assert isinstance(response, TogetherResponse) + + # Return the response data directly since our types match the API structure + return ExecuteResponse(**response.data) diff --git a/src/together/resources/completions.py b/src/together/resources/completions.py index de48471..f0888da 100644 --- a/src/together/resources/completions.py +++ b/src/together/resources/completions.py @@ -123,7 +123,7 @@ def create( response, _, _ = requestor.request( options=TogetherRequest( method="POST", - url="completions", + url="/v1/completions", params=parameter_payload, ), stream=stream, @@ -247,7 +247,7 @@ async def create( response, _, _ = await requestor.arequest( options=TogetherRequest( method="POST", - url="completions", + url="/v1/completions", params=parameter_payload, ), stream=stream, diff --git a/src/together/resources/embeddings.py b/src/together/resources/embeddings.py index c7f9e16..6201a3e 100644 --- a/src/together/resources/embeddings.py +++ b/src/together/resources/embeddings.py @@ -47,7 +47,7 @@ def create( response, _, _ = requestor.request( options=TogetherRequest( method="POST", - url="embeddings", + url="/v1/embeddings", params=parameter_payload, ), stream=False, @@ -93,7 +93,7 @@ async def create( response, _, _ = await requestor.arequest( options=TogetherRequest( method="POST", - url="embeddings", + url="/v1/embeddings", params=parameter_payload, ), stream=False, diff --git a/src/together/resources/endpoints.py b/src/together/resources/endpoints.py index 176894f..946e4cf 100644 --- a/src/together/resources/endpoints.py +++ b/src/together/resources/endpoints.py @@ -35,7 +35,7 @@ def list( response, _, _ = requestor.request( options=TogetherRequest( method="GET", - url="endpoints", + url="/v1/endpoints", params=params, ), stream=False, @@ -98,7 +98,7 @@ def create( response, _, _ = requestor.request( options=TogetherRequest( method="POST", - url="endpoints", + url="/v1/endpoints", params=data, ), stream=False, @@ -125,7 +125,7 @@ def get(self, endpoint_id: str) -> DedicatedEndpoint: response, _, _ = requestor.request( options=TogetherRequest( method="GET", - url=f"endpoints/{endpoint_id}", + url=f"/v1/endpoints/{endpoint_id}", ), stream=False, ) @@ -148,7 +148,7 @@ def delete(self, endpoint_id: str) -> None: requestor.request( options=TogetherRequest( method="DELETE", - url=f"endpoints/{endpoint_id}", + url=f"/v1/endpoints/{endpoint_id}", ), stream=False, ) @@ -203,7 +203,7 @@ def update( response, _, _ = requestor.request( options=TogetherRequest( method="PATCH", - url=f"endpoints/{endpoint_id}", + url=f"/v1/endpoints/{endpoint_id}", params=data, ), stream=False, @@ -235,7 +235,7 @@ def list_hardware(self, model: Optional[str] = None) -> List[HardwareWithStatus] response, _, _ = requestor.request( options=TogetherRequest( method="GET", - url="hardware", + url="/v1/hardware", params=params, ), stream=False, @@ -275,7 +275,7 @@ async def list( response, _, _ = await requestor.arequest( options=TogetherRequest( method="GET", - url="endpoints", + url="/v1/endpoints", params=params, ), stream=False, @@ -336,7 +336,7 @@ async def create( response, _, _ = await requestor.arequest( options=TogetherRequest( method="POST", - url="endpoints", + url="/v1/endpoints", params=data, ), stream=False, @@ -363,7 +363,7 @@ async def get(self, endpoint_id: str) -> DedicatedEndpoint: response, _, _ = await requestor.arequest( options=TogetherRequest( method="GET", - url=f"endpoints/{endpoint_id}", + url=f"/v1/endpoints/{endpoint_id}", ), stream=False, ) @@ -386,7 +386,7 @@ async def delete(self, endpoint_id: str) -> None: await requestor.arequest( options=TogetherRequest( method="DELETE", - url=f"endpoints/{endpoint_id}", + url=f"/v1/endpoints/{endpoint_id}", ), stream=False, ) @@ -441,7 +441,7 @@ async def update( response, _, _ = await requestor.arequest( options=TogetherRequest( method="PATCH", - url=f"endpoints/{endpoint_id}", + url=f"/v1/endpoints/{endpoint_id}", params=data, ), stream=False, @@ -475,7 +475,7 @@ async def list_hardware( response, _, _ = await requestor.arequest( options=TogetherRequest( method="GET", - url="hardware", + url="/v1/hardware", params=params, ), stream=False, diff --git a/src/together/resources/files.py b/src/together/resources/files.py index 14500b2..33a6294 100644 --- a/src/together/resources/files.py +++ b/src/together/resources/files.py @@ -57,7 +57,7 @@ def list(self) -> FileList: response, _, _ = requestor.request( options=TogetherRequest( method="GET", - url="files", + url="/v1/files", ), stream=False, ) @@ -74,7 +74,7 @@ def retrieve(self, id: str) -> FileResponse: response, _, _ = requestor.request( options=TogetherRequest( method="GET", - url=f"files/{id}", + url=f"/v1/files/{id}", ), stream=False, ) @@ -110,7 +110,7 @@ def delete(self, id: str) -> FileDeleteResponse: response, _, _ = requestor.request( options=TogetherRequest( method="DELETE", - url=f"files/{id}", + url=f"/v1/files/{id}", ), stream=False, ) @@ -137,7 +137,7 @@ async def list(self) -> FileList: response, _, _ = await requestor.arequest( options=TogetherRequest( method="GET", - url="files", + url="/v1/files", ), stream=False, ) @@ -154,7 +154,7 @@ async def retrieve(self, id: str) -> FileResponse: response, _, _ = await requestor.arequest( options=TogetherRequest( method="GET", - url=f"files/{id}", + url=f"/v1/files/{id}", ), stream=False, ) @@ -176,7 +176,7 @@ async def delete(self, id: str) -> FileDeleteResponse: response, _, _ = await requestor.arequest( options=TogetherRequest( method="DELETE", - url=f"files/{id}", + url=f"/v1/files/{id}", ), stream=False, ) diff --git a/src/together/resources/finetune.py b/src/together/resources/finetune.py index b58cdae..be0550a 100644 --- a/src/together/resources/finetune.py +++ b/src/together/resources/finetune.py @@ -256,7 +256,7 @@ def create( response, _, _ = requestor.request( options=TogetherRequest( method="POST", - url="fine-tunes", + url="/v1/fine-tunes", params=parameter_payload, ), stream=False, @@ -281,7 +281,7 @@ def list(self) -> FinetuneList: response, _, _ = requestor.request( options=TogetherRequest( method="GET", - url="fine-tunes", + url="/v1/fine-tunes", ), stream=False, ) @@ -308,7 +308,7 @@ def retrieve(self, id: str) -> FinetuneResponse: response, _, _ = requestor.request( options=TogetherRequest( method="GET", - url=f"fine-tunes/{id}", + url=f"/v1/fine-tunes/{id}", ), stream=False, ) @@ -335,7 +335,7 @@ def cancel(self, id: str) -> FinetuneResponse: response, _, _ = requestor.request( options=TogetherRequest( method="POST", - url=f"fine-tunes/{id}/cancel", + url=f"/v1/fine-tunes/{id}/cancel", ), stream=False, ) @@ -362,7 +362,7 @@ def list_events(self, id: str) -> FinetuneListEvents: response, _, _ = requestor.request( options=TogetherRequest( method="GET", - url=f"fine-tunes/{id}/events", + url=f"/v1/fine-tunes/{id}/events", ), stream=False, ) @@ -460,7 +460,7 @@ def get_model_limits(self, *, model: str) -> FinetuneTrainingLimits: model_limits_response, _, _ = requestor.request( options=TogetherRequest( method="GET", - url="fine-tunes/models/limits", + url="/v1/fine-tunes/models/limits", params={"model_name": model}, ), stream=False, @@ -597,7 +597,7 @@ async def create( response, _, _ = await requestor.arequest( options=TogetherRequest( method="POST", - url="fine-tunes", + url="/v1/fine-tunes", params=parameter_payload, ), stream=False, @@ -622,7 +622,7 @@ async def list(self) -> FinetuneList: response, _, _ = await requestor.arequest( options=TogetherRequest( method="GET", - url="fine-tunes", + url="/v1/fine-tunes", ), stream=False, ) @@ -649,7 +649,7 @@ async def retrieve(self, id: str) -> FinetuneResponse: response, _, _ = await requestor.arequest( options=TogetherRequest( method="GET", - url=f"fine-tunes/{id}", + url=f"/v1/fine-tunes/{id}", ), stream=False, ) @@ -676,7 +676,7 @@ async def cancel(self, id: str) -> FinetuneResponse: response, _, _ = await requestor.arequest( options=TogetherRequest( method="POST", - url=f"fine-tunes/{id}/cancel", + url=f"/v1/fine-tunes/{id}/cancel", ), stream=False, ) @@ -703,7 +703,7 @@ async def list_events(self, id: str) -> FinetuneListEvents: response, _, _ = await requestor.arequest( options=TogetherRequest( method="GET", - url=f"fine-tunes/{id}/events", + url=f"/v1/fine-tunes/{id}/events", ), stream=False, ) @@ -742,7 +742,7 @@ async def get_model_limits(self, *, model: str) -> FinetuneTrainingLimits: model_limits_response, _, _ = await requestor.arequest( options=TogetherRequest( method="GET", - url="fine-tunes/models/limits", + url="/v1/fine-tunes/models/limits", params={"model": model}, ), stream=False, diff --git a/src/together/resources/images.py b/src/together/resources/images.py index 5304210..0612eda 100644 --- a/src/together/resources/images.py +++ b/src/together/resources/images.py @@ -76,7 +76,7 @@ def generate( response, _, _ = requestor.request( options=TogetherRequest( method="POST", - url="images/generations", + url="/v1/images/generations", params=parameter_payload, ), stream=False, @@ -151,7 +151,7 @@ async def generate( response, _, _ = await requestor.arequest( options=TogetherRequest( method="POST", - url="images/generations", + url="/v1/images/generations", params=parameter_payload, ), stream=False, diff --git a/src/together/resources/models.py b/src/together/resources/models.py index 9a85e9b..c8e1d3e 100644 --- a/src/together/resources/models.py +++ b/src/together/resources/models.py @@ -32,7 +32,7 @@ def list( response, _, _ = requestor.request( options=TogetherRequest( method="GET", - url="models", + url="/v1/models", ), stream=False, ) @@ -64,7 +64,7 @@ async def list( response, _, _ = await requestor.arequest( options=TogetherRequest( method="GET", - url="models", + url="/v1/models", ), stream=False, ) diff --git a/src/together/resources/rerank.py b/src/together/resources/rerank.py index 6c384ea..a57a91c 100644 --- a/src/together/resources/rerank.py +++ b/src/together/resources/rerank.py @@ -59,7 +59,7 @@ def create( response, _, _ = requestor.request( options=TogetherRequest( method="POST", - url="rerank", + url="/v1/rerank", params=parameter_payload, ), stream=False, @@ -117,7 +117,7 @@ async def create( response, _, _ = await requestor.arequest( options=TogetherRequest( method="POST", - url="rerank", + url="/v1/rerank", params=parameter_payload, ), stream=False, diff --git a/src/together/types/code_interpreter.py b/src/together/types/code_interpreter.py new file mode 100644 index 0000000..ead0288 --- /dev/null +++ b/src/together/types/code_interpreter.py @@ -0,0 +1,48 @@ +from __future__ import annotations + +from typing import Any, Dict, Literal, Union + +from pydantic import Field + +from together.types.endpoints import TogetherJSONModel + + +class InterpreterOutput(TogetherJSONModel): + """Base class for interpreter output types.""" + type: Literal["stdout", "stderr", "error", "display_data", "execute_result"] = Field( + description="The type of output" + ) + data: Union[str, Dict[str, Any]] = Field(description="The output data") + + +class ExecuteResponseData(TogetherJSONModel): + """Data from code execution response.""" + outputs: list[InterpreterOutput] = Field( + description="List of outputs from execution", + default_factory=list + ) + errors: Union[str, None] = Field( + description="Any errors that occurred during execution", + default=None + ) + session_id: str = Field( + description="Identifier of the current session. Used to make follow-up calls." + ) + status: str = Field( + description="Status of the execution", + default="completed" + ) + + +class ExecuteResponse(TogetherJSONModel): + """Response from code execution.""" + data: ExecuteResponseData = Field( + description="The response data containing outputs and session information" + ) + + +__all__ = [ + "InterpreterOutput", + "ExecuteResponseData", + "ExecuteResponse", +] diff --git a/tests/unit/test_code_interpreter.py b/tests/unit/test_code_interpreter.py new file mode 100644 index 0000000..da95b5a --- /dev/null +++ b/tests/unit/test_code_interpreter.py @@ -0,0 +1,145 @@ +from __future__ import annotations + +import pytest + +from together.resources.code_interpreter import CodeInterpreter +from together.together_response import TogetherResponse +from together.types.code_interpreter import ExecuteResponse, ExecuteResponseData, InterpreterOutput + + +def test_interpreter_output_validation(): + # Test valid stdout output + stdout = InterpreterOutput(type="stdout", data="Hello, world!") + assert stdout.type == "stdout" + assert stdout.data == "Hello, world!" + + # Test valid display_data output + display_data = InterpreterOutput( + type="display_data", + data={ + "text/plain": "Hello", + "text/html": "

Hello

", + }, + ) + assert display_data.type == "display_data" + assert display_data.data["text/plain"] == "Hello" + + # Test invalid type + with pytest.raises(ValueError): + InterpreterOutput(type="invalid", data="test") + + +def test_execute_response_validation(): + # Test valid response + outputs = [ + InterpreterOutput(type="stdout", data="Hello"), + InterpreterOutput(type="stderr", data="Warning"), + ] + response = ExecuteResponse( + data=ExecuteResponseData( + session_id="test_session", + status="success", + outputs=outputs, + ) + ) + assert response.data.session_id == "test_session" + assert response.data.status == "success" + assert len(response.data.outputs) == 2 + assert response.data.outputs[0].type == "stdout" + assert response.data.outputs[1].type == "stderr" + + +def test_code_interpreter_run(mocker): + # Mock the API requestor + mock_requestor = mocker.MagicMock() + response_data = { + "data": { + "session_id": "test_session", + "status": "success", + "outputs": [{"type": "stdout", "data": "Hello, world!"}], + } + } + mock_headers = { + "cf-ray": "test-ray-id", + "x-ratelimit-remaining": "100", + "x-hostname": "test-host", + "x-total-time": "42.0", + } + mock_response = TogetherResponse(data=response_data, headers=mock_headers) + mock_requestor.request.return_value = (mock_response, None, None) + mocker.patch("together.abstract.api_requestor.APIRequestor", return_value=mock_requestor) + + # Create code interpreter instance + client = mocker.MagicMock() + interpreter = CodeInterpreter(client) + + # Test run method + response = interpreter.run( + code='print("Hello, world!")', + language="python", + session_id="test_session", + ) + + # Verify the response + assert isinstance(response, ExecuteResponse) + assert response.data.session_id == "test_session" + assert response.data.status == "success" + assert len(response.data.outputs) == 1 + assert response.data.outputs[0].type == "stdout" + assert response.data.outputs[0].data == "Hello, world!" + + # Verify API request + mock_requestor.request.assert_called_once_with( + options=mocker.ANY, + stream=False, + ) + request_options = mock_requestor.request.call_args[1]["options"] + assert request_options.method == "POST" + assert request_options.url == "/tci/execute" + assert request_options.params == { + "code": 'print("Hello, world!")', + "language": "python", + "session_id": "test_session", + } + + +def test_code_interpreter_run_without_session(mocker): + # Mock the API requestor + mock_requestor = mocker.MagicMock() + response_data = { + "data": { + "session_id": "new_session", + "status": "success", + "outputs": [], + } + } + mock_headers = { + "cf-ray": "test-ray-id-2", + "x-ratelimit-remaining": "99", + "x-hostname": "test-host", + "x-total-time": "42.0", + } + mock_response = TogetherResponse(data=response_data, headers=mock_headers) + mock_requestor.request.return_value = (mock_response, None, None) + mocker.patch("together.abstract.api_requestor.APIRequestor", return_value=mock_requestor) + + # Create code interpreter instance + client = mocker.MagicMock() + interpreter = CodeInterpreter(client) + + # Test run method without session_id + response = interpreter.run( + code="x = 1", + language="python", + ) + + # Verify the response + assert isinstance(response, ExecuteResponse) + assert response.data.session_id == "new_session" + + # Verify API request doesn't include session_id + request_options = mock_requestor.request.call_args[1]["options"] + assert request_options.params == { + "code": "x = 1", + "language": "python", + } From c8be2b1ac098ffd614f8c3000e7e5a7a869a1ddf Mon Sep 17 00:00:00 2001 From: Ives van Hoorne Date: Fri, 7 Mar 2025 16:25:56 -0800 Subject: [PATCH 2/7] fix base url tests --- tests/unit/test_async_client.py | 2 +- tests/unit/test_client.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_async_client.py b/tests/unit/test_async_client.py index 0b11b39..858414a 100644 --- a/tests/unit/test_async_client.py +++ b/tests/unit/test_async_client.py @@ -50,7 +50,7 @@ def test_init_with_default_base_url(self): async_together = AsyncTogether(api_key="fake_api_key") - assert async_together.client.base_url == "https://api.together.xyz/v1/" + assert async_together.client.base_url == "https://api.together.xyz/" def test_init_with_supplied_headers(self): """ diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index f8bdcbe..827cae9 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -51,7 +51,7 @@ def test_init_with_default_base_url(self): with patch.dict("os.environ", clear=True): sync_together = Together(api_key="fake_api_key") - assert sync_together.client.base_url == "https://api.together.xyz/v1/" + assert sync_together.client.base_url == "https://api.together.xyz/" def test_init_with_supplied_headers(self): """ From 061379963527dd17d0cefb9ebc627f3d96911b63 Mon Sep 17 00:00:00 2001 From: Ives van Hoorne Date: Thu, 27 Mar 2025 17:17:31 -0700 Subject: [PATCH 3/7] use v1 route --- src/together/constants.py | 2 +- src/together/resources/audio/speech.py | 4 +- src/together/resources/chat/completions.py | 4 +- src/together/resources/completions.py | 4 +- src/together/resources/embeddings.py | 4 +- src/together/resources/endpoints.py | 52 ++-- src/together/resources/files.py | 12 +- src/together/resources/finetune.py | 316 ++++++++++++++++++--- src/together/resources/images.py | 4 +- src/together/resources/models.py | 79 +++++- src/together/resources/rerank.py | 4 +- tests/unit/test_async_client.py | 2 +- tests/unit/test_client.py | 2 +- 13 files changed, 395 insertions(+), 94 deletions(-) diff --git a/src/together/constants.py b/src/together/constants.py index 067a676..c64af32 100644 --- a/src/together/constants.py +++ b/src/together/constants.py @@ -9,7 +9,7 @@ MAX_RETRY_DELAY = 8.0 # API defaults -BASE_URL = "https://api.together.xyz" +BASE_URL = "https://api.together.xyz/v1" # Download defaults DOWNLOAD_BLOCK_SIZE = 10 * 1024 * 1024 # 10 MB diff --git a/src/together/resources/audio/speech.py b/src/together/resources/audio/speech.py index 2aecd10..da01586 100644 --- a/src/together/resources/audio/speech.py +++ b/src/together/resources/audio/speech.py @@ -76,7 +76,7 @@ def create( response, streamed, _ = requestor.request( options=TogetherRequest( method="POST", - url="/v1/audio/speech", + url="audio/speech", params=parameter_payload, ), stream=stream, @@ -144,7 +144,7 @@ async def create( response, _, _ = await requestor.arequest( options=TogetherRequest( method="POST", - url="/v1/audio/speech", + url="audio/speech", params=parameter_payload, ), stream=stream, diff --git a/src/together/resources/chat/completions.py b/src/together/resources/chat/completions.py index f28205d..5e4b44b 100644 --- a/src/together/resources/chat/completions.py +++ b/src/together/resources/chat/completions.py @@ -141,7 +141,7 @@ def create( response, _, _ = requestor.request( options=TogetherRequest( method="POST", - url="/v1/chat/completions", + url="chat/completions", params=parameter_payload, ), stream=stream, @@ -283,7 +283,7 @@ async def create( response, _, _ = await requestor.arequest( options=TogetherRequest( method="POST", - url="/v1/chat/completions", + url="chat/completions", params=parameter_payload, ), stream=stream, diff --git a/src/together/resources/completions.py b/src/together/resources/completions.py index f0888da..de48471 100644 --- a/src/together/resources/completions.py +++ b/src/together/resources/completions.py @@ -123,7 +123,7 @@ def create( response, _, _ = requestor.request( options=TogetherRequest( method="POST", - url="/v1/completions", + url="completions", params=parameter_payload, ), stream=stream, @@ -247,7 +247,7 @@ async def create( response, _, _ = await requestor.arequest( options=TogetherRequest( method="POST", - url="/v1/completions", + url="completions", params=parameter_payload, ), stream=stream, diff --git a/src/together/resources/embeddings.py b/src/together/resources/embeddings.py index 6201a3e..c7f9e16 100644 --- a/src/together/resources/embeddings.py +++ b/src/together/resources/embeddings.py @@ -47,7 +47,7 @@ def create( response, _, _ = requestor.request( options=TogetherRequest( method="POST", - url="/v1/embeddings", + url="embeddings", params=parameter_payload, ), stream=False, @@ -93,7 +93,7 @@ async def create( response, _, _ = await requestor.arequest( options=TogetherRequest( method="POST", - url="/v1/embeddings", + url="embeddings", params=parameter_payload, ), stream=False, diff --git a/src/together/resources/endpoints.py b/src/together/resources/endpoints.py index 946e4cf..5d8f9a4 100644 --- a/src/together/resources/endpoints.py +++ b/src/together/resources/endpoints.py @@ -35,7 +35,7 @@ def list( response, _, _ = requestor.request( options=TogetherRequest( method="GET", - url="/v1/endpoints", + url="endpoints", params=params, ), stream=False, @@ -59,6 +59,7 @@ def create( disable_prompt_cache: bool = False, disable_speculative_decoding: bool = False, state: Literal["STARTED", "STOPPED"] = "STARTED", + inactive_timeout: Optional[int] = None, ) -> DedicatedEndpoint: """ Create a new dedicated endpoint. @@ -72,6 +73,7 @@ def create( disable_prompt_cache (bool, optional): Whether to disable the prompt cache. Defaults to False. disable_speculative_decoding (bool, optional): Whether to disable speculative decoding. Defaults to False. state (str, optional): The desired state of the endpoint. Defaults to "STARTED". + inactive_timeout (int, optional): The number of minutes of inactivity after which the endpoint will be automatically stopped. Set to 0 to disable automatic timeout. Returns: DedicatedEndpoint: Object containing endpoint information @@ -80,7 +82,7 @@ def create( client=self._client, ) - data: Dict[str, Union[str, bool, Dict[str, int]]] = { + data: Dict[str, Union[str, bool, Dict[str, int], int]] = { "model": model, "hardware": hardware, "autoscaling": { @@ -95,10 +97,13 @@ def create( if display_name is not None: data["display_name"] = display_name + if inactive_timeout is not None: + data["inactive_timeout"] = inactive_timeout + response, _, _ = requestor.request( options=TogetherRequest( method="POST", - url="/v1/endpoints", + url="endpoints", params=data, ), stream=False, @@ -125,7 +130,7 @@ def get(self, endpoint_id: str) -> DedicatedEndpoint: response, _, _ = requestor.request( options=TogetherRequest( method="GET", - url=f"/v1/endpoints/{endpoint_id}", + url=f"endpoints/{endpoint_id}", ), stream=False, ) @@ -148,7 +153,7 @@ def delete(self, endpoint_id: str) -> None: requestor.request( options=TogetherRequest( method="DELETE", - url=f"/v1/endpoints/{endpoint_id}", + url=f"endpoints/{endpoint_id}", ), stream=False, ) @@ -161,6 +166,7 @@ def update( max_replicas: Optional[int] = None, state: Optional[Literal["STARTED", "STOPPED"]] = None, display_name: Optional[str] = None, + inactive_timeout: Optional[int] = None, ) -> DedicatedEndpoint: """ Update an endpoint's configuration. @@ -171,6 +177,7 @@ def update( max_replicas (int, optional): The maximum number of replicas to scale up to state (str, optional): The desired state of the endpoint ("STARTED" or "STOPPED") display_name (str, optional): A human-readable name for the endpoint + inactive_timeout (int, optional): The number of minutes of inactivity after which the endpoint will be automatically stopped. Set to 0 to disable automatic timeout. Returns: DedicatedEndpoint: Object containing endpoint information @@ -179,7 +186,7 @@ def update( client=self._client, ) - data: Dict[str, Union[str, Dict[str, int]]] = {} + data: Dict[str, Union[str, Dict[str, int], int]] = {} if min_replicas is not None or max_replicas is not None: current_min = min_replicas @@ -200,10 +207,13 @@ def update( if display_name is not None: data["display_name"] = display_name + if inactive_timeout is not None: + data["inactive_timeout"] = inactive_timeout + response, _, _ = requestor.request( options=TogetherRequest( method="PATCH", - url=f"/v1/endpoints/{endpoint_id}", + url=f"endpoints/{endpoint_id}", params=data, ), stream=False, @@ -235,7 +245,7 @@ def list_hardware(self, model: Optional[str] = None) -> List[HardwareWithStatus] response, _, _ = requestor.request( options=TogetherRequest( method="GET", - url="/v1/hardware", + url="hardware", params=params, ), stream=False, @@ -275,7 +285,7 @@ async def list( response, _, _ = await requestor.arequest( options=TogetherRequest( method="GET", - url="/v1/endpoints", + url="endpoints", params=params, ), stream=False, @@ -297,6 +307,7 @@ async def create( disable_prompt_cache: bool = False, disable_speculative_decoding: bool = False, state: Literal["STARTED", "STOPPED"] = "STARTED", + inactive_timeout: Optional[int] = None, ) -> DedicatedEndpoint: """ Create a new dedicated endpoint. @@ -310,6 +321,7 @@ async def create( disable_prompt_cache (bool, optional): Whether to disable the prompt cache. Defaults to False. disable_speculative_decoding (bool, optional): Whether to disable speculative decoding. Defaults to False. state (str, optional): The desired state of the endpoint. Defaults to "STARTED". + inactive_timeout (int, optional): The number of minutes of inactivity after which the endpoint will be automatically stopped. Set to 0 to disable automatic timeout. Returns: DedicatedEndpoint: Object containing endpoint information @@ -318,7 +330,7 @@ async def create( client=self._client, ) - data: Dict[str, Union[str, bool, Dict[str, int]]] = { + data: Dict[str, Union[str, bool, Dict[str, int], int]] = { "model": model, "hardware": hardware, "autoscaling": { @@ -333,10 +345,13 @@ async def create( if display_name is not None: data["display_name"] = display_name + if inactive_timeout is not None: + data["inactive_timeout"] = inactive_timeout + response, _, _ = await requestor.arequest( options=TogetherRequest( method="POST", - url="/v1/endpoints", + url="endpoints", params=data, ), stream=False, @@ -363,7 +378,7 @@ async def get(self, endpoint_id: str) -> DedicatedEndpoint: response, _, _ = await requestor.arequest( options=TogetherRequest( method="GET", - url=f"/v1/endpoints/{endpoint_id}", + url=f"endpoints/{endpoint_id}", ), stream=False, ) @@ -386,7 +401,7 @@ async def delete(self, endpoint_id: str) -> None: await requestor.arequest( options=TogetherRequest( method="DELETE", - url=f"/v1/endpoints/{endpoint_id}", + url=f"endpoints/{endpoint_id}", ), stream=False, ) @@ -399,6 +414,7 @@ async def update( max_replicas: Optional[int] = None, state: Optional[Literal["STARTED", "STOPPED"]] = None, display_name: Optional[str] = None, + inactive_timeout: Optional[int] = None, ) -> DedicatedEndpoint: """ Update an endpoint's configuration. @@ -409,6 +425,7 @@ async def update( max_replicas (int, optional): The maximum number of replicas to scale up to state (str, optional): The desired state of the endpoint ("STARTED" or "STOPPED") display_name (str, optional): A human-readable name for the endpoint + inactive_timeout (int, optional): The number of minutes of inactivity after which the endpoint will be automatically stopped. Set to 0 to disable automatic timeout. Returns: DedicatedEndpoint: Object containing endpoint information @@ -417,7 +434,7 @@ async def update( client=self._client, ) - data: Dict[str, Union[str, Dict[str, int]]] = {} + data: Dict[str, Union[str, Dict[str, int], int]] = {} if min_replicas is not None or max_replicas is not None: current_min = min_replicas @@ -438,10 +455,13 @@ async def update( if display_name is not None: data["display_name"] = display_name + if inactive_timeout is not None: + data["inactive_timeout"] = inactive_timeout + response, _, _ = await requestor.arequest( options=TogetherRequest( method="PATCH", - url=f"/v1/endpoints/{endpoint_id}", + url=f"endpoints/{endpoint_id}", params=data, ), stream=False, @@ -475,7 +495,7 @@ async def list_hardware( response, _, _ = await requestor.arequest( options=TogetherRequest( method="GET", - url="/v1/hardware", + url="hardware", params=params, ), stream=False, diff --git a/src/together/resources/files.py b/src/together/resources/files.py index 33a6294..14500b2 100644 --- a/src/together/resources/files.py +++ b/src/together/resources/files.py @@ -57,7 +57,7 @@ def list(self) -> FileList: response, _, _ = requestor.request( options=TogetherRequest( method="GET", - url="/v1/files", + url="files", ), stream=False, ) @@ -74,7 +74,7 @@ def retrieve(self, id: str) -> FileResponse: response, _, _ = requestor.request( options=TogetherRequest( method="GET", - url=f"/v1/files/{id}", + url=f"files/{id}", ), stream=False, ) @@ -110,7 +110,7 @@ def delete(self, id: str) -> FileDeleteResponse: response, _, _ = requestor.request( options=TogetherRequest( method="DELETE", - url=f"/v1/files/{id}", + url=f"files/{id}", ), stream=False, ) @@ -137,7 +137,7 @@ async def list(self) -> FileList: response, _, _ = await requestor.arequest( options=TogetherRequest( method="GET", - url="/v1/files", + url="files", ), stream=False, ) @@ -154,7 +154,7 @@ async def retrieve(self, id: str) -> FileResponse: response, _, _ = await requestor.arequest( options=TogetherRequest( method="GET", - url=f"/v1/files/{id}", + url=f"files/{id}", ), stream=False, ) @@ -176,7 +176,7 @@ async def delete(self, id: str) -> FileDeleteResponse: response, _, _ = await requestor.arequest( options=TogetherRequest( method="DELETE", - url=f"/v1/files/{id}", + url=f"files/{id}", ), stream=False, ) diff --git a/src/together/resources/finetune.py b/src/together/resources/finetune.py index be0550a..6dce5db 100644 --- a/src/together/resources/finetune.py +++ b/src/together/resources/finetune.py @@ -1,7 +1,8 @@ from __future__ import annotations +import re from pathlib import Path -from typing import Literal +from typing import Literal, List from rich import print as rprint @@ -21,23 +22,47 @@ TogetherRequest, TrainingType, FinetuneLRScheduler, + FinetuneLinearLRScheduler, + FinetuneCosineLRScheduler, FinetuneLinearLRSchedulerArgs, + FinetuneCosineLRSchedulerArgs, + TrainingMethodDPO, + TrainingMethodSFT, + FinetuneCheckpoint, ) -from together.types.finetune import DownloadCheckpointType -from together.utils import log_warn_once, normalize_key +from together.types.finetune import ( + DownloadCheckpointType, + FinetuneEventType, + FinetuneEvent, +) +from together.utils import ( + log_warn_once, + normalize_key, + get_event_step, +) + +_FT_JOB_WITH_STEP_REGEX = r"^ft-[\dabcdef-]+:\d+$" + + +AVAILABLE_TRAINING_METHODS = { + TrainingMethodSFT().method, + TrainingMethodDPO().method, +} def createFinetuneRequest( model_limits: FinetuneTrainingLimits, training_file: str, - model: str, + model: str | None = None, n_epochs: int = 1, validation_file: str | None = "", n_evals: int | None = 0, n_checkpoints: int | None = 1, batch_size: int | Literal["max"] = "max", learning_rate: float | None = 0.00001, + lr_scheduler_type: Literal["linear", "cosine"] = "linear", min_lr_ratio: float = 0.0, + scheduler_num_cycles: float = 0.5, warmup_ratio: float = 0.0, max_grad_norm: float = 1.0, weight_decay: float = 0.0, @@ -52,7 +77,19 @@ def createFinetuneRequest( wandb_project_name: str | None = None, wandb_name: str | None = None, train_on_inputs: bool | Literal["auto"] = "auto", + training_method: str = "sft", + dpo_beta: float | None = None, + from_checkpoint: str | None = None, ) -> FinetuneRequest: + + if model is not None and from_checkpoint is not None: + raise ValueError( + "You must specify either a model or a checkpoint to start a job from, not both" + ) + + if model is None and from_checkpoint is None: + raise ValueError("You must specify either a model or a checkpoint") + if batch_size == "max": log_warn_once( "Starting from together>=1.3.0, " @@ -62,6 +99,8 @@ def createFinetuneRequest( warmup_ratio = 0.0 training_type: TrainingType = FullTrainingType() + max_batch_size: int = 0 + min_batch_size: int = 0 if lora: if model_limits.lora_training is None: raise ValueError("LoRA adapters are not supported for the selected model.") @@ -74,18 +113,26 @@ def createFinetuneRequest( lora_trainable_modules=lora_trainable_modules, ) - batch_size = ( - batch_size - if batch_size != "max" - else model_limits.lora_training.max_batch_size - ) + max_batch_size = model_limits.lora_training.max_batch_size + min_batch_size = model_limits.lora_training.min_batch_size + else: if model_limits.full_training is None: raise ValueError("Full training is not supported for the selected model.") - batch_size = ( - batch_size - if batch_size != "max" - else model_limits.full_training.max_batch_size + + max_batch_size = model_limits.full_training.max_batch_size + min_batch_size = model_limits.full_training.min_batch_size + + batch_size = batch_size if batch_size != "max" else max_batch_size + + if batch_size > max_batch_size: + raise ValueError( + "Requested batch size is higher that the maximum allowed value." + ) + + if batch_size < min_batch_size: + raise ValueError( + "Requested batch size is lower that the minimum allowed value." ) if warmup_ratio > 1 or warmup_ratio < 0: @@ -100,10 +147,31 @@ def createFinetuneRequest( if weight_decay is not None and (weight_decay < 0): raise ValueError("Weight decay should be non-negative") - lrScheduler = FinetuneLRScheduler( - lr_scheduler_type="linear", - lr_scheduler_args=FinetuneLinearLRSchedulerArgs(min_lr_ratio=min_lr_ratio), - ) + if training_method not in AVAILABLE_TRAINING_METHODS: + raise ValueError( + f"training_method must be one of {', '.join(AVAILABLE_TRAINING_METHODS)}" + ) + + # Default to generic lr scheduler + lrScheduler: FinetuneLRScheduler = FinetuneLRScheduler(lr_scheduler_type="linear") + + if lr_scheduler_type == "cosine": + if scheduler_num_cycles <= 0.0: + raise ValueError("Number of cycles should be greater than 0") + + lrScheduler = FinetuneCosineLRScheduler( + lr_scheduler_args=FinetuneCosineLRSchedulerArgs( + min_lr_ratio=min_lr_ratio, num_cycles=scheduler_num_cycles + ), + ) + else: + lrScheduler = FinetuneLinearLRScheduler( + lr_scheduler_args=FinetuneLinearLRSchedulerArgs(min_lr_ratio=min_lr_ratio), + ) + + training_method_cls: TrainingMethodSFT | TrainingMethodDPO = TrainingMethodSFT() + if training_method == "dpo": + training_method_cls = TrainingMethodDPO(dpo_beta=dpo_beta) finetune_request = FinetuneRequest( model=model, @@ -125,11 +193,77 @@ def createFinetuneRequest( wandb_project_name=wandb_project_name, wandb_name=wandb_name, train_on_inputs=train_on_inputs, + training_method=training_method_cls, + from_checkpoint=from_checkpoint, ) return finetune_request +def _process_checkpoints_from_events( + events: List[FinetuneEvent], id: str +) -> List[FinetuneCheckpoint]: + """ + Helper function to process events and create checkpoint list. + + Args: + events (List[FinetuneEvent]): List of fine-tune events to process + id (str): Fine-tune job ID + + Returns: + List[FinetuneCheckpoint]: List of available checkpoints + """ + checkpoints: List[FinetuneCheckpoint] = [] + + for event in events: + event_type = event.type + + if event_type == FinetuneEventType.CHECKPOINT_SAVE: + step = get_event_step(event) + checkpoint_name = f"{id}:{step}" if step is not None else id + + checkpoints.append( + FinetuneCheckpoint( + type=( + f"Intermediate (step {step})" + if step is not None + else "Intermediate" + ), + timestamp=event.created_at, + name=checkpoint_name, + ) + ) + elif event_type == FinetuneEventType.JOB_COMPLETE: + if hasattr(event, "model_path"): + checkpoints.append( + FinetuneCheckpoint( + type=( + "Final Merged" + if hasattr(event, "adapter_path") + else "Final" + ), + timestamp=event.created_at, + name=id, + ) + ) + + if hasattr(event, "adapter_path"): + checkpoints.append( + FinetuneCheckpoint( + type=( + "Final Adapter" if hasattr(event, "model_path") else "Final" + ), + timestamp=event.created_at, + name=id, + ) + ) + + # Sort by timestamp (newest first) + checkpoints.sort(key=lambda x: x.timestamp, reverse=True) + + return checkpoints + + class FineTuning: def __init__(self, client: TogetherClient) -> None: self._client = client @@ -138,14 +272,16 @@ def create( self, *, training_file: str, - model: str, + model: str | None = None, n_epochs: int = 1, validation_file: str | None = "", n_evals: int | None = 0, n_checkpoints: int | None = 1, batch_size: int | Literal["max"] = "max", learning_rate: float | None = 0.00001, + lr_scheduler_type: Literal["linear", "cosine"] = "linear", min_lr_ratio: float = 0.0, + scheduler_num_cycles: float = 0.5, warmup_ratio: float = 0.0, max_grad_norm: float = 1.0, weight_decay: float = 0.0, @@ -162,13 +298,16 @@ def create( verbose: bool = False, model_limits: FinetuneTrainingLimits | None = None, train_on_inputs: bool | Literal["auto"] = "auto", + training_method: str = "sft", + dpo_beta: float | None = None, + from_checkpoint: str | None = None, ) -> FinetuneResponse: """ Method to initiate a fine-tuning job Args: training_file (str): File-ID of a file uploaded to the Together API - model (str): Name of the base model to run fine-tune job on + model (str, optional): Name of the base model to run fine-tune job on n_epochs (int, optional): Number of epochs for fine-tuning. Defaults to 1. validation file (str, optional): File ID of a file uploaded to the Together API for validation. n_evals (int, optional): Number of evaluation loops to run. Defaults to 0. @@ -177,9 +316,11 @@ def create( batch_size (int or "max"): Batch size for fine-tuning. Defaults to max. learning_rate (float, optional): Learning rate multiplier to use for training Defaults to 0.00001. + lr_scheduler_type (Literal["linear", "cosine"]): Learning rate scheduler type. Defaults to "linear". min_lr_ratio (float, optional): Min learning rate ratio of the initial learning rate for the learning rate scheduler. Defaults to 0.0. - warmup_ratio (float, optional): Warmup ratio for learning rate scheduler. + scheduler_num_cycles (float, optional): Number or fraction of cycles for the cosine learning rate scheduler. Defaults to 0.5. + warmup_ratio (float, optional): Warmup ratio for the learning rate scheduler. max_grad_norm (float, optional): Max gradient norm. Defaults to 1.0, set to 0 to disable. weight_decay (float, optional): Weight decay. Defaults to 0.0. lora (bool, optional): Whether to use LoRA adapters. Defaults to True. @@ -207,6 +348,12 @@ def create( For datasets with the "messages" field (conversational format) or "prompt" and "completion" fields (Instruction format), inputs will be masked. Defaults to "auto". + training_method (str, optional): Training method. Defaults to "sft". + Supported methods: "sft", "dpo". + dpo_beta (float, optional): DPO beta parameter. Defaults to None. + from_checkpoint (str, optional): The checkpoint identifier to continue training from a previous fine-tuning job. + The format: {$JOB_ID/$OUTPUT_MODEL_NAME}:{$STEP}. + The step value is optional, without it the final checkpoint will be used. Returns: FinetuneResponse: Object containing information about fine-tuning job. @@ -217,7 +364,15 @@ def create( ) if model_limits is None: - model_limits = self.get_model_limits(model=model) + # mypy doesn't understand that model or from_checkpoint is not None + if model is not None: + model_name = model + elif from_checkpoint is not None: + model_name = from_checkpoint.split(":")[0] + else: + # this branch is unreachable, but mypy doesn't know that + pass + model_limits = self.get_model_limits(model=model_name) finetune_request = createFinetuneRequest( model_limits=model_limits, @@ -229,7 +384,9 @@ def create( n_checkpoints=n_checkpoints, batch_size=batch_size, learning_rate=learning_rate, + lr_scheduler_type=lr_scheduler_type, min_lr_ratio=min_lr_ratio, + scheduler_num_cycles=scheduler_num_cycles, warmup_ratio=warmup_ratio, max_grad_norm=max_grad_norm, weight_decay=weight_decay, @@ -244,6 +401,9 @@ def create( wandb_project_name=wandb_project_name, wandb_name=wandb_name, train_on_inputs=train_on_inputs, + training_method=training_method, + dpo_beta=dpo_beta, + from_checkpoint=from_checkpoint, ) if verbose: @@ -256,12 +416,11 @@ def create( response, _, _ = requestor.request( options=TogetherRequest( method="POST", - url="/v1/fine-tunes", + url="fine-tunes", params=parameter_payload, ), stream=False, ) - assert isinstance(response, TogetherResponse) return FinetuneResponse(**response.data) @@ -281,7 +440,7 @@ def list(self) -> FinetuneList: response, _, _ = requestor.request( options=TogetherRequest( method="GET", - url="/v1/fine-tunes", + url="fine-tunes", ), stream=False, ) @@ -308,7 +467,7 @@ def retrieve(self, id: str) -> FinetuneResponse: response, _, _ = requestor.request( options=TogetherRequest( method="GET", - url=f"/v1/fine-tunes/{id}", + url=f"fine-tunes/{id}", ), stream=False, ) @@ -335,7 +494,7 @@ def cancel(self, id: str) -> FinetuneResponse: response, _, _ = requestor.request( options=TogetherRequest( method="POST", - url=f"/v1/fine-tunes/{id}/cancel", + url=f"fine-tunes/{id}/cancel", ), stream=False, ) @@ -362,21 +521,33 @@ def list_events(self, id: str) -> FinetuneListEvents: response, _, _ = requestor.request( options=TogetherRequest( method="GET", - url=f"/v1/fine-tunes/{id}/events", + url=f"fine-tunes/{id}/events", ), stream=False, ) - assert isinstance(response, TogetherResponse) return FinetuneListEvents(**response.data) + def list_checkpoints(self, id: str) -> List[FinetuneCheckpoint]: + """ + List available checkpoints for a fine-tuning job + + Args: + id (str): Unique identifier of the fine-tune job to list checkpoints for + + Returns: + List[FinetuneCheckpoint]: List of available checkpoints + """ + events = self.list_events(id).data or [] + return _process_checkpoints_from_events(events, id) + def download( self, id: str, *, output: Path | str | None = None, - checkpoint_step: int = -1, + checkpoint_step: int | None = None, checkpoint_type: DownloadCheckpointType = DownloadCheckpointType.DEFAULT, ) -> FinetuneDownloadResult: """ @@ -397,9 +568,19 @@ def download( FinetuneDownloadResult: Object containing downloaded model metadata """ + if re.match(_FT_JOB_WITH_STEP_REGEX, id) is not None: + if checkpoint_step is None: + checkpoint_step = int(id.split(":")[1]) + id = id.split(":")[0] + else: + raise ValueError( + "Fine-tuning job ID {id} contains a colon to specify the step to download, but `checkpoint_step` " + "was also set. Remove one of the step specifiers to proceed." + ) + url = f"finetune/download?ft_id={id}" - if checkpoint_step > 0: + if checkpoint_step is not None: url += f"&checkpoint_step={checkpoint_step}" ft_job = self.retrieve(id) @@ -460,7 +641,7 @@ def get_model_limits(self, *, model: str) -> FinetuneTrainingLimits: model_limits_response, _, _ = requestor.request( options=TogetherRequest( method="GET", - url="/v1/fine-tunes/models/limits", + url="fine-tunes/models/limits", params={"model_name": model}, ), stream=False, @@ -479,14 +660,16 @@ async def create( self, *, training_file: str, - model: str, + model: str | None = None, n_epochs: int = 1, validation_file: str | None = "", n_evals: int | None = 0, n_checkpoints: int | None = 1, batch_size: int | Literal["max"] = "max", learning_rate: float | None = 0.00001, + lr_scheduler_type: Literal["linear", "cosine"] = "linear", min_lr_ratio: float = 0.0, + scheduler_num_cycles: float = 0.5, warmup_ratio: float = 0.0, max_grad_norm: float = 1.0, weight_decay: float = 0.0, @@ -503,13 +686,16 @@ async def create( verbose: bool = False, model_limits: FinetuneTrainingLimits | None = None, train_on_inputs: bool | Literal["auto"] = "auto", + training_method: str = "sft", + dpo_beta: float | None = None, + from_checkpoint: str | None = None, ) -> FinetuneResponse: """ Async method to initiate a fine-tuning job Args: training_file (str): File-ID of a file uploaded to the Together API - model (str): Name of the base model to run fine-tune job on + model (str, optional): Name of the base model to run fine-tune job on n_epochs (int, optional): Number of epochs for fine-tuning. Defaults to 1. validation file (str, optional): File ID of a file uploaded to the Together API for validation. n_evals (int, optional): Number of evaluation loops to run. Defaults to 0. @@ -518,9 +704,11 @@ async def create( batch_size (int, optional): Batch size for fine-tuning. Defaults to max. learning_rate (float, optional): Learning rate multiplier to use for training Defaults to 0.00001. + lr_scheduler_type (Literal["linear", "cosine"]): Learning rate scheduler type. Defaults to "linear". min_lr_ratio (float, optional): Min learning rate ratio of the initial learning rate for the learning rate scheduler. Defaults to 0.0. - warmup_ratio (float, optional): Warmup ratio for learning rate scheduler. + scheduler_num_cycles (float, optional): Number or fraction of cycles for the cosine learning rate scheduler. Defaults to 0.5. + warmup_ratio (float, optional): Warmup ratio for the learning rate scheduler. max_grad_norm (float, optional): Max gradient norm. Defaults to 1.0, set to 0 to disable. weight_decay (float, optional): Weight decay. Defaults to 0.0. lora (bool, optional): Whether to use LoRA adapters. Defaults to True. @@ -548,6 +736,12 @@ async def create( For datasets with the "messages" field (conversational format) or "prompt" and "completion" fields (Instruction format), inputs will be masked. Defaults to "auto". + training_method (str, optional): Training method. Defaults to "sft". + Supported methods: "sft", "dpo". + dpo_beta (float, optional): DPO beta parameter. Defaults to None. + from_checkpoint (str, optional): The checkpoint identifier to continue training from a previous fine-tuning job. + The format: {$JOB_ID/$OUTPUT_MODEL_NAME}:{$STEP}. + The step value is optional, without it the final checkpoint will be used. Returns: FinetuneResponse: Object containing information about fine-tuning job. @@ -558,7 +752,15 @@ async def create( ) if model_limits is None: - model_limits = await self.get_model_limits(model=model) + # mypy doesn't understand that model or from_checkpoint is not None + if model is not None: + model_name = model + elif from_checkpoint is not None: + model_name = from_checkpoint.split(":")[0] + else: + # this branch is unreachable, but mypy doesn't know that + pass + model_limits = await self.get_model_limits(model=model_name) finetune_request = createFinetuneRequest( model_limits=model_limits, @@ -570,7 +772,9 @@ async def create( n_checkpoints=n_checkpoints, batch_size=batch_size, learning_rate=learning_rate, + lr_scheduler_type=lr_scheduler_type, min_lr_ratio=min_lr_ratio, + scheduler_num_cycles=scheduler_num_cycles, warmup_ratio=warmup_ratio, max_grad_norm=max_grad_norm, weight_decay=weight_decay, @@ -585,6 +789,9 @@ async def create( wandb_project_name=wandb_project_name, wandb_name=wandb_name, train_on_inputs=train_on_inputs, + training_method=training_method, + dpo_beta=dpo_beta, + from_checkpoint=from_checkpoint, ) if verbose: @@ -597,7 +804,7 @@ async def create( response, _, _ = await requestor.arequest( options=TogetherRequest( method="POST", - url="/v1/fine-tunes", + url="fine-tunes", params=parameter_payload, ), stream=False, @@ -622,7 +829,7 @@ async def list(self) -> FinetuneList: response, _, _ = await requestor.arequest( options=TogetherRequest( method="GET", - url="/v1/fine-tunes", + url="fine-tunes", ), stream=False, ) @@ -649,7 +856,7 @@ async def retrieve(self, id: str) -> FinetuneResponse: response, _, _ = await requestor.arequest( options=TogetherRequest( method="GET", - url=f"/v1/fine-tunes/{id}", + url=f"fine-tunes/{id}", ), stream=False, ) @@ -676,7 +883,7 @@ async def cancel(self, id: str) -> FinetuneResponse: response, _, _ = await requestor.arequest( options=TogetherRequest( method="POST", - url=f"/v1/fine-tunes/{id}/cancel", + url=f"fine-tunes/{id}/cancel", ), stream=False, ) @@ -687,30 +894,45 @@ async def cancel(self, id: str) -> FinetuneResponse: async def list_events(self, id: str) -> FinetuneListEvents: """ - Async method to lists events of a fine-tune job + List fine-tuning events Args: - id (str): Fine-tune ID to list events for. A string that starts with `ft-`. + id (str): Unique identifier of the fine-tune job to list events for Returns: - FinetuneListEvents: Object containing list of fine-tune events + FinetuneListEvents: Object containing list of fine-tune job events """ requestor = api_requestor.APIRequestor( client=self._client, ) - response, _, _ = await requestor.arequest( + events_response, _, _ = await requestor.arequest( options=TogetherRequest( method="GET", - url=f"/v1/fine-tunes/{id}/events", + url=f"fine-tunes/{normalize_key(id)}/events", ), stream=False, ) - assert isinstance(response, TogetherResponse) + # FIXME: API returns "data" field with no object type (should be "list") + events_list = FinetuneListEvents(object="list", **events_response.data) - return FinetuneListEvents(**response.data) + return events_list + + async def list_checkpoints(self, id: str) -> List[FinetuneCheckpoint]: + """ + List available checkpoints for a fine-tuning job + + Args: + id (str): Unique identifier of the fine-tune job to list checkpoints for + + Returns: + List[FinetuneCheckpoint]: Object containing list of available checkpoints + """ + events_list = await self.list_events(id) + events = events_list.data or [] + return _process_checkpoints_from_events(events, id) async def download( self, id: str, *, output: str | None = None, checkpoint_step: int = -1 @@ -742,7 +964,7 @@ async def get_model_limits(self, *, model: str) -> FinetuneTrainingLimits: model_limits_response, _, _ = await requestor.arequest( options=TogetherRequest( method="GET", - url="/v1/fine-tunes/models/limits", + url="fine-tunes/models/limits", params={"model": model}, ), stream=False, diff --git a/src/together/resources/images.py b/src/together/resources/images.py index 0612eda..5304210 100644 --- a/src/together/resources/images.py +++ b/src/together/resources/images.py @@ -76,7 +76,7 @@ def generate( response, _, _ = requestor.request( options=TogetherRequest( method="POST", - url="/v1/images/generations", + url="images/generations", params=parameter_payload, ), stream=False, @@ -151,7 +151,7 @@ async def generate( response, _, _ = await requestor.arequest( options=TogetherRequest( method="POST", - url="/v1/images/generations", + url="images/generations", params=parameter_payload, ), stream=False, diff --git a/src/together/resources/models.py b/src/together/resources/models.py index c8e1d3e..1e16c9a 100644 --- a/src/together/resources/models.py +++ b/src/together/resources/models.py @@ -11,20 +11,47 @@ ) -class Models: +class ModelsBase: def __init__(self, client: TogetherClient) -> None: self._client = client + def _filter_dedicated_models( + self, models: List[ModelObject], dedicated_response: TogetherResponse + ) -> List[ModelObject]: + """ + Filter models based on dedicated model response. + + Args: + models (List[ModelObject]): List of all models + dedicated_response (TogetherResponse): Response from autoscale models endpoint + + Returns: + List[ModelObject]: Filtered list of models + """ + assert isinstance(dedicated_response.data, list) + + # Create a set of dedicated model names for efficient lookup + dedicated_model_names = {model["name"] for model in dedicated_response.data} + + # Filter models to only include those in dedicated_model_names + # Note: The model.id from ModelObject matches the name field in the autoscale response + return [model for model in models if model.id in dedicated_model_names] + + +class Models(ModelsBase): def list( self, + dedicated: bool = False, ) -> List[ModelObject]: """ Method to return list of models on the API + Args: + dedicated (bool, optional): If True, returns only dedicated models. Defaults to False. + Returns: List[ModelObject]: List of model objects """ - requestor = api_requestor.APIRequestor( client=self._client, ) @@ -32,7 +59,7 @@ def list( response, _, _ = requestor.request( options=TogetherRequest( method="GET", - url="/v1/models", + url="models", ), stream=False, ) @@ -40,23 +67,39 @@ def list( assert isinstance(response, TogetherResponse) assert isinstance(response.data, list) - return [ModelObject(**model) for model in response.data] + models = [ModelObject(**model) for model in response.data] + if dedicated: + # Get dedicated models + dedicated_response, _, _ = requestor.request( + options=TogetherRequest( + method="GET", + url="autoscale/models", + ), + stream=False, + ) -class AsyncModels: - def __init__(self, client: TogetherClient) -> None: - self._client = client + models = self._filter_dedicated_models(models, dedicated_response) + + models.sort(key=lambda x: x.id.lower()) + return models + + +class AsyncModels(ModelsBase): async def list( self, + dedicated: bool = False, ) -> List[ModelObject]: """ Async method to return list of models on API + Args: + dedicated (bool, optional): If True, returns only dedicated models. Defaults to False. + Returns: List[ModelObject]: List of model objects """ - requestor = api_requestor.APIRequestor( client=self._client, ) @@ -64,7 +107,7 @@ async def list( response, _, _ = await requestor.arequest( options=TogetherRequest( method="GET", - url="/v1/models", + url="models", ), stream=False, ) @@ -72,4 +115,20 @@ async def list( assert isinstance(response, TogetherResponse) assert isinstance(response.data, list) - return [ModelObject(**model) for model in response.data] + models = [ModelObject(**model) for model in response.data] + + if dedicated: + # Get dedicated models + dedicated_response, _, _ = await requestor.arequest( + options=TogetherRequest( + method="GET", + url="autoscale/models", + ), + stream=False, + ) + + models = self._filter_dedicated_models(models, dedicated_response) + + models.sort(key=lambda x: x.id.lower()) + + return models diff --git a/src/together/resources/rerank.py b/src/together/resources/rerank.py index a57a91c..6c384ea 100644 --- a/src/together/resources/rerank.py +++ b/src/together/resources/rerank.py @@ -59,7 +59,7 @@ def create( response, _, _ = requestor.request( options=TogetherRequest( method="POST", - url="/v1/rerank", + url="rerank", params=parameter_payload, ), stream=False, @@ -117,7 +117,7 @@ async def create( response, _, _ = await requestor.arequest( options=TogetherRequest( method="POST", - url="/v1/rerank", + url="rerank", params=parameter_payload, ), stream=False, diff --git a/tests/unit/test_async_client.py b/tests/unit/test_async_client.py index 858414a..0b11b39 100644 --- a/tests/unit/test_async_client.py +++ b/tests/unit/test_async_client.py @@ -50,7 +50,7 @@ def test_init_with_default_base_url(self): async_together = AsyncTogether(api_key="fake_api_key") - assert async_together.client.base_url == "https://api.together.xyz/" + assert async_together.client.base_url == "https://api.together.xyz/v1/" def test_init_with_supplied_headers(self): """ diff --git a/tests/unit/test_client.py b/tests/unit/test_client.py index 827cae9..f8bdcbe 100644 --- a/tests/unit/test_client.py +++ b/tests/unit/test_client.py @@ -51,7 +51,7 @@ def test_init_with_default_base_url(self): with patch.dict("os.environ", clear=True): sync_together = Together(api_key="fake_api_key") - assert sync_together.client.base_url == "https://api.together.xyz/" + assert sync_together.client.base_url == "https://api.together.xyz/v1/" def test_init_with_supplied_headers(self): """ From f0034c833403665de4d0e89595e206c9020e687c Mon Sep 17 00:00:00 2001 From: Ives van Hoorne Date: Thu, 27 Mar 2025 17:24:22 -0700 Subject: [PATCH 4/7] fix tests --- tests/unit/test_code_interpreter.py | 203 ++++++++++++++++++++++++++-- 1 file changed, 193 insertions(+), 10 deletions(-) diff --git a/tests/unit/test_code_interpreter.py b/tests/unit/test_code_interpreter.py index da95b5a..525a2da 100644 --- a/tests/unit/test_code_interpreter.py +++ b/tests/unit/test_code_interpreter.py @@ -1,19 +1,32 @@ from __future__ import annotations -import pytest from together.resources.code_interpreter import CodeInterpreter from together.together_response import TogetherResponse -from together.types.code_interpreter import ExecuteResponse, ExecuteResponseData, InterpreterOutput +from together.types.code_interpreter import ( + ExecuteResponse, + ExecuteResponseData, + InterpreterOutput, +) def test_interpreter_output_validation(): - # Test valid stdout output + # Test stdout output stdout = InterpreterOutput(type="stdout", data="Hello, world!") assert stdout.type == "stdout" assert stdout.data == "Hello, world!" - # Test valid display_data output + # Test stderr output + stderr = InterpreterOutput(type="stderr", data="Warning message") + assert stderr.type == "stderr" + assert stderr.data == "Warning message" + + # Test error output + error = InterpreterOutput(type="error", data="Error occurred") + assert error.type == "error" + assert error.data == "Error occurred" + + # Test display_data output with dict data display_data = InterpreterOutput( type="display_data", data={ @@ -22,11 +35,14 @@ def test_interpreter_output_validation(): }, ) assert display_data.type == "display_data" - assert display_data.data["text/plain"] == "Hello" + assert isinstance(display_data.data, dict) + assert display_data.data.get("text/plain") == "Hello" + assert display_data.data.get("text/html") == "

Hello

" - # Test invalid type - with pytest.raises(ValueError): - InterpreterOutput(type="invalid", data="test") + # Test execute_result output + execute_result = InterpreterOutput(type="execute_result", data="42") + assert execute_result.type == "execute_result" + assert execute_result.data == "42" def test_execute_response_validation(): @@ -67,7 +83,9 @@ def test_code_interpreter_run(mocker): } mock_response = TogetherResponse(data=response_data, headers=mock_headers) mock_requestor.request.return_value = (mock_response, None, None) - mocker.patch("together.abstract.api_requestor.APIRequestor", return_value=mock_requestor) + mocker.patch( + "together.abstract.api_requestor.APIRequestor", return_value=mock_requestor + ) # Create code interpreter instance client = mocker.MagicMock() @@ -121,7 +139,9 @@ def test_code_interpreter_run_without_session(mocker): } mock_response = TogetherResponse(data=response_data, headers=mock_headers) mock_requestor.request.return_value = (mock_response, None, None) - mocker.patch("together.abstract.api_requestor.APIRequestor", return_value=mock_requestor) + mocker.patch( + "together.abstract.api_requestor.APIRequestor", return_value=mock_requestor + ) # Create code interpreter instance client = mocker.MagicMock() @@ -143,3 +163,166 @@ def test_code_interpreter_run_without_session(mocker): "code": "x = 1", "language": "python", } + + +def test_code_interpreter_error_handling(mocker): + # Mock the API requestor to simulate an error + mock_requestor = mocker.MagicMock() + response_data = { + "data": { + "session_id": "test_session", + "status": "error", + "outputs": [{"type": "error", "data": "Division by zero"}], + } + } + mock_headers = { + "cf-ray": "test-ray-id", + "x-ratelimit-remaining": "100", + "x-hostname": "test-host", + "x-total-time": "42.0", + } + mock_response = TogetherResponse(data=response_data, headers=mock_headers) + mock_requestor.request.return_value = (mock_response, None, None) + mocker.patch( + "together.abstract.api_requestor.APIRequestor", return_value=mock_requestor + ) + + # Create code interpreter instance + client = mocker.MagicMock() + interpreter = CodeInterpreter(client) + + # Test run method with code that would cause an error + response = interpreter.run( + code="1/0", # This will cause a division by zero error + language="python", + session_id="test_session", + ) + + # Verify the error response + assert isinstance(response, ExecuteResponse) + assert response.data.status == "error" + assert len(response.data.outputs) == 1 + assert response.data.outputs[0].type == "error" + assert "Division by zero" in response.data.outputs[0].data + + +def test_code_interpreter_multiple_outputs(mocker): + # Mock the API requestor + mock_requestor = mocker.MagicMock() + response_data = { + "data": { + "session_id": "test_session", + "status": "success", + "outputs": [ + {"type": "stdout", "data": "First line"}, + {"type": "stderr", "data": "Warning message"}, + {"type": "execute_result", "data": "42"}, + ], + } + } + mock_headers = { + "cf-ray": "test-ray-id", + "x-ratelimit-remaining": "100", + "x-hostname": "test-host", + "x-total-time": "42.0", + } + mock_response = TogetherResponse(data=response_data, headers=mock_headers) + mock_requestor.request.return_value = (mock_response, None, None) + mocker.patch( + "together.abstract.api_requestor.APIRequestor", return_value=mock_requestor + ) + + # Create code interpreter instance + client = mocker.MagicMock() + interpreter = CodeInterpreter(client) + + # Test run method with code that produces multiple outputs + response = interpreter.run( + code='print("First line")\nimport sys\nsys.stderr.write("Warning message")\n42', + language="python", + session_id="test_session", + ) + + # Verify the response with multiple outputs + assert isinstance(response, ExecuteResponse) + assert response.data.status == "success" + assert len(response.data.outputs) == 3 + assert response.data.outputs[0].type == "stdout" + assert response.data.outputs[1].type == "stderr" + assert response.data.outputs[2].type == "execute_result" + + +def test_code_interpreter_session_management(mocker): + # Mock the API requestor + mock_requestor = mocker.MagicMock() + + # First response - create new session + response_data1 = { + "data": { + "session_id": "new_session", + "status": "success", + "outputs": [{"type": "stdout", "data": "First execution"}], + } + } + + # Second response - use existing session + response_data2 = { + "data": { + "session_id": "new_session", + "status": "success", + "outputs": [{"type": "stdout", "data": "Second execution"}], + } + } + + mock_headers = { + "cf-ray": "test-ray-id", + "x-ratelimit-remaining": "100", + "x-hostname": "test-host", + "x-total-time": "42.0", + } + + mock_response1 = TogetherResponse(data=response_data1, headers=mock_headers) + mock_response2 = TogetherResponse(data=response_data2, headers=mock_headers) + mock_requestor.request.side_effect = [ + (mock_response1, None, None), + (mock_response2, None, None), + ] + + mocker.patch( + "together.abstract.api_requestor.APIRequestor", return_value=mock_requestor + ) + + # Create code interpreter instance + client = mocker.MagicMock() + interpreter = CodeInterpreter(client) + + # First execution - no session ID + response1 = interpreter.run( + code='print("First execution")', + language="python", + ) + + # Second execution - using session ID from first execution + response2 = interpreter.run( + code='print("Second execution")', + language="python", + session_id=response1.data.session_id, + ) + + # Verify both responses + assert response1.data.session_id == "new_session" + assert response2.data.session_id == "new_session" + assert len(response1.data.outputs) == 1 + assert len(response2.data.outputs) == 1 + assert response1.data.outputs[0].data == "First execution" + assert response2.data.outputs[0].data == "Second execution" + + # Verify API calls + assert mock_requestor.request.call_count == 2 + calls = mock_requestor.request.call_args_list + + # First call should not have session_id + assert "session_id" not in calls[0][1]["options"].params + + # Second call should have session_id + assert calls[1][1]["options"].params["session_id"] == "new_session" From 6727c4d90569177cab392603825cf5368e9c2b70 Mon Sep 17 00:00:00 2001 From: Ives van Hoorne Date: Thu, 27 Mar 2025 17:27:01 -0700 Subject: [PATCH 5/7] black --- examples/code_interpreter_demo.py | 23 ++++++----------------- src/together/types/code_interpreter.py | 18 ++++++++---------- 2 files changed, 14 insertions(+), 27 deletions(-) diff --git a/examples/code_interpreter_demo.py b/examples/code_interpreter_demo.py index 191b2f1..ac4f705 100644 --- a/examples/code_interpreter_demo.py +++ b/examples/code_interpreter_demo.py @@ -7,10 +7,7 @@ # Example 1: Simple print statement print("Example 1: Simple print") -response = code_interpreter.run( - code='print("Hello from Together!")', - language="python" -) +response = code_interpreter.run(code='print("Hello from Together!")', language="python") print(f"Status: {response.data.status}") for output in response.data.outputs: print(f"{output.type}: {output.data}") @@ -20,16 +17,11 @@ # Example 2: Using session for maintaining state print("Example 2: Using session for state") -response1 = code_interpreter.run( - code='x = 42', - language="python" -) +response1 = code_interpreter.run(code="x = 42", language="python") session_id = response1.data.session_id response2 = code_interpreter.run( - code='print(f"The value of x is {x}")', - language="python", - session_id=session_id + code='print(f"The value of x is {x}")', language="python", session_id=session_id ) for output in response2.data.outputs: print(f"{output.type}: {output.data}") @@ -39,7 +31,7 @@ # Example 3: More complex computation print("Example 3: Complex computation") -code = ''' +code = """ !pip install numpy import numpy as np @@ -52,12 +44,9 @@ eigenvalues = np.linalg.eigvals(matrix) print("\\nEigenvalues:") print(eigenvalues) -''' +""" -response = code_interpreter.run( - code=code, - language="python" -) +response = code_interpreter.run(code=code, language="python") for output in response.data.outputs: print(f"{output.type}: {output.data}") if response.data.errors: diff --git a/src/together/types/code_interpreter.py b/src/together/types/code_interpreter.py index ead0288..6f960f7 100644 --- a/src/together/types/code_interpreter.py +++ b/src/together/types/code_interpreter.py @@ -9,33 +9,31 @@ class InterpreterOutput(TogetherJSONModel): """Base class for interpreter output types.""" - type: Literal["stdout", "stderr", "error", "display_data", "execute_result"] = Field( - description="The type of output" + + type: Literal["stdout", "stderr", "error", "display_data", "execute_result"] = ( + Field(description="The type of output") ) data: Union[str, Dict[str, Any]] = Field(description="The output data") class ExecuteResponseData(TogetherJSONModel): """Data from code execution response.""" + outputs: list[InterpreterOutput] = Field( - description="List of outputs from execution", - default_factory=list + description="List of outputs from execution", default_factory=list ) errors: Union[str, None] = Field( - description="Any errors that occurred during execution", - default=None + description="Any errors that occurred during execution", default=None ) session_id: str = Field( description="Identifier of the current session. Used to make follow-up calls." ) - status: str = Field( - description="Status of the execution", - default="completed" - ) + status: str = Field(description="Status of the execution", default="completed") class ExecuteResponse(TogetherJSONModel): """Response from code execution.""" + data: ExecuteResponseData = Field( description="The response data containing outputs and session information" ) From 1ea63571041b18497d23748ea635ed43c54a9aeb Mon Sep 17 00:00:00 2001 From: Ives van Hoorne Date: Thu, 27 Mar 2025 17:27:54 -0700 Subject: [PATCH 6/7] add pytest mock --- pyproject.toml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 2b79f58..7d0b01d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,9 +13,7 @@ build-backend = "poetry.masonry.api" [tool.poetry] name = "together" version = "1.5.3" -authors = [ - "Together AI " -] +authors = ["Together AI "] description = "Python client for Together's Cloud Platform!" readme = "README.md" license = "Apache-2.0" @@ -65,6 +63,7 @@ optional = true [tool.poetry.group.tests.dependencies] pytest = ">=7.4.2,<9.0.0" pytest-watch = "^4.2.0" +pytest-mock = "^4.0.0" tox = "^4.14.1" [tool.poetry.group.examples] From bcadb36835a9c8c5d69675bb2c90655310edc935 Mon Sep 17 00:00:00 2001 From: Ives van Hoorne Date: Thu, 27 Mar 2025 17:28:58 -0700 Subject: [PATCH 7/7] add pytest mock --- poetry.lock | 20 +++++++++++++++++++- pyproject.toml | 2 +- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/poetry.lock b/poetry.lock index ec39219..d4585fb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1669,6 +1669,24 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-mock" +version = "3.14.0" +description = "Thin-wrapper around the mock package for easier use with pytest" +optional = false +python-versions = ">=3.8" +groups = ["tests"] +files = [ + {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, + {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, +] + +[package.dependencies] +pytest = ">=6.2.5" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + [[package]] name = "pytest-watch" version = "4.2.0" @@ -2623,4 +2641,4 @@ propcache = ">=0.2.0" [metadata] lock-version = "2.1" python-versions = "^3.10" -content-hash = "aa3f2327e6f33fcb7224b089f4319728cf800c8d71913e4851d817403a2484e4" +content-hash = "a5e8c66dcdc0cbb934e23aa5fad38b0790b8e83f1242d6d6c49538011f017e06" diff --git a/pyproject.toml b/pyproject.toml index 7d0b01d..ea29d8c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,7 +63,7 @@ optional = true [tool.poetry.group.tests.dependencies] pytest = ">=7.4.2,<9.0.0" pytest-watch = "^4.2.0" -pytest-mock = "^4.0.0" +pytest-mock = "^3.14.0" tox = "^4.14.1" [tool.poetry.group.examples]