together 1.5.29__py3-none-any.whl → 1.5.31__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- together/abstract/api_requestor.py +44 -3
- together/cli/api/chat.py +24 -2
- together/cli/api/endpoints.py +56 -6
- together/constants.py +3 -3
- together/filemanager.py +45 -22
- together/resources/audio/__init__.py +9 -0
- together/resources/audio/speech.py +8 -2
- together/resources/audio/transcriptions.py +20 -2
- together/resources/audio/voices.py +65 -0
- together/resources/endpoints.py +98 -7
- together/types/__init__.py +4 -0
- together/types/audio_speech.py +127 -14
- together/types/chat_completions.py +6 -0
- together/types/common.py +1 -0
- together/types/files.py +1 -0
- together/utils/files.py +183 -54
- {together-1.5.29.dist-info → together-1.5.31.dist-info}/METADATA +2 -1
- {together-1.5.29.dist-info → together-1.5.31.dist-info}/RECORD +21 -20
- {together-1.5.29.dist-info → together-1.5.31.dist-info}/WHEEL +0 -0
- {together-1.5.29.dist-info → together-1.5.31.dist-info}/entry_points.txt +0 -0
- {together-1.5.29.dist-info → together-1.5.31.dist-info}/licenses/LICENSE +0 -0
together/resources/endpoints.py
CHANGED
|
@@ -13,13 +13,18 @@ class Endpoints:
|
|
|
13
13
|
self._client = client
|
|
14
14
|
|
|
15
15
|
def list(
|
|
16
|
-
self,
|
|
16
|
+
self,
|
|
17
|
+
type: Optional[Literal["dedicated", "serverless"]] = None,
|
|
18
|
+
usage_type: Optional[Literal["on-demand", "reserved"]] = None,
|
|
19
|
+
mine: Optional[bool] = None,
|
|
17
20
|
) -> List[ListEndpoint]:
|
|
18
21
|
"""
|
|
19
|
-
List all endpoints, can be filtered by type.
|
|
22
|
+
List all endpoints, can be filtered by endpoint type and ownership.
|
|
20
23
|
|
|
21
24
|
Args:
|
|
22
|
-
type (str, optional): Filter endpoints by type ("dedicated" or "serverless"). Defaults to None.
|
|
25
|
+
type (str, optional): Filter endpoints by endpoint type ("dedicated" or "serverless"). Defaults to None.
|
|
26
|
+
usage_type (str, optional): Filter endpoints by usage type ("on-demand" or "reserved"). Defaults to None.
|
|
27
|
+
mine (bool, optional): If True, return only endpoints owned by the caller. Defaults to None.
|
|
23
28
|
|
|
24
29
|
Returns:
|
|
25
30
|
List[ListEndpoint]: List of endpoint objects
|
|
@@ -28,9 +33,20 @@ class Endpoints:
|
|
|
28
33
|
client=self._client,
|
|
29
34
|
)
|
|
30
35
|
|
|
31
|
-
params
|
|
36
|
+
params: Dict[
|
|
37
|
+
str,
|
|
38
|
+
Union[
|
|
39
|
+
Literal["dedicated", "serverless"],
|
|
40
|
+
Literal["on-demand", "reserved"],
|
|
41
|
+
bool,
|
|
42
|
+
],
|
|
43
|
+
] = {}
|
|
32
44
|
if type is not None:
|
|
33
45
|
params["type"] = type
|
|
46
|
+
if usage_type is not None:
|
|
47
|
+
params["usage_type"] = usage_type
|
|
48
|
+
if mine is not None:
|
|
49
|
+
params["mine"] = mine
|
|
34
50
|
|
|
35
51
|
response, _, _ = requestor.request(
|
|
36
52
|
options=TogetherRequest(
|
|
@@ -60,6 +76,7 @@ class Endpoints:
|
|
|
60
76
|
disable_speculative_decoding: bool = True,
|
|
61
77
|
state: Literal["STARTED", "STOPPED"] = "STARTED",
|
|
62
78
|
inactive_timeout: Optional[int] = None,
|
|
79
|
+
availability_zone: Optional[str] = None,
|
|
63
80
|
) -> DedicatedEndpoint:
|
|
64
81
|
"""
|
|
65
82
|
Create a new dedicated endpoint.
|
|
@@ -74,6 +91,7 @@ class Endpoints:
|
|
|
74
91
|
disable_speculative_decoding (bool, optional): Whether to disable speculative decoding. Defaults to False.
|
|
75
92
|
state (str, optional): The desired state of the endpoint. Defaults to "STARTED".
|
|
76
93
|
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.
|
|
94
|
+
availability_zone (str, optional): Start endpoint in specified availability zone (e.g., us-central-4b).
|
|
77
95
|
|
|
78
96
|
Returns:
|
|
79
97
|
DedicatedEndpoint: Object containing endpoint information
|
|
@@ -100,6 +118,9 @@ class Endpoints:
|
|
|
100
118
|
if inactive_timeout is not None:
|
|
101
119
|
data["inactive_timeout"] = inactive_timeout
|
|
102
120
|
|
|
121
|
+
if availability_zone is not None:
|
|
122
|
+
data["availability_zone"] = availability_zone
|
|
123
|
+
|
|
103
124
|
response, _, _ = requestor.request(
|
|
104
125
|
options=TogetherRequest(
|
|
105
126
|
method="POST",
|
|
@@ -257,19 +278,49 @@ class Endpoints:
|
|
|
257
278
|
|
|
258
279
|
return [HardwareWithStatus(**item) for item in response.data["data"]]
|
|
259
280
|
|
|
281
|
+
def list_avzones(self) -> List[str]:
|
|
282
|
+
"""
|
|
283
|
+
List all available availability zones.
|
|
284
|
+
|
|
285
|
+
Returns:
|
|
286
|
+
List[str]: List of unique availability zones
|
|
287
|
+
"""
|
|
288
|
+
requestor = api_requestor.APIRequestor(
|
|
289
|
+
client=self._client,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
response, _, _ = requestor.request(
|
|
293
|
+
options=TogetherRequest(
|
|
294
|
+
method="GET",
|
|
295
|
+
url="clusters/availability-zones",
|
|
296
|
+
),
|
|
297
|
+
stream=False,
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
assert isinstance(response, TogetherResponse)
|
|
301
|
+
assert isinstance(response.data, dict)
|
|
302
|
+
assert isinstance(response.data["avzones"], list)
|
|
303
|
+
|
|
304
|
+
return response.data["avzones"]
|
|
305
|
+
|
|
260
306
|
|
|
261
307
|
class AsyncEndpoints:
|
|
262
308
|
def __init__(self, client: TogetherClient) -> None:
|
|
263
309
|
self._client = client
|
|
264
310
|
|
|
265
311
|
async def list(
|
|
266
|
-
self,
|
|
312
|
+
self,
|
|
313
|
+
type: Optional[Literal["dedicated", "serverless"]] = None,
|
|
314
|
+
usage_type: Optional[Literal["on-demand", "reserved"]] = None,
|
|
315
|
+
mine: Optional[bool] = None,
|
|
267
316
|
) -> List[ListEndpoint]:
|
|
268
317
|
"""
|
|
269
|
-
List all endpoints, can be filtered by type.
|
|
318
|
+
List all endpoints, can be filtered by type and ownership.
|
|
270
319
|
|
|
271
320
|
Args:
|
|
272
321
|
type (str, optional): Filter endpoints by type ("dedicated" or "serverless"). Defaults to None.
|
|
322
|
+
usage_type (str, optional): Filter endpoints by usage type ("on-demand" or "reserved"). Defaults to None.
|
|
323
|
+
mine (bool, optional): If True, return only endpoints owned by the caller. Defaults to None.
|
|
273
324
|
|
|
274
325
|
Returns:
|
|
275
326
|
List[ListEndpoint]: List of endpoint objects
|
|
@@ -278,9 +329,20 @@ class AsyncEndpoints:
|
|
|
278
329
|
client=self._client,
|
|
279
330
|
)
|
|
280
331
|
|
|
281
|
-
params
|
|
332
|
+
params: Dict[
|
|
333
|
+
str,
|
|
334
|
+
Union[
|
|
335
|
+
Literal["dedicated", "serverless"],
|
|
336
|
+
Literal["on-demand", "reserved"],
|
|
337
|
+
bool,
|
|
338
|
+
],
|
|
339
|
+
] = {}
|
|
282
340
|
if type is not None:
|
|
283
341
|
params["type"] = type
|
|
342
|
+
if usage_type is not None:
|
|
343
|
+
params["usage_type"] = usage_type
|
|
344
|
+
if mine is not None:
|
|
345
|
+
params["mine"] = mine
|
|
284
346
|
|
|
285
347
|
response, _, _ = await requestor.arequest(
|
|
286
348
|
options=TogetherRequest(
|
|
@@ -308,6 +370,7 @@ class AsyncEndpoints:
|
|
|
308
370
|
disable_speculative_decoding: bool = True,
|
|
309
371
|
state: Literal["STARTED", "STOPPED"] = "STARTED",
|
|
310
372
|
inactive_timeout: Optional[int] = None,
|
|
373
|
+
availability_zone: Optional[str] = None,
|
|
311
374
|
) -> DedicatedEndpoint:
|
|
312
375
|
"""
|
|
313
376
|
Create a new dedicated endpoint.
|
|
@@ -348,6 +411,9 @@ class AsyncEndpoints:
|
|
|
348
411
|
if inactive_timeout is not None:
|
|
349
412
|
data["inactive_timeout"] = inactive_timeout
|
|
350
413
|
|
|
414
|
+
if availability_zone is not None:
|
|
415
|
+
data["availability_zone"] = availability_zone
|
|
416
|
+
|
|
351
417
|
response, _, _ = await requestor.arequest(
|
|
352
418
|
options=TogetherRequest(
|
|
353
419
|
method="POST",
|
|
@@ -506,3 +572,28 @@ class AsyncEndpoints:
|
|
|
506
572
|
assert isinstance(response.data["data"], list)
|
|
507
573
|
|
|
508
574
|
return [HardwareWithStatus(**item) for item in response.data["data"]]
|
|
575
|
+
|
|
576
|
+
async def list_avzones(self) -> List[str]:
|
|
577
|
+
"""
|
|
578
|
+
List all availability zones.
|
|
579
|
+
|
|
580
|
+
Returns:
|
|
581
|
+
List[str]: List of unique availability zones
|
|
582
|
+
"""
|
|
583
|
+
requestor = api_requestor.APIRequestor(
|
|
584
|
+
client=self._client,
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
response, _, _ = await requestor.arequest(
|
|
588
|
+
options=TogetherRequest(
|
|
589
|
+
method="GET",
|
|
590
|
+
url="clusters/availability-zones",
|
|
591
|
+
),
|
|
592
|
+
stream=False,
|
|
593
|
+
)
|
|
594
|
+
|
|
595
|
+
assert isinstance(response, TogetherResponse)
|
|
596
|
+
assert isinstance(response.data, dict)
|
|
597
|
+
assert isinstance(response.data["avzones"], list)
|
|
598
|
+
|
|
599
|
+
return response.data["avzones"]
|
together/types/__init__.py
CHANGED
|
@@ -15,6 +15,8 @@ from together.types.audio_speech import (
|
|
|
15
15
|
AudioTranslationVerboseResponse,
|
|
16
16
|
AudioTranscriptionResponseFormat,
|
|
17
17
|
AudioTimestampGranularities,
|
|
18
|
+
ModelVoices,
|
|
19
|
+
VoiceListResponse,
|
|
18
20
|
)
|
|
19
21
|
from together.types.chat_completions import (
|
|
20
22
|
ChatCompletionChunk,
|
|
@@ -140,6 +142,8 @@ __all__ = [
|
|
|
140
142
|
"AudioTranslationVerboseResponse",
|
|
141
143
|
"AudioTranscriptionResponseFormat",
|
|
142
144
|
"AudioTimestampGranularities",
|
|
145
|
+
"ModelVoices",
|
|
146
|
+
"VoiceListResponse",
|
|
143
147
|
"DedicatedEndpoint",
|
|
144
148
|
"ListEndpoint",
|
|
145
149
|
"Autoscaling",
|
together/types/audio_speech.py
CHANGED
|
@@ -2,7 +2,8 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
import base64
|
|
4
4
|
from enum import Enum
|
|
5
|
-
from
|
|
5
|
+
from re import S
|
|
6
|
+
from typing import BinaryIO, Dict, Iterator, List, Optional, Union
|
|
6
7
|
|
|
7
8
|
from pydantic import BaseModel, ConfigDict
|
|
8
9
|
|
|
@@ -82,27 +83,126 @@ class AudioSpeechStreamResponse(BaseModel):
|
|
|
82
83
|
|
|
83
84
|
model_config = ConfigDict(arbitrary_types_allowed=True)
|
|
84
85
|
|
|
85
|
-
def stream_to_file(
|
|
86
|
+
def stream_to_file(
|
|
87
|
+
self, file_path: str, response_format: AudioResponseFormat | str | None = None
|
|
88
|
+
) -> None:
|
|
89
|
+
"""
|
|
90
|
+
Save the audio response to a file.
|
|
91
|
+
|
|
92
|
+
For non-streaming responses, writes the complete file as received.
|
|
93
|
+
For streaming responses, collects binary chunks and constructs a valid
|
|
94
|
+
file format based on the response_format parameter.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
file_path: Path where the audio file should be saved.
|
|
98
|
+
response_format: Format of the audio (wav, mp3, or raw). If not provided,
|
|
99
|
+
will attempt to infer from file extension or default to wav.
|
|
100
|
+
"""
|
|
101
|
+
# Determine response format
|
|
102
|
+
if response_format is None:
|
|
103
|
+
# Infer from file extension
|
|
104
|
+
ext = file_path.lower().split(".")[-1] if "." in file_path else ""
|
|
105
|
+
if ext in ["wav"]:
|
|
106
|
+
response_format = AudioResponseFormat.WAV
|
|
107
|
+
elif ext in ["mp3", "mpeg"]:
|
|
108
|
+
response_format = AudioResponseFormat.MP3
|
|
109
|
+
elif ext in ["raw", "pcm"]:
|
|
110
|
+
response_format = AudioResponseFormat.RAW
|
|
111
|
+
else:
|
|
112
|
+
# Default to WAV if unknown
|
|
113
|
+
response_format = AudioResponseFormat.WAV
|
|
114
|
+
|
|
115
|
+
if isinstance(response_format, str):
|
|
116
|
+
response_format = AudioResponseFormat(response_format)
|
|
117
|
+
|
|
86
118
|
if isinstance(self.response, TogetherResponse):
|
|
87
|
-
# save
|
|
119
|
+
# Non-streaming: save complete file
|
|
88
120
|
with open(file_path, "wb") as f:
|
|
89
121
|
f.write(self.response.data)
|
|
90
122
|
|
|
91
123
|
elif isinstance(self.response, Iterator):
|
|
124
|
+
# Streaming: collect binary chunks
|
|
125
|
+
audio_chunks = []
|
|
126
|
+
for chunk in self.response:
|
|
127
|
+
if isinstance(chunk.data, bytes):
|
|
128
|
+
audio_chunks.append(chunk.data)
|
|
129
|
+
elif isinstance(chunk.data, dict):
|
|
130
|
+
# SSE format with JSON/base64
|
|
131
|
+
try:
|
|
132
|
+
stream_event = AudioSpeechStreamEventResponse(
|
|
133
|
+
response={"data": chunk.data}
|
|
134
|
+
)
|
|
135
|
+
if isinstance(stream_event.response, StreamSentinel):
|
|
136
|
+
break
|
|
137
|
+
audio_chunks.append(
|
|
138
|
+
base64.b64decode(stream_event.response.data.b64)
|
|
139
|
+
)
|
|
140
|
+
except Exception:
|
|
141
|
+
continue # Skip malformed chunks
|
|
142
|
+
|
|
143
|
+
if not audio_chunks:
|
|
144
|
+
raise ValueError("No audio data received in streaming response")
|
|
145
|
+
|
|
146
|
+
# Concatenate all chunks
|
|
147
|
+
audio_data = b"".join(audio_chunks)
|
|
148
|
+
|
|
92
149
|
with open(file_path, "wb") as f:
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
150
|
+
if response_format == AudioResponseFormat.WAV:
|
|
151
|
+
if audio_data.startswith(b"RIFF"):
|
|
152
|
+
# Already a valid WAV file
|
|
153
|
+
f.write(audio_data)
|
|
154
|
+
else:
|
|
155
|
+
# Raw PCM - add WAV header
|
|
156
|
+
self._write_wav_header(f, audio_data)
|
|
157
|
+
elif response_format == AudioResponseFormat.MP3:
|
|
158
|
+
# MP3 format: Check if data is actually MP3 or raw PCM
|
|
159
|
+
# MP3 files start with ID3 tag or sync word (0xFF 0xFB/0xFA/0xF3/0xF2)
|
|
160
|
+
is_mp3 = audio_data.startswith(b"ID3") or (
|
|
161
|
+
len(audio_data) > 0
|
|
162
|
+
and audio_data[0:1] == b"\xff"
|
|
163
|
+
and len(audio_data) > 1
|
|
164
|
+
and audio_data[1] & 0xE0 == 0xE0
|
|
97
165
|
)
|
|
98
166
|
|
|
99
|
-
if
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
f.write(
|
|
167
|
+
if is_mp3:
|
|
168
|
+
f.write(audio_data)
|
|
169
|
+
else:
|
|
170
|
+
raise ValueError("Invalid MP3 data received.")
|
|
171
|
+
else:
|
|
172
|
+
# RAW format: write PCM data as-is
|
|
173
|
+
f.write(audio_data)
|
|
174
|
+
|
|
175
|
+
@staticmethod
|
|
176
|
+
def _write_wav_header(file_handle: BinaryIO, audio_data: bytes) -> None:
|
|
177
|
+
"""
|
|
178
|
+
Write WAV file header for raw PCM audio data.
|
|
179
|
+
|
|
180
|
+
Uses default TTS parameters: 16-bit PCM, mono, 24000 Hz sample rate.
|
|
181
|
+
"""
|
|
182
|
+
import struct
|
|
183
|
+
|
|
184
|
+
sample_rate = 24000
|
|
185
|
+
num_channels = 1
|
|
186
|
+
bits_per_sample = 16
|
|
187
|
+
byte_rate = sample_rate * num_channels * bits_per_sample // 8
|
|
188
|
+
block_align = num_channels * bits_per_sample // 8
|
|
189
|
+
data_size = len(audio_data)
|
|
190
|
+
|
|
191
|
+
# Write WAV header
|
|
192
|
+
file_handle.write(b"RIFF")
|
|
193
|
+
file_handle.write(struct.pack("<I", 36 + data_size)) # File size - 8
|
|
194
|
+
file_handle.write(b"WAVE")
|
|
195
|
+
file_handle.write(b"fmt ")
|
|
196
|
+
file_handle.write(struct.pack("<I", 16)) # fmt chunk size
|
|
197
|
+
file_handle.write(struct.pack("<H", 1)) # Audio format (1 = PCM)
|
|
198
|
+
file_handle.write(struct.pack("<H", num_channels))
|
|
199
|
+
file_handle.write(struct.pack("<I", sample_rate))
|
|
200
|
+
file_handle.write(struct.pack("<I", byte_rate))
|
|
201
|
+
file_handle.write(struct.pack("<H", block_align))
|
|
202
|
+
file_handle.write(struct.pack("<H", bits_per_sample))
|
|
203
|
+
file_handle.write(b"data")
|
|
204
|
+
file_handle.write(struct.pack("<I", data_size))
|
|
205
|
+
file_handle.write(audio_data)
|
|
106
206
|
|
|
107
207
|
|
|
108
208
|
class AudioTranscriptionResponseFormat(str, Enum):
|
|
@@ -196,3 +296,16 @@ class AudioTranslationVerboseResponse(BaseModel):
|
|
|
196
296
|
text: str
|
|
197
297
|
segments: Optional[List[AudioTranscriptionSegment]] = None
|
|
198
298
|
words: Optional[List[AudioTranscriptionWord]] = None
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
class ModelVoices(BaseModel):
|
|
302
|
+
"""Represents a model with its available voices."""
|
|
303
|
+
|
|
304
|
+
model: str
|
|
305
|
+
voices: List[Dict[str, str]] # Each voice is a dict with 'name' key
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
class VoiceListResponse(BaseModel):
|
|
309
|
+
"""Response containing a list of models and their available voices."""
|
|
310
|
+
|
|
311
|
+
data: List[ModelVoices]
|
|
@@ -46,6 +46,7 @@ class ChatCompletionMessageContentType(str, Enum):
|
|
|
46
46
|
TEXT = "text"
|
|
47
47
|
IMAGE_URL = "image_url"
|
|
48
48
|
VIDEO_URL = "video_url"
|
|
49
|
+
AUDIO_URL = "audio_url"
|
|
49
50
|
|
|
50
51
|
|
|
51
52
|
class ChatCompletionMessageContentImageURL(BaseModel):
|
|
@@ -56,11 +57,16 @@ class ChatCompletionMessageContentVideoURL(BaseModel):
|
|
|
56
57
|
url: str
|
|
57
58
|
|
|
58
59
|
|
|
60
|
+
class ChatCompletionMessageContentAudioURL(BaseModel):
|
|
61
|
+
url: str
|
|
62
|
+
|
|
63
|
+
|
|
59
64
|
class ChatCompletionMessageContent(BaseModel):
|
|
60
65
|
type: ChatCompletionMessageContentType
|
|
61
66
|
text: str | None = None
|
|
62
67
|
image_url: ChatCompletionMessageContentImageURL | None = None
|
|
63
68
|
video_url: ChatCompletionMessageContentVideoURL | None = None
|
|
69
|
+
audio_url: ChatCompletionMessageContentAudioURL | None = None
|
|
64
70
|
|
|
65
71
|
|
|
66
72
|
class ChatCompletionMessage(BaseModel):
|
together/types/common.py
CHANGED