together 2.0.0a11__py3-none-any.whl → 2.0.0a12__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/_base_client.py CHANGED
@@ -1774,7 +1774,7 @@ class AsyncAPIClient(BaseClient[httpx.AsyncClient, AsyncStream[Any]]):
1774
1774
  options: RequestOptions = {},
1775
1775
  ) -> ResponseT:
1776
1776
  opts = FinalRequestOptions.construct(
1777
- method="patch", url=path, json_data=body, files=to_httpx_files(files), **options
1777
+ method="patch", url=path, json_data=body, files=await async_to_httpx_files(files), **options
1778
1778
  )
1779
1779
  return await self.request(cast_to, opts)
1780
1780
 
together/_version.py CHANGED
@@ -1,4 +1,4 @@
1
1
  # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
2
2
 
3
3
  __title__ = "together"
4
- __version__ = "2.0.0-alpha.11" # x-release-please-version
4
+ __version__ = "2.0.0-alpha.12" # x-release-please-version
@@ -176,6 +176,12 @@ def fine_tuning(ctx: click.Context) -> None:
176
176
  help="Whether to mask the user messages in conversational data or prompts in instruction data. "
177
177
  "`auto` will automatically determine whether to mask the inputs based on the data format.",
178
178
  )
179
+ @click.option(
180
+ "--train-vision",
181
+ type=bool,
182
+ default=False,
183
+ help="Whether to train the vision encoder. Only supported for multimodal models.",
184
+ )
179
185
  @click.option(
180
186
  "--from-checkpoint",
181
187
  type=str,
@@ -231,6 +237,7 @@ def create(
231
237
  lora_dropout: float | None,
232
238
  lora_alpha: float | None,
233
239
  lora_trainable_modules: str | None,
240
+ train_vision: bool,
234
241
  suffix: str | None,
235
242
  wandb_api_key: str | None,
236
243
  wandb_base_url: str | None,
@@ -272,6 +279,7 @@ def create(
272
279
  lora_dropout=lora_dropout,
273
280
  lora_alpha=lora_alpha,
274
281
  lora_trainable_modules=lora_trainable_modules,
282
+ train_vision=train_vision,
275
283
  suffix=suffix,
276
284
  wandb_api_key=wandb_api_key,
277
285
  wandb_base_url=wandb_base_url,
@@ -363,6 +371,10 @@ def create(
363
371
  simpo_gamma=simpo_gamma or 0,
364
372
  )
365
373
 
374
+ if model_limits.supports_vision:
375
+ # Don't show price estimation for multimodal models yet
376
+ confirm = True
377
+
366
378
  finetune_price_estimation_result = client.fine_tuning.estimate_price(
367
379
  training_file=training_file,
368
380
  validation_file=validation_file,
@@ -426,9 +438,7 @@ def list(ctx: click.Context) -> None:
426
438
  "Price": f"""${
427
439
  finetune_price_to_dollars(float(str(i.total_price)))
428
440
  }""", # convert to string for mypy typing
429
- "Progress": generate_progress_bar(
430
- i, datetime.now().astimezone(), use_rich=False
431
- ),
441
+ "Progress": generate_progress_bar(i, datetime.now().astimezone(), use_rich=False),
432
442
  }
433
443
  )
434
444
  table = tabulate(display_list, headers="keys", tablefmt="grid", showindex=True)
@@ -449,9 +459,7 @@ def retrieve(ctx: click.Context, fine_tune_id: str) -> None:
449
459
  response.events = None
450
460
 
451
461
  rprint(JSON.from_data(response.model_json_schema()))
452
- progress_text = generate_progress_bar(
453
- response, datetime.now().astimezone(), use_rich=True
454
- )
462
+ progress_text = generate_progress_bar(response, datetime.now().astimezone(), use_rich=True)
455
463
  prefix = f"Status: [bold]{response.status}[/bold],"
456
464
  rprint(f"{prefix} {progress_text}")
457
465
 
@@ -28,9 +28,7 @@ class AutoIntParamType(click.ParamType):
28
28
  return int(value)
29
29
  except ValueError:
30
30
  self.fail(
31
- _("{value!r} is not a valid {number_type}.").format(
32
- value=value, number_type=self.name
33
- ),
31
+ _("{value!r} is not a valid {number_type}.").format(value=value, number_type=self.name),
34
32
  param,
35
33
  ctx,
36
34
  )
@@ -39,7 +37,7 @@ class AutoIntParamType(click.ParamType):
39
37
  class BooleanWithAutoParamType(click.ParamType):
40
38
  name = "boolean_or_auto"
41
39
 
42
- def convert( # pyright: ignore[reportImplicitOverride]
40
+ def convert( # pyright: ignore[reportImplicitOverride]
43
41
  self, value: str, param: click.Parameter | None, ctx: click.Context | None
44
42
  ) -> bool | Literal["auto"] | None:
45
43
  if value == "auto":
@@ -48,9 +46,7 @@ class BooleanWithAutoParamType(click.ParamType):
48
46
  return bool(value)
49
47
  except ValueError:
50
48
  self.fail(
51
- _("{value!r} is not a valid {type}.").format(
52
- value=value, type=self.name
53
- ),
49
+ _("{value!r} is not a valid {type}.").format(value=value, type=self.name),
54
50
  param,
55
51
  ctx,
56
52
  )
@@ -119,17 +115,13 @@ def generate_progress_bar(
119
115
  return progress
120
116
 
121
117
  elapsed_time = (current_time - update_at).total_seconds()
122
- ratio_filled = min(
123
- elapsed_time / finetune_job.progress.seconds_remaining, 1.0
124
- )
118
+ ratio_filled = min(elapsed_time / finetune_job.progress.seconds_remaining, 1.0)
125
119
  percentage = ratio_filled * 100
126
120
  filled = math.ceil(ratio_filled * _PROGRESS_BAR_WIDTH)
127
121
  bar = "█" * filled + "░" * (_PROGRESS_BAR_WIDTH - filled)
128
122
  time_left = "N/A"
129
123
  if finetune_job.progress.seconds_remaining > elapsed_time:
130
- time_left = _human_readable_time(
131
- finetune_job.progress.seconds_remaining - elapsed_time
132
- )
124
+ time_left = _human_readable_time(finetune_job.progress.seconds_remaining - elapsed_time)
133
125
  time_text = f"{time_left} left"
134
126
  progress = f"Progress: {bar} [bold]{percentage:>3.0f}%[/bold] [yellow]{time_text}[/yellow]"
135
127
 
together/lib/constants.py CHANGED
@@ -37,6 +37,12 @@ NUM_BYTES_IN_GB = 2**30
37
37
  # maximum number of GB sized files we support finetuning for
38
38
  MAX_FILE_SIZE_GB = 50.1
39
39
 
40
+ # Multimodal limits
41
+ MAX_IMAGES_PER_EXAMPLE = 10
42
+ MAX_IMAGE_BYTES = 10 * 1024 * 1024 # 10MB
43
+ # Max length = Header length + base64 factor (4/3) * image bytes
44
+ MAX_BASE64_IMAGE_LENGTH = len("data:image/jpeg;base64,") + 4 * MAX_IMAGE_BYTES // 3
45
+
40
46
  # expected columns for Parquet files
41
47
  PARQUET_EXPECTED_COLUMNS = ["input_ids", "attention_mask", "labels"]
42
48
 
@@ -22,6 +22,7 @@ from together.lib.types.fine_tuning import (
22
22
  CosineLRSchedulerArgs,
23
23
  LinearLRSchedulerArgs,
24
24
  FinetuneTrainingLimits,
25
+ FinetuneMultimodalParams,
25
26
  )
26
27
 
27
28
  AVAILABLE_TRAINING_METHODS = {
@@ -51,6 +52,7 @@ def create_finetune_request(
51
52
  lora_dropout: float | None = 0,
52
53
  lora_alpha: float | None = None,
53
54
  lora_trainable_modules: str | None = "all-linear",
55
+ train_vision: bool = False,
54
56
  suffix: str | None = None,
55
57
  wandb_api_key: str | None = None,
56
58
  wandb_base_url: str | None = None,
@@ -207,6 +209,13 @@ def create_finetune_request(
207
209
  simpo_gamma=simpo_gamma,
208
210
  )
209
211
 
212
+ if model_limits.supports_vision:
213
+ multimodal_params = FinetuneMultimodalParams(train_vision=train_vision)
214
+ elif not model_limits.supports_vision and train_vision:
215
+ raise ValueError(f"Vision encoder training is not supported for the non-multimodal model `{model}`")
216
+ else:
217
+ multimodal_params = None
218
+
210
219
  finetune_request = FinetuneRequest(
211
220
  model=model,
212
221
  training_file=training_file,
@@ -227,6 +236,7 @@ def create_finetune_request(
227
236
  wandb_project_name=wandb_project_name,
228
237
  wandb_name=wandb_name,
229
238
  training_method=training_method_cls, # pyright: ignore[reportPossiblyUnboundVariable]
239
+ multimodal_params=multimodal_params,
230
240
  from_checkpoint=from_checkpoint,
231
241
  from_hf_model=from_hf_model,
232
242
  hf_model_revision=hf_model_revision,
@@ -238,7 +248,10 @@ def create_finetune_request(
238
248
 
239
249
  return finetune_request, training_type_pe, training_method_pe
240
250
 
241
- def create_price_estimation_params(finetune_request: FinetuneRequest) -> tuple[pe_params.TrainingType, pe_params.TrainingMethod]:
251
+
252
+ def create_price_estimation_params(
253
+ finetune_request: FinetuneRequest,
254
+ ) -> tuple[pe_params.TrainingType, pe_params.TrainingMethod]:
242
255
  training_type_cls: pe_params.TrainingType
243
256
  if isinstance(finetune_request.training_type, FullTrainingType):
244
257
  training_type_cls = pe_params.TrainingTypeFullTrainingType(
@@ -275,6 +288,7 @@ def create_price_estimation_params(finetune_request: FinetuneRequest) -> tuple[p
275
288
 
276
289
  return training_type_cls, training_method_cls
277
290
 
291
+
278
292
  def get_model_limits(client: Together, model: str) -> FinetuneTrainingLimits:
279
293
  """
280
294
  Requests training limits for a specific model
@@ -189,6 +189,7 @@ class TrainingMethodUnknown(BaseModel):
189
189
 
190
190
  method: str
191
191
 
192
+
192
193
  TrainingMethod: TypeAlias = Union[
193
194
  TrainingMethodSFT,
194
195
  TrainingMethodDPO,
@@ -202,6 +203,7 @@ class FinetuneTrainingLimits(BaseModel):
202
203
  min_learning_rate: float
203
204
  full_training: Optional[FinetuneFullTrainingLimits] = None
204
205
  lora_training: Optional[FinetuneLoraTrainingLimits] = None
206
+ supports_vision: bool = False
205
207
 
206
208
 
207
209
  class LinearLRSchedulerArgs(BaseModel):
@@ -249,6 +251,7 @@ class EmptyLRScheduler(BaseModel):
249
251
  lr_scheduler_type: Literal[""]
250
252
  lr_scheduler_args: None = None
251
253
 
254
+
252
255
  class UnknownLRScheduler(BaseModel):
253
256
  """
254
257
  Unknown learning rate scheduler
@@ -268,6 +271,14 @@ FinetuneLRScheduler: TypeAlias = Union[
268
271
  ]
269
272
 
270
273
 
274
+ class FinetuneMultimodalParams(BaseModel):
275
+ """
276
+ Multimodal parameters
277
+ """
278
+
279
+ train_vision: bool = False
280
+
281
+
271
282
  class FinetuneProgress(BaseModel):
272
283
  """
273
284
  Fine-tune job progress
@@ -303,6 +314,9 @@ class FinetuneResponse(BaseModel):
303
314
  from_checkpoint: Optional[str] = None
304
315
  """Checkpoint used to continue training"""
305
316
 
317
+ multimodal_params: Optional[FinetuneMultimodalParams] = None
318
+ """Multimodal parameters"""
319
+
306
320
  from_hf_model: Optional[str] = None
307
321
  """Hugging Face Hub repo to start training from"""
308
322
 
@@ -467,6 +481,9 @@ class FinetuneRequest(BaseModel):
467
481
  training_method: TrainingMethod = Field(default_factory=TrainingMethodSFT)
468
482
  # from step
469
483
  from_checkpoint: Union[str, None] = None
484
+ # multimodal parameters
485
+ multimodal_params: Union[FinetuneMultimodalParams, None] = None
486
+ # hugging face related fields
470
487
  from_hf_model: Union[str, None] = None
471
488
  hf_model_revision: Union[str, None] = None
472
489
  # hf related fields
@@ -3,7 +3,7 @@ from __future__ import annotations
3
3
  import os
4
4
  import csv
5
5
  import json
6
- from typing import Any, Dict, List, cast
6
+ from typing import Any, Dict, List, Union, cast
7
7
  from pathlib import Path
8
8
  from traceback import format_exc
9
9
 
@@ -13,8 +13,11 @@ from together.types import FilePurpose
13
13
  from together.lib.constants import (
14
14
  MIN_SAMPLES,
15
15
  DISABLE_TQDM,
16
+ MAX_IMAGE_BYTES,
16
17
  NUM_BYTES_IN_GB,
17
18
  MAX_FILE_SIZE_GB,
19
+ MAX_IMAGES_PER_EXAMPLE,
20
+ MAX_BASE64_IMAGE_LENGTH,
18
21
  PARQUET_EXPECTED_COLUMNS,
19
22
  REQUIRED_COLUMNS_MESSAGE,
20
23
  JSONL_REQUIRED_COLUMNS_MAP,
@@ -22,6 +25,15 @@ from together.lib.constants import (
22
25
  DatasetFormat,
23
26
  )
24
27
 
28
+ # MessageContent is a string or a list of dicts with 'type': 'text' or 'image_url', and 'text' or 'image_url.url'
29
+ # Example: "Hello" or [
30
+ # {"type": "text", "text": "Hello"},
31
+ # {"type": "image_url", "image_url": {
32
+ # "url": "data:image/jpeg;base64,..."
33
+ # }}
34
+ # ]
35
+ MessageContent = Union[str, List[Dict[str, Any]]]
36
+
25
37
 
26
38
  class InvalidFileFormatError(ValueError):
27
39
  """Exception raised for invalid file formats during file checks."""
@@ -103,7 +115,7 @@ def check_file(
103
115
  return report_dict
104
116
 
105
117
 
106
- def _check_conversation_type(messages: List[Dict[str, str | bool]], idx: int) -> None:
118
+ def _check_conversation_type(messages: List[Dict[str, str | int | MessageContent]], idx: int) -> None:
107
119
  """Check that the conversation has correct type.
108
120
 
109
121
  Args:
@@ -144,12 +156,6 @@ def _check_conversation_type(messages: List[Dict[str, str | bool]], idx: int) ->
144
156
  line_number=idx + 1,
145
157
  error_source="key_value",
146
158
  )
147
- if not isinstance(message[column], str):
148
- raise InvalidFileFormatError(
149
- message=f"Column `{column}` is not a string on line {idx + 1}. Found {type(message[column])}",
150
- line_number=idx + 1,
151
- error_source="text_field",
152
- )
153
159
 
154
160
 
155
161
  def _check_conversation_roles(require_assistant_role: bool, assistant_role_exists: bool, idx: int) -> None:
@@ -172,7 +178,7 @@ def _check_conversation_roles(require_assistant_role: bool, assistant_role_exist
172
178
  )
173
179
 
174
180
 
175
- def _check_message_weight(message: Dict[str, str | bool], idx: int) -> None:
181
+ def _check_message_weight(message: Dict[str, str | int | MessageContent], idx: int) -> int | None:
176
182
  """Check that the message has a weight with the correct type and value.
177
183
 
178
184
  Args:
@@ -196,9 +202,12 @@ def _check_message_weight(message: Dict[str, str | bool], idx: int) -> None:
196
202
  line_number=idx + 1,
197
203
  error_source="key_value",
198
204
  )
205
+ return weight
206
+
207
+ return None
199
208
 
200
209
 
201
- def _check_message_role(message: Dict[str, str | bool], previous_role: str | bool | None, idx: int) -> str | bool:
210
+ def _check_message_role(message: Dict[str, str | int | MessageContent], previous_role: str | None, idx: int) -> str:
202
211
  """Check that the message has correct roles.
203
212
 
204
213
  Args:
@@ -212,6 +221,14 @@ def _check_message_role(message: Dict[str, str | bool], previous_role: str | boo
212
221
  Raises:
213
222
  InvalidFileFormatError: If the message role is invalid.
214
223
  """
224
+ if not isinstance(message["role"], str):
225
+ raise InvalidFileFormatError(
226
+ message=f"Invalid role `{message['role']}` in conversation on line {idx + 1}. "
227
+ f"Role must be a string. Found {type(message['role'])}",
228
+ line_number=idx + 1,
229
+ error_source="key_value",
230
+ )
231
+
215
232
  if message["role"] not in POSSIBLE_ROLES_CONVERSATION:
216
233
  raise InvalidFileFormatError(
217
234
  message=f"Invalid role `{message['role']}` in conversation on line {idx + 1}. "
@@ -229,7 +246,130 @@ def _check_message_role(message: Dict[str, str | bool], previous_role: str | boo
229
246
  return message["role"]
230
247
 
231
248
 
232
- def validate_messages(messages: List[Dict[str, str | bool]], idx: int, require_assistant_role: bool = True) -> None:
249
+ def _check_message_content(message_content: str | int | MessageContent, role: str, idx: int) -> tuple[bool, int]:
250
+ """Check that the message content has the correct type.
251
+ Message content can be either a) a string or b) an OpenAI-style multimodal list of content items
252
+ Example:
253
+ a) "Hello", or
254
+ b) [
255
+ {"type": "text", "text": "Hello"},
256
+ {"type": "image_url", "image_url": {
257
+ "url": "data:image/jpeg;base64,..."
258
+ }}
259
+ ]
260
+
261
+ Args:
262
+ message_content: The message content to check.
263
+ role: The role of the message.
264
+ idx: Line number in the file.
265
+
266
+ Returns:
267
+ tuple[bool, int]: A tuple with message is multimodal and the number of images in the message content.
268
+ """
269
+ # Text-only message content
270
+ if isinstance(message_content, str):
271
+ return False, 0
272
+
273
+ # Multimodal message content
274
+ if isinstance(message_content, list):
275
+ num_images = 0
276
+ for item in message_content:
277
+ if not isinstance(cast(Any, item), dict):
278
+ raise InvalidFileFormatError(
279
+ "The dataset is malformed, the `content` field must be a list of dicts.",
280
+ line_number=idx + 1,
281
+ error_source="key_value",
282
+ )
283
+ if "type" not in item:
284
+ raise InvalidFileFormatError(
285
+ "The dataset is malformed, the `content` field must be a list of dicts with a `type` field.",
286
+ line_number=idx + 1,
287
+ error_source="key_value",
288
+ )
289
+
290
+ if item["type"] == "text":
291
+ if "text" not in item or not isinstance(item["text"], str):
292
+ raise InvalidFileFormatError(
293
+ "The dataset is malformed, the `text` field must be present in the `content` item field and be"
294
+ f" a string. Got '{item.get('text')!r}' instead.",
295
+ line_number=idx + 1,
296
+ error_source="key_value",
297
+ )
298
+ elif item["type"] == "image_url":
299
+ if role != "user":
300
+ raise InvalidFileFormatError(
301
+ "The dataset is malformed, only user messages can contain images.",
302
+ line_number=idx + 1,
303
+ error_source="key_value",
304
+ )
305
+
306
+ if "image_url" not in item or not isinstance(item["image_url"], dict):
307
+ raise InvalidFileFormatError(
308
+ "The dataset is malformed, the `image_url` field must be present in the `content` field and "
309
+ f"be a dictionary. Got {item.get('image_url')!r} instead.",
310
+ line_number=idx + 1,
311
+ error_source="key_value",
312
+ )
313
+
314
+ image_data = cast(Any, item["image_url"]).get("url")
315
+ if not image_data or not isinstance(image_data, str):
316
+ raise InvalidFileFormatError(
317
+ "The dataset is malformed, the `url` field must be present in the `image_url` field and be "
318
+ f"a string. Got {image_data!r} instead.",
319
+ line_number=idx + 1,
320
+ error_source="key_value",
321
+ )
322
+
323
+ if not any(image_data.startswith(f"data:image/{fmt};base64,") for fmt in ["jpeg", "png", "webp"]):
324
+ raise InvalidFileFormatError(
325
+ "The dataset is malformed, the `url` field must be either a JPEG, PNG or WEBP base64-encoded "
326
+ "image in 'data:image/<format>;base64,<base64_encoded_image>' format. "
327
+ f"Got '{image_data[:100]}...' instead.",
328
+ line_number=idx + 1,
329
+ )
330
+
331
+ if len(image_data) > MAX_BASE64_IMAGE_LENGTH:
332
+ raise InvalidFileFormatError(
333
+ "The dataset is malformed, the `url` field must contain base64-encoded image "
334
+ f"that is less than {MAX_IMAGE_BYTES // (1024**2)}MB, found ~{len(image_data) * 3 // 4} bytes.",
335
+ line_number=idx + 1,
336
+ error_source="key_value",
337
+ )
338
+
339
+ num_images += 1
340
+ else:
341
+ raise InvalidFileFormatError(
342
+ "The dataset is malformed, the `type` field must be either 'text' or 'image_url'. "
343
+ f"Got {item['type']!r}.",
344
+ line_number=idx + 1,
345
+ error_source="key_value",
346
+ )
347
+
348
+ if num_images > MAX_IMAGES_PER_EXAMPLE:
349
+ raise InvalidFileFormatError(
350
+ f"The dataset is malformed, the `content` field must contain at most "
351
+ f"{MAX_IMAGES_PER_EXAMPLE} images, found {num_images}.",
352
+ line_number=idx + 1,
353
+ error_source="key_value",
354
+ )
355
+
356
+ # We still consider text-only messages in such format as multimodal, even if they don't have any images
357
+ # included - so we can process datasets with rather sparse images (i.e. not in each sample) consistently.
358
+ return True, num_images
359
+
360
+ raise InvalidFileFormatError(
361
+ f"Invalid content type on line {idx + 1} of the input file. Expected string or multimodal list of dicts, "
362
+ f"found {type(message_content)}",
363
+ line_number=idx + 1,
364
+ error_source="key_value",
365
+ )
366
+
367
+
368
+ def validate_messages(
369
+ messages: List[Dict[str, str | int | MessageContent]],
370
+ idx: int,
371
+ require_assistant_role: bool = True,
372
+ ) -> None:
233
373
  """Validate the messages column.
234
374
 
235
375
  Args:
@@ -242,15 +382,43 @@ def validate_messages(messages: List[Dict[str, str | bool]], idx: int, require_a
242
382
  """
243
383
  _check_conversation_type(messages, idx)
244
384
 
245
- has_weights = any("weight" in message for message in messages)
246
385
  previous_role = None
247
386
  assistant_role_exists = False
248
387
 
388
+ messages_are_multimodal: bool | None = None
389
+ total_number_of_images = 0
390
+
249
391
  for message in messages:
250
- if has_weights:
251
- _check_message_weight(message, idx)
392
+ message_weight = _check_message_weight(message, idx)
252
393
  previous_role = _check_message_role(message, previous_role, idx)
253
394
  assistant_role_exists |= previous_role == "assistant"
395
+ is_multimodal, number_of_images = _check_message_content(message["content"], role=previous_role, idx=idx)
396
+ # Multimodal validation
397
+ if number_of_images > 0 and message_weight is not None and message_weight != 0:
398
+ raise InvalidFileFormatError(
399
+ "Messages with images cannot have non-zero weights.",
400
+ line_number=idx + 1,
401
+ error_source="key_value",
402
+ )
403
+ if messages_are_multimodal is None:
404
+ # Detect the format of the messages in the conversation.
405
+ messages_are_multimodal = is_multimodal
406
+ elif messages_are_multimodal != is_multimodal:
407
+ # Due to the format limitation, we cannot mix multimodal and text only messages in the same sample.
408
+ raise InvalidFileFormatError(
409
+ "Messages in the conversation must be either all in multimodal or all in text-only format.",
410
+ line_number=idx + 1,
411
+ error_source="key_value",
412
+ )
413
+ total_number_of_images += number_of_images
414
+
415
+ if total_number_of_images > MAX_IMAGES_PER_EXAMPLE:
416
+ raise InvalidFileFormatError(
417
+ f"The dataset is malformed, the `messages` must contain at most {MAX_IMAGES_PER_EXAMPLE} images. "
418
+ f"Found {total_number_of_images} images.",
419
+ line_number=idx + 1,
420
+ error_source="key_value",
421
+ )
254
422
 
255
423
  _check_conversation_roles(require_assistant_role, assistant_role_exists, idx)
256
424
 
@@ -279,7 +447,7 @@ def validate_preference_openai(example: Dict[str, Any], idx: int = 0) -> None:
279
447
  error_source="key_value",
280
448
  )
281
449
 
282
- messages: List[Dict[str, str | bool]] = cast(Any, example["input"]["messages"])
450
+ messages: List[Dict[str, str | int | MessageContent]] = cast(Any, example["input"]["messages"])
283
451
  validate_messages(messages, idx, require_assistant_role=False)
284
452
 
285
453
  if example["input"]["messages"][-1]["role"] == "assistant":
@@ -341,12 +509,7 @@ def validate_preference_openai(example: Dict[str, Any], idx: int = 0) -> None:
341
509
  error_source="key_value",
342
510
  )
343
511
 
344
- if not isinstance(example[key][0]["content"], str):
345
- raise InvalidFileFormatError(
346
- message=f"The dataset is malformed, the 'content' field in `{key}` must be a string on line {idx + 1}.",
347
- line_number=idx + 1,
348
- error_source="key_value",
349
- )
512
+ _check_message_content(example[key][0]["content"], role="assistant", idx=idx)
350
513
 
351
514
 
352
515
  def _check_utf8(file: Path) -> Dict[str, Any]:
@@ -514,7 +677,7 @@ def _check_jsonl(file: Path, purpose: FilePurpose | str) -> Dict[str, Any]:
514
677
  elif current_format == DatasetFormat.CONVERSATION:
515
678
  message_column = JSONL_REQUIRED_COLUMNS_MAP[DatasetFormat.CONVERSATION][0]
516
679
  require_assistant = purpose != "eval"
517
- message: List[Dict[str, str | bool]] = cast(Any, json_line[message_column])
680
+ message: List[Dict[str, str | int | MessageContent]] = cast(Any, json_line[message_column])
518
681
  validate_messages(
519
682
  message,
520
683
  idx,
@@ -522,13 +685,8 @@ def _check_jsonl(file: Path, purpose: FilePurpose | str) -> Dict[str, Any]:
522
685
  )
523
686
  else:
524
687
  for column in JSONL_REQUIRED_COLUMNS_MAP[current_format]:
525
- if not isinstance(json_line[column], str):
526
- raise InvalidFileFormatError(
527
- message=f'Invalid value type for "{column}" key on line {idx + 1}. '
528
- f"Expected string. Found {type(cast(Any, json_line[column]))}.",
529
- line_number=idx + 1,
530
- error_source="key_value",
531
- )
688
+ role = "assistant" if column in {"completion"} else "user"
689
+ _check_message_content(cast(Any, json_line[column]), role=role, idx=idx)
532
690
 
533
691
  if dataset_format is None:
534
692
  dataset_format = current_format
@@ -53,6 +53,7 @@ _WARNING_MESSAGE_INSUFFICIENT_FUNDS = (
53
53
  "Proceed at your own risk."
54
54
  )
55
55
 
56
+
56
57
  class FineTuningResource(SyncAPIResource):
57
58
  @cached_property
58
59
  def with_raw_response(self) -> FineTuningResourceWithRawResponse:
@@ -95,6 +96,7 @@ class FineTuningResource(SyncAPIResource):
95
96
  lora_dropout: float | None = 0,
96
97
  lora_alpha: float | None = None,
97
98
  lora_trainable_modules: str | None = "all-linear",
99
+ train_vision: bool = False,
98
100
  suffix: str | None = None,
99
101
  wandb_api_key: str | None = None,
100
102
  wandb_base_url: str | None = None,
@@ -140,6 +142,7 @@ class FineTuningResource(SyncAPIResource):
140
142
  lora_dropout (float, optional): Dropout rate for LoRA adapters. Defaults to 0.
141
143
  lora_alpha (float, optional): Alpha for LoRA adapters. Defaults to 8.
142
144
  lora_trainable_modules (str, optional): Trainable modules for LoRA adapters. Defaults to "all-linear".
145
+ train_vision (bool, optional): Whether to train the vision encoder (Only for multimodal models). Defaults to False.
143
146
  suffix (str, optional): Up to 40 character suffix that will be added to your fine-tuned model name.
144
147
  Defaults to None.
145
148
  wandb_api_key (str, optional): API key for Weights & Biases integration.
@@ -214,6 +217,7 @@ class FineTuningResource(SyncAPIResource):
214
217
  lora_dropout=lora_dropout,
215
218
  lora_alpha=lora_alpha,
216
219
  lora_trainable_modules=lora_trainable_modules,
220
+ train_vision=train_vision,
217
221
  suffix=suffix,
218
222
  wandb_api_key=wandb_api_key,
219
223
  wandb_base_url=wandb_base_url,
@@ -232,29 +236,32 @@ class FineTuningResource(SyncAPIResource):
232
236
  hf_output_repo_name=hf_output_repo_name,
233
237
  )
234
238
 
235
-
236
- price_estimation_result = self.estimate_price(
237
- training_file=training_file,
238
- from_checkpoint=from_checkpoint or Omit(),
239
- validation_file=validation_file or Omit(),
240
- model=model or "",
241
- n_epochs=finetune_request.n_epochs,
242
- n_evals=finetune_request.n_evals or 0,
243
- training_type=training_type_cls,
244
- training_method=training_method_cls,
245
- )
246
-
239
+ if not model_limits.supports_vision:
240
+ price_estimation_result = self.estimate_price(
241
+ training_file=training_file,
242
+ from_checkpoint=from_checkpoint or Omit(),
243
+ validation_file=validation_file or Omit(),
244
+ model=model or "",
245
+ n_epochs=finetune_request.n_epochs,
246
+ n_evals=finetune_request.n_evals or 0,
247
+ training_type=training_type_cls,
248
+ training_method=training_method_cls,
249
+ )
250
+ price_limit_passed = price_estimation_result.allowed_to_proceed
251
+ else:
252
+ # unsupported case
253
+ price_limit_passed = True
247
254
 
248
255
  if verbose:
249
256
  rprint(
250
257
  "Submitting a fine-tuning job with the following parameters:",
251
258
  finetune_request,
252
259
  )
253
- if not price_estimation_result.allowed_to_proceed:
260
+ if not price_limit_passed:
254
261
  rprint(
255
262
  "[red]"
256
263
  + _WARNING_MESSAGE_INSUFFICIENT_FUNDS.format(
257
- price_estimation_result.estimated_total_price # pyright: ignore[reportPossiblyUnboundVariable]
264
+ price_estimation_result.estimated_total_price # pyright: ignore[reportPossiblyUnboundVariable]
258
265
  )
259
266
  + "[/red]",
260
267
  )
@@ -627,6 +634,7 @@ class AsyncFineTuningResource(AsyncAPIResource):
627
634
  lora_dropout: float | None = 0,
628
635
  lora_alpha: float | None = None,
629
636
  lora_trainable_modules: str | None = "all-linear",
637
+ train_vision: bool = False,
630
638
  suffix: str | None = None,
631
639
  wandb_api_key: str | None = None,
632
640
  wandb_base_url: str | None = None,
@@ -672,6 +680,7 @@ class AsyncFineTuningResource(AsyncAPIResource):
672
680
  lora_dropout (float, optional): Dropout rate for LoRA adapters. Defaults to 0.
673
681
  lora_alpha (float, optional): Alpha for LoRA adapters. Defaults to 8.
674
682
  lora_trainable_modules (str, optional): Trainable modules for LoRA adapters. Defaults to "all-linear".
683
+ train_vision (bool, optional): Whether to train the vision encoder (Only for multimodal models). Defaults to False.
675
684
  suffix (str, optional): Up to 40 character suffix that will be added to your fine-tuned model name.
676
685
  Defaults to None.
677
686
  wandb_api_key (str, optional): API key for Weights & Biases integration.
@@ -746,6 +755,7 @@ class AsyncFineTuningResource(AsyncAPIResource):
746
755
  lora_dropout=lora_dropout,
747
756
  lora_alpha=lora_alpha,
748
757
  lora_trainable_modules=lora_trainable_modules,
758
+ train_vision=train_vision,
749
759
  suffix=suffix,
750
760
  wandb_api_key=wandb_api_key,
751
761
  wandb_base_url=wandb_base_url,
@@ -764,7 +774,6 @@ class AsyncFineTuningResource(AsyncAPIResource):
764
774
  hf_output_repo_name=hf_output_repo_name,
765
775
  )
766
776
 
767
-
768
777
  price_estimation_result = await self.estimate_price(
769
778
  training_file=training_file,
770
779
  from_checkpoint=from_checkpoint or Omit(),
@@ -776,7 +785,6 @@ class AsyncFineTuningResource(AsyncAPIResource):
776
785
  training_method=training_method_cls,
777
786
  )
778
787
 
779
-
780
788
  if verbose:
781
789
  rprint(
782
790
  "Submitting a fine-tuning job with the following parameters:",
@@ -786,7 +794,7 @@ class AsyncFineTuningResource(AsyncAPIResource):
786
794
  rprint(
787
795
  "[red]"
788
796
  + _WARNING_MESSAGE_INSUFFICIENT_FUNDS.format(
789
- price_estimation_result.estimated_total_price # pyright: ignore[reportPossiblyUnboundVariable]
797
+ price_estimation_result.estimated_total_price # pyright: ignore[reportPossiblyUnboundVariable]
790
798
  )
791
799
  + "[/red]",
792
800
  )
@@ -15,6 +15,7 @@ __all__ = [
15
15
  "LrSchedulerLrSchedulerArgs",
16
16
  "LrSchedulerLrSchedulerArgsLinearLrSchedulerArgs",
17
17
  "LrSchedulerLrSchedulerArgsCosineLrSchedulerArgs",
18
+ "MultimodalParams",
18
19
  "Progress",
19
20
  "TrainingMethod",
20
21
  "TrainingMethodTrainingMethodSft",
@@ -49,6 +50,14 @@ class LrScheduler(BaseModel):
49
50
  lr_scheduler_args: Optional[LrSchedulerLrSchedulerArgs] = None
50
51
 
51
52
 
53
+ class MultimodalParams(BaseModel):
54
+ train_vision: Optional[bool] = None
55
+ """Whether to train the vision encoder of the model.
56
+
57
+ Only available for multimodal models.
58
+ """
59
+
60
+
52
61
  class Progress(BaseModel):
53
62
  """Progress information for a fine-tuning job"""
54
63
 
@@ -150,6 +159,8 @@ class FinetuneResponse(BaseModel):
150
159
 
151
160
  x_model_output_path: Optional[str] = FieldInfo(alias="model_output_path", default=None)
152
161
 
162
+ multimodal_params: Optional[MultimodalParams] = None
163
+
153
164
  n_checkpoints: Optional[int] = None
154
165
 
155
166
  n_epochs: Optional[int] = None
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: together
3
- Version: 2.0.0a11
3
+ Version: 2.0.0a12
4
4
  Summary: The official Python library for the together API
5
5
  Project-URL: Homepage, https://github.com/togethercomputer/together-py
6
6
  Project-URL: Repository, https://github.com/togethercomputer/together-py
@@ -183,7 +183,7 @@ stream = client.chat.completions.create(
183
183
  messages=[
184
184
  {
185
185
  "role": "user",
186
- "content": "Say this is a test",
186
+ "content": "Say this is a test!",
187
187
  }
188
188
  ],
189
189
  model="meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo",
@@ -204,7 +204,7 @@ stream = await client.chat.completions.create(
204
204
  messages=[
205
205
  {
206
206
  "role": "user",
207
- "content": "Say this is a test",
207
+ "content": "Say this is a test!",
208
208
  }
209
209
  ],
210
210
  model="meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo",
@@ -1,5 +1,5 @@
1
1
  together/__init__.py,sha256=ghwEH6EUrPERUwHVSXaCJVqS7QmLN7NsUxKJNXQrOYM,2842
2
- together/_base_client.py,sha256=yP8nFDC-tFtGBrXHhcfcW4xF7KzAV5GQCLKlCZyp8sg,67237
2
+ together/_base_client.py,sha256=U6Lhqesx9l1qhaYKaP2jR-_mIyqbNhAOo2r2bgTQegI,67249
3
3
  together/_client.py,sha256=Nw2fyh2kf3RABNaHuqWKKhTujaHttGXoUZENECGaqyI,37788
4
4
  together/_compat.py,sha256=DQBVORjFb33zch24jzkhM14msvnzY7mmSmgDLaVFUM8,6562
5
5
  together/_constants.py,sha256=i39tJ7BP8nnqvdHFJwMhN6LWR6-jg5LLYiFudwCD3Ic,463
@@ -11,7 +11,7 @@ together/_resource.py,sha256=-ZTq9O5qf2YsgjJk_gwJs-CM_OG4p6gdMLcNWjuxFwQ,1112
11
11
  together/_response.py,sha256=lvqEsCbpD8SRJTjlhhUFGbnLUR_4-Qva-OApxfVdiY4,28800
12
12
  together/_streaming.py,sha256=sk6fVYbpdO3Y-0S5iwZTHQJ3N24UkK0KaupgUTftWZk,11825
13
13
  together/_types.py,sha256=LzaeqN09mUAEvRg_XrLzihdOaW0D_R9qrG7jKsFjnQY,7297
14
- together/_version.py,sha256=F4XFYcFCYn77Ecwy5AT-x99HNaeVVRSV5vbTWx512a8,169
14
+ together/_version.py,sha256=OH5dGztmdGmTCfPzpSFY8dmywHPRzyc6lLUVwFJNg8w,169
15
15
  together/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
16
16
  together/_utils/__init__.py,sha256=7fch0GT9zpNnErbciSpUNa-SjTxxjY6kxHxKMOM4AGs,2305
17
17
  together/_utils/_compat.py,sha256=rN17SSvjMoQE1GmKFTLniRuG1sKj2WAD5VjdLPeRlF0,1231
@@ -27,25 +27,25 @@ together/_utils/_typing.py,sha256=N_5PPuFNsaygbtA_npZd98SVN1LQQvFTKL6bkWPBZGU,47
27
27
  together/_utils/_utils.py,sha256=g9ftElB09kVT6EVfCIlD_nUfANhDX5_vZO61FDWoIQI,12334
28
28
  together/lib/.keep,sha256=wuNrz-5SXo3jJaJOJgz4vFHM41YH_g20F5cRQo0vLes,224
29
29
  together/lib/__init__.py,sha256=Qtdi6geFNzxE-F51eNDk1ESXYyYDt8b82MR1POANQBQ,394
30
- together/lib/constants.py,sha256=pFWjUtItT-6_-Gpky0P2Q7bSj4iRmApbSqkcYetE7V0,1996
30
+ together/lib/constants.py,sha256=w8-zVl8XZiJxqMdhbWekigHJ0JUMPoV9R3ejUHIcUJk,2237
31
31
  together/lib/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
32
32
  together/lib/cli/cli.py,sha256=bNzYeLF8JdlMnSmIqFClp28MzjLGCwQ9hqSpaXHBQ0s,1939
33
33
  together/lib/cli/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
34
34
  together/lib/cli/api/endpoints.py,sha256=MyliUrTuJWw2qFd80J27pFs9xTazIVAP0mqgRYxdVsw,14851
35
35
  together/lib/cli/api/evals.py,sha256=KkSvz2wIYmPQ3sFQBte6inNBZt1aptIkMVL5TKWTW5k,19074
36
36
  together/lib/cli/api/files.py,sha256=HbflC45PpzBIF0CE0TLucQaVr319ScL05VyAFKf2T6Y,3596
37
- together/lib/cli/api/fine_tuning.py,sha256=-PdNR_16WiwI_wZkqemb_m5SRceRuSvpOdEL8LGFGJU,22571
37
+ together/lib/cli/api/fine_tuning.py,sha256=Tb4J9_LnHZHp73zNW1lHNSl_0UQfOd0yi9-IvMHBY08,22863
38
38
  together/lib/cli/api/models.py,sha256=Jfrl7gcbWAkbBQ1i1gCy485HHT2C4C784OMaaHZPiPw,4084
39
- together/lib/cli/api/utils.py,sha256=1W9Ebc_deJ6vss1IBGa80SFx61xENCyvnYRYwjCZ_Xo,4524
39
+ together/lib/cli/api/utils.py,sha256=j4IYurqcoqisstAQuqWMsUKbMQITNX8ax2Vv6U6qF7I,4381
40
40
  together/lib/resources/__init__.py,sha256=ystIb0pBHQLuwUBtHJwhRgtjK3_TV6K0KuM8NGuuNoU,172
41
41
  together/lib/resources/files.py,sha256=Z_D23IvjYYWBpYrfYolCNfUslJBcE4PnU0WtuLsN67M,37277
42
- together/lib/resources/fine_tuning.py,sha256=-yCS6AIbgXpAcUkeqJcia8CPrnVcmyiaUN4dvksEjcc,12642
42
+ together/lib/resources/fine_tuning.py,sha256=A-hOJqcGSPzw24wwX6K27OqV3B-u43dfdrK4nj4ItTg,13088
43
43
  together/lib/types/__init__.py,sha256=1-kHsAp9Sh9HxjTGKfdHnF1nTS_cM_Tazv-3Z9hrEbY,205
44
44
  together/lib/types/error.py,sha256=i-rnTZPRZuJDUf1lM-52abG2JHWOUBTCh55zPNGoakg,135
45
- together/lib/types/fine_tuning.py,sha256=uK_hke1huHjbjzQQBzz7EYMj1UHgCFVS0o7AG5L5xwY,12942
45
+ together/lib/types/fine_tuning.py,sha256=gTB66x4jrmxyWuKJGcqX_4aYcI2iGgZvL3MzMLePDC8,13325
46
46
  together/lib/utils/__init__.py,sha256=F_CVqnvK-aEshMg-5FLFincPbhuVbsM6IKSCNyEByKs,545
47
47
  together/lib/utils/_log.py,sha256=mo5tDhyFTNqEj8MOcpy3bLmLBcC0OQ67orTw_nxFdcU,1930
48
- together/lib/utils/files.py,sha256=S6orZixBPeRtV_iq_IktuYHIm41irrHuOuexz_NYZJ0,24863
48
+ together/lib/utils/files.py,sha256=CVTFwI7yMzpaQ-GsGr1tD4O2kXA-i369Pi0eMnlWMmI,31854
49
49
  together/lib/utils/serializer.py,sha256=wJwySGxAL0e1giZzFpl4hHH3s9lkoNN_yzu-P_jdRIo,287
50
50
  together/lib/utils/tools.py,sha256=rrpz3EXEVViou5GDPjVoCSt2zDPJYDzWYqTsVO1-OgI,2183
51
51
  together/resources/__init__.py,sha256=Cuiy4FcdrfUzb0N-jZbl8Phqjvlzt12Iq7BhI9tFsXw,7577
@@ -55,7 +55,7 @@ together/resources/embeddings.py,sha256=7EU6DZQd0Nm0Sh7x7v37QQOLNuLqNmcjdJAyOTck
55
55
  together/resources/endpoints.py,sha256=dYdLlAJ0P7HJNhzZGxlbzEQYpUWsh35cjAMVfdWiifw,27884
56
56
  together/resources/evals.py,sha256=FPjvkbsBY5rrzLyQ-X1G9fWt2QmivI9ol5GExGtqYVA,16216
57
57
  together/resources/files.py,sha256=0paHeVqNt3NQCXoztCgFS8PEIg_-mMVto-ulHTr7GzE,16854
58
- together/resources/fine_tuning.py,sha256=rpdUFb2p7_AGn8TIn7dPuhcFCCsuE-yuUGzo8S-epO8,54170
58
+ together/resources/fine_tuning.py,sha256=BiCxQpdTjW5ArBufmWHNQoYY4z7Ulge8dI9GDCa5Dow,54795
59
59
  together/resources/hardware.py,sha256=xgfCmMrrwF5o1igax0JGec8RY7kkS0s4kKm62RdC3ME,6850
60
60
  together/resources/images.py,sha256=mVPQYpDHKBjLVO_Sv0uT62zYXdtWKN2PW3fCvfQLQCs,12612
61
61
  together/resources/jobs.py,sha256=TnzSnvJw4x5pqo1xzrkYH8f0viZrzyOqT-_w7xc0NzY,7797
@@ -117,7 +117,7 @@ together/types/fine_tuning_list_events_response.py,sha256=DeDJLF1IxQV47HOwfuVt8Z
117
117
  together/types/fine_tuning_list_response.py,sha256=vdVZBQ4V1GFzHUBiRR0drEvKg1ASTyG5g84P0oM1S_c,5992
118
118
  together/types/finetune_event.py,sha256=0apAXe6Anx2_ffse2pOBJDxngCeuSvuDMYczZ0DtzZg,787
119
119
  together/types/finetune_event_type.py,sha256=Bm4lkBhsLI_kaD5yabsvW6BpnjXzZO_lwDtiEeMNXnw,824
120
- together/types/finetune_response.py,sha256=7W2_HBLCleIAF_u-qwvh8f4QEMsVyYkwtSPO3BqFI2Q,4708
120
+ together/types/finetune_response.py,sha256=klN6uuZt1tQ_PJ3rt7OSd_Cf7f0MuUnFZ4uJ6ISdrEU,4975
121
121
  together/types/hardware_list_params.py,sha256=BbfiigtdQE0QNGFGr6o-Twg912x_riH5mbUNpZWYWO4,435
122
122
  together/types/hardware_list_response.py,sha256=KfGhnEy7qEW2Bzt4Q8b-JSvxG-IKIIFfcpWEQHS9YdM,1732
123
123
  together/types/image_data_b64.py,sha256=pLY7JDBb1HF1T29ACbae_xn6JQfttpqQVeG_jJeenZU,284
@@ -159,8 +159,8 @@ together/types/chat/chat_completion_warning.py,sha256=_Dp7YKlxyY2HeZopTvT-Go7qqK
159
159
  together/types/chat/completion_create_params.py,sha256=Ia2dWuVxjderCRfpqV4Zpl-9rVng0p8deILUXVC3O0s,13687
160
160
  together/types/code_interpreter/__init__.py,sha256=dAXfb3ryLMtcBalCfxxNu2wJVswVP8G1xXryZnahPQY,201
161
161
  together/types/code_interpreter/session_list_response.py,sha256=TRxLGFTmIY-KLpStKjJtsrm4EI6BBvakpx43B6pkhnw,662
162
- together-2.0.0a11.dist-info/METADATA,sha256=YQHojaT_Y39KC8RAq-bktHXAKF3SUO4YEd2jrscrp9U,20247
163
- together-2.0.0a11.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
164
- together-2.0.0a11.dist-info/entry_points.txt,sha256=4f4RAX89wQkx3AnfHXiGrKyg2fCPnwMd2UdPX48OczA,55
165
- together-2.0.0a11.dist-info/licenses/LICENSE,sha256=5I5MO2DiiBFcD_p4ZF2T4GDb-WeBMD591ALtADdtXDc,11338
166
- together-2.0.0a11.dist-info/RECORD,,
162
+ together-2.0.0a12.dist-info/METADATA,sha256=9QxYAaPhjBTRxpvWRO3CIFG_ebYHjTzRwgQ2f2QJwko,20249
163
+ together-2.0.0a12.dist-info/WHEEL,sha256=C2FUgwZgiLbznR-k0b_5k3Ai_1aASOXDss3lzCUsUug,87
164
+ together-2.0.0a12.dist-info/entry_points.txt,sha256=4f4RAX89wQkx3AnfHXiGrKyg2fCPnwMd2UdPX48OczA,55
165
+ together-2.0.0a12.dist-info/licenses/LICENSE,sha256=oSs-kmJHhMue4vIIPIxQMvXou9PbxgNdIX-r_AwfO7c,11338
166
+ together-2.0.0a12.dist-info/RECORD,,
@@ -186,7 +186,7 @@
186
186
  same "printed page" as the copyright notice for easier
187
187
  identification within third-party archives.
188
188
 
189
- Copyright 2025 Together
189
+ Copyright 2026 Together
190
190
 
191
191
  Licensed under the Apache License, Version 2.0 (the "License");
192
192
  you may not use this file except in compliance with the License.