Skip to content

Commit 21d569a

Browse files
gary-huangYun-Kim
andauthored
chore(llmobs): use upload endpoint for CSV's and large pushes (#14473)
**case 1: create dataset from CSVs** with big datasets / big CSVs, we will run into timeout errors ``` Traceback (most recent call last): File "/Users/gary.huang/go/src/github.com/DataDog/llm-observability/preview/experiments/notebooks/test.py", line 23, in <module> dataset = LLMObs.create_dataset_from_csv(csv_path="/Users/gary.huang/Downloads/weatherAUS.csv", dataset_name="weatheraus-2", input_data_columns=["Date", "input", "Evaporation", "Sunshine"], expected_output_columns=["RainTomorrow"]) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/gary.huang/go/src/github.com/DataDog/llm-observability/preview/experiments/notebooks/.venv/lib/python3.12/site-packages/ddtrace/llmobs/_llmobs.py", line 722, in create_dataset_from_csv ds.push() File "/Users/gary.huang/go/src/github.com/DataDog/llm-observability/preview/experiments/notebooks/.venv/lib/python3.12/site-packages/ddtrace/llmobs/_experiment.py", line 140, in push new_version, new_record_ids = self._dne_client.dataset_batch_update( ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/gary.huang/go/src/github.com/DataDog/llm-observability/preview/experiments/notebooks/.venv/lib/python3.12/site-packages/ddtrace/llmobs/_writer.py", line 404, in dataset_batch_update resp = self.request("POST", path, body) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/gary.huang/go/src/github.com/DataDog/llm-observability/preview/experiments/notebooks/.venv/lib/python3.12/site-packages/ddtrace/llmobs/_writer.py", line 319, in request resp = conn.getresponse() ^^^^^^^^^^^^^^^^^^ File "/Users/gary.huang/.pyenv/versions/3.12.7/lib/python3.12/http/client.py", line 1428, in getresponse response.begin() File "/Users/gary.huang/.pyenv/versions/3.12.7/lib/python3.12/http/client.py", line 331, in begin version, status, reason = self._read_status() ^^^^^^^^^^^^^^^^^^^ File "/Users/gary.huang/.pyenv/versions/3.12.7/lib/python3.12/http/client.py", line 292, in _read_status line = str(self.fp.readline(_MAXLINE + 1), "iso-8859-1") ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/gary.huang/.pyenv/versions/3.12.7/lib/python3.12/socket.py", line 720, in readinto return self._sock.recv_into(b) ^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/gary.huang/.pyenv/versions/3.12.7/lib/python3.12/ssl.py", line 1251, in recv_into return self.read(nbytes, buffer) ^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/gary.huang/.pyenv/versions/3.12.7/lib/python3.12/ssl.py", line 1103, in read return self._sslobj.read(len, buffer) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ TimeoutError: The read operation timed out ``` now it succeeds https://dddev.datadoghq.com/llm/datasets/1e794d07-8b77-4a08-8363-e7236432f262 **case 2: large push after dataset is created** in cases where a push is extremely big, it used to fail before for example with this snippet ``` dataset = LLMObs.create_dataset("1-then-big-gh-09021126", "", [{"input_data": "first", "expected_output": "1"}]) print(dataset.as_dataframe()) print(dataset.url) for i in range(0, 25000): dataset.append({"input_data": "a"*5000, "expected_output": "b"*100}) dataset.push() ``` results in something like ``` 3.13.0rc1 input_data expected_output 0 first 1 https://app.datadoghq.com/llm/datasets/f9a2b6a7-9690-4e3d-98ff-e11b7832c344 Traceback (most recent call last): File "/Users/gary.huang/go/src/github.com/DataDog/llm-observability/preview/experiments/notebooks/test.py", line 33, in <module> dataset.push() File "/Users/gary.huang/go/src/github.com/DataDog/llm-observability/preview/experiments/notebooks/.venv/lib/python3.12/site-packages/ddtrace/llmobs/_experiment.py", line 140, in push new_version, new_record_ids = self._dne_client.dataset_batch_update( ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/gary.huang/go/src/github.com/DataDog/llm-observability/preview/experiments/notebooks/.venv/lib/python3.12/site-packages/ddtrace/llmobs/_writer.py", line 404, in dataset_batch_update resp = self.request("POST", path, body) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/gary.huang/go/src/github.com/DataDog/llm-observability/preview/experiments/notebooks/.venv/lib/python3.12/site-packages/ddtrace/llmobs/_writer.py", line 318, in request conn.request(method, url, encoded_body, headers) File "/Users/gary.huang/go/src/github.com/DataDog/llm-observability/preview/experiments/notebooks/.venv/lib/python3.12/site-packages/ddtrace/internal/http.py", line 37, in request return super().request(method, url, body=body, headers=_headers, encode_chunked=encode_chunked) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/gary.huang/.pyenv/versions/3.12.7/lib/python3.12/http/client.py", line 1336, in request self._send_request(method, url, body, headers, encode_chunked) File "/Users/gary.huang/.pyenv/versions/3.12.7/lib/python3.12/http/client.py", line 1382, in _send_request self.endheaders(body, encode_chunked=encode_chunked) File "/Users/gary.huang/.pyenv/versions/3.12.7/lib/python3.12/http/client.py", line 1331, in endheaders self._send_output(message_body, encode_chunked=encode_chunked) File "/Users/gary.huang/.pyenv/versions/3.12.7/lib/python3.12/http/client.py", line 1130, in _send_output self.send(chunk) File "/Users/gary.huang/.pyenv/versions/3.12.7/lib/python3.12/http/client.py", line 1055, in send self.sock.sendall(data) File "/Users/gary.huang/.pyenv/versions/3.12.7/lib/python3.12/ssl.py", line 1210, in sendall v = self.send(byte_view[count:]) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/gary.huang/.pyenv/versions/3.12.7/lib/python3.12/ssl.py", line 1179, in send return self._sslobj.write(data) ^^^^^^^^^^^^^^^^^^^^^^^^ TimeoutError: The write operation timed out ``` but with this change this succeeds by estimating the size of the dataset to upload and using the bulk endpoint if the dataset gets large enough https://dddev.datadoghq.com/llm/datasets/f9a2b6a7-9690-4e3d-98ff-e11b7832c344 **case 3: create dataset with a large list of records** ``` 3.13.0rc1 Traceback (most recent call last): File "/Users/gary.huang/go/src/github.com/DataDog/llm-observability/preview/experiments/notebooks/test.py", line 27, in <module> dataset = LLMObs.create_dataset("1-then-big-gh-09021127", "", recs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/gary.huang/go/src/github.com/DataDog/llm-observability/preview/experiments/notebooks/.venv/lib/python3.12/site-packages/ddtrace/llmobs/_llmobs.py", line 657, in create_dataset ds.push() File "/Users/gary.huang/go/src/github.com/DataDog/llm-observability/preview/experiments/notebooks/.venv/lib/python3.12/site-packages/ddtrace/llmobs/_experiment.py", line 140, in push new_version, new_record_ids = self._dne_client.dataset_batch_update( ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/gary.huang/go/src/github.com/DataDog/llm-observability/preview/experiments/notebooks/.venv/lib/python3.12/site-packages/ddtrace/llmobs/_writer.py", line 404, in dataset_batch_update resp = self.request("POST", path, body) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/gary.huang/go/src/github.com/DataDog/llm-observability/preview/experiments/notebooks/.venv/lib/python3.12/site-packages/ddtrace/llmobs/_writer.py", line 318, in request conn.request(method, url, encoded_body, headers) File "/Users/gary.huang/go/src/github.com/DataDog/llm-observability/preview/experiments/notebooks/.venv/lib/python3.12/site-packages/ddtrace/internal/http.py", line 37, in request return super().request(method, url, body=body, headers=_headers, encode_chunked=encode_chunked) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/gary.huang/.pyenv/versions/3.12.7/lib/python3.12/http/client.py", line 1336, in request self._send_request(method, url, body, headers, encode_chunked) File "/Users/gary.huang/.pyenv/versions/3.12.7/lib/python3.12/http/client.py", line 1382, in _send_request self.endheaders(body, encode_chunked=encode_chunked) File "/Users/gary.huang/.pyenv/versions/3.12.7/lib/python3.12/http/client.py", line 1331, in endheaders self._send_output(message_body, encode_chunked=encode_chunked) File "/Users/gary.huang/.pyenv/versions/3.12.7/lib/python3.12/http/client.py", line 1130, in _send_output self.send(chunk) File "/Users/gary.huang/.pyenv/versions/3.12.7/lib/python3.12/http/client.py", line 1055, in send self.sock.sendall(data) File "/Users/gary.huang/.pyenv/versions/3.12.7/lib/python3.12/ssl.py", line 1210, in sendall v = self.send(byte_view[count:]) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/Users/gary.huang/.pyenv/versions/3.12.7/lib/python3.12/ssl.py", line 1179, in send return self._sslobj.write(data) ^^^^^^^^^^^^^^^^^^^^^^^^ TimeoutError: The write operation timed out ``` it now succeeds https://dddev.datadoghq.com/llm/datasets/4b4af632-a51b-4a18-9393-f0a5dfe8a2b5 ## Checklist - [x] PR author has checked that all the criteria below are met - The PR description includes an overview of the change - The PR description articulates the motivation for the change - The change includes tests OR the PR description describes a testing strategy - The PR description notes risks associated with the change, if any - Newly-added code is easy to change - The change follows the [library release note guidelines](https://ddtrace.readthedocs.io/en/stable/releasenotes.html) - The change includes or references documentation updates if necessary - Backport labels are set (if [applicable](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting)) ## Reviewer Checklist - [x] Reviewer has checked that all the criteria below are met - Title is accurate - All changes are related to the pull request's stated goal - Avoids breaking [API](https://ddtrace.readthedocs.io/en/stable/versioning.html#interfaces) changes - Testing strategy adequately addresses listed risks - Newly-added code is easy to change - Release note makes sense to a user of the library - If necessary, author has acknowledged and discussed the performance implications of this PR as reported in the benchmarks PR comment - Backport labels are set in a manner that is consistent with the [release branch maintenance policy](https://ddtrace.readthedocs.io/en/latest/contributing.html#backporting) --------- Co-authored-by: Yun Kim <[email protected]>
1 parent a8ccafd commit 21d569a

10 files changed

+562
-151
lines changed

ddtrace/llmobs/_experiment.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,8 @@ class Dataset:
101101
_updated_record_ids_to_new_fields: Dict[str, UpdatableDatasetRecord]
102102
_deleted_record_ids: List[str]
103103

104+
BATCH_UPDATE_THRESHOLD = 5 * 1024 * 1024 # 5MB
105+
104106
def __init__(
105107
self,
106108
name: str,
@@ -136,17 +138,24 @@ def push(self) -> None:
136138
)
137139
)
138140

139-
updated_records = list(self._updated_record_ids_to_new_fields.values())
140-
new_version, new_record_ids = self._dne_client.dataset_batch_update(
141-
self._id, list(self._new_records_by_record_id.values()), updated_records, self._deleted_record_ids
142-
)
141+
delta_size = self._estimate_delta_size()
142+
if delta_size > self.BATCH_UPDATE_THRESHOLD:
143+
logger.debug("dataset delta is %d, using bulk upload", delta_size)
144+
# TODO must return version too
145+
self._dne_client.dataset_bulk_upload(self._id, self._records)
146+
else:
147+
logger.debug("dataset delta is %d, using batch update", delta_size)
148+
updated_records = list(self._updated_record_ids_to_new_fields.values())
149+
new_version, new_record_ids = self._dne_client.dataset_batch_update(
150+
self._id, list(self._new_records_by_record_id.values()), updated_records, self._deleted_record_ids
151+
)
143152

144-
# attach record ids to newly created records
145-
for record, record_id in zip(self._new_records_by_record_id.values(), new_record_ids):
146-
record["record_id"] = record_id # type: ignore
153+
# attach record ids to newly created records
154+
for record, record_id in zip(self._new_records_by_record_id.values(), new_record_ids):
155+
record["record_id"] = record_id # type: ignore
147156

148-
# FIXME: we don't get version numbers in responses to deletion requests
149-
self._version = new_version if new_version != -1 else self._version + 1
157+
# FIXME: we don't get version numbers in responses to deletion requests
158+
self._version = new_version if new_version != -1 else self._version + 1
150159
self._new_records_by_record_id = {}
151160
self._deleted_record_ids = []
152161
self._updated_record_ids_to_new_fields = {}
@@ -203,6 +212,12 @@ def url(self) -> str:
203212
# FIXME: will not work for subdomain orgs
204213
return f"{_get_base_url()}/llm/datasets/{self._id}"
205214

215+
def _estimate_delta_size(self) -> int:
216+
"""rough estimate (in bytes) of the size of the next batch update call if it happens"""
217+
size = len(safe_json(self._new_records_by_record_id)) + len(safe_json(self._updated_record_ids_to_new_fields))
218+
logger.debug("estimated delta size %d", size)
219+
return size
220+
206221
@overload
207222
def __getitem__(self, index: int) -> DatasetRecord:
208223
...

ddtrace/llmobs/_llmobs.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -720,7 +720,7 @@ def create_dataset_from_csv(
720720
csv.field_size_limit(original_field_size_limit)
721721

722722
if len(ds) > 0:
723-
ds.push()
723+
cls._instance._dne_client.dataset_bulk_upload(ds._id, ds._records)
724724
return ds
725725

726726
@classmethod

ddtrace/llmobs/_writer.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import atexit
2+
import csv
23
import json
34
import os
5+
import tempfile
46
from typing import Any
57
from typing import Dict
68
from typing import List
@@ -300,6 +302,8 @@ class LLMObsExperimentsClient(BaseLLMObsWriter):
300302
AGENTLESS_BASE_URL = AGENTLESS_EXP_BASE_URL
301303
ENDPOINT = ""
302304
TIMEOUT = 5.0
305+
BULK_UPLOAD_TIMEOUT = 60.0
306+
SUPPORTED_UPLOAD_EXTS = {"csv"}
303307

304308
def request(self, method: str, path: str, body: JSONType = None) -> Response:
305309
headers = {
@@ -321,6 +325,23 @@ def request(self, method: str, path: str, body: JSONType = None) -> Response:
321325
finally:
322326
conn.close()
323327

328+
def multipart_request(self, method: str, path: str, content_type: str, body: bytes = b"") -> Response:
329+
headers = {
330+
"Content-Type": content_type,
331+
"DD-API-KEY": self._api_key,
332+
"DD-APPLICATION-KEY": self._app_key,
333+
}
334+
335+
conn = get_connection(url=self._intake, timeout=self.BULK_UPLOAD_TIMEOUT)
336+
try:
337+
url = self._intake + self._endpoint + path
338+
logger.debug("requesting %s, %s", url, content_type)
339+
conn.request(method, url, body, headers)
340+
resp = conn.getresponse()
341+
return Response.from_http_response(resp)
342+
finally:
343+
conn.close()
344+
324345
def dataset_delete(self, dataset_id: str) -> None:
325346
path = "/api/unstable/llm-obs/v1/datasets/delete"
326347
resp = self.request(
@@ -353,6 +374,8 @@ def dataset_create(self, name: str, description: str) -> Dataset:
353374
raise ValueError(f"Failed to create dataset {name}: {resp.status} {resp.get_json()}")
354375
response_data = resp.get_json()
355376
dataset_id = response_data["data"]["id"]
377+
if dataset_id is None or dataset_id == "":
378+
raise ValueError(f"unexpected dataset state, invalid ID (is None: {dataset_id is None})")
356379
curr_version = response_data["data"]["attributes"]["current_version"]
357380
return Dataset(name, dataset_id, [], description, curr_version, _dne_client=self)
358381

@@ -446,6 +469,57 @@ def dataset_get_with_records(self, name: str) -> Dataset:
446469
)
447470
return Dataset(name, dataset_id, class_records, dataset_description, curr_version, _dne_client=self)
448471

472+
def dataset_bulk_upload(self, dataset_id: str, records: List[DatasetRecord]):
473+
with tempfile.NamedTemporaryFile(suffix=".csv") as tmp:
474+
file_name = os.path.basename(tmp.name)
475+
file_name_parts = file_name.rsplit(".", 1)
476+
if len(file_name_parts) != 2:
477+
raise ValueError(f"invalid file {file_name} from {tmp.name}")
478+
479+
file_ext = file_name_parts[1]
480+
481+
if file_ext not in self.SUPPORTED_UPLOAD_EXTS:
482+
raise ValueError(f"{file_ext} files not supported")
483+
484+
with open(tmp.name, "w", newline="") as csv_file:
485+
field_names = ["input", "expected_output", "metadata"]
486+
writer = csv.writer(csv_file)
487+
writer.writerow(field_names)
488+
for r in records:
489+
writer.writerow(
490+
[
491+
json.dumps(r.get("input_data", "")),
492+
json.dumps(r.get("expected_output", "")),
493+
json.dumps(r.get("metadata", "")),
494+
]
495+
)
496+
497+
with open(tmp.name, mode="rb") as f:
498+
file_content = f.read()
499+
500+
path = f"/api/unstable/llm-obs/v1/datasets/{dataset_id}/records/upload"
501+
BOUNDARY = b"----------boundary------"
502+
CRLF = b"\r\n"
503+
504+
body = CRLF.join(
505+
[
506+
b"--" + BOUNDARY,
507+
b'Content-Disposition: form-data; name="file"; filename="%s"' % file_name.encode("utf-8"),
508+
b"Content-Type: text/%s" % file_ext.encode("utf-8"),
509+
b"",
510+
file_content,
511+
b"--" + BOUNDARY + b"--",
512+
b"",
513+
]
514+
)
515+
516+
resp = self.multipart_request(
517+
"POST", path, content_type="multipart/form-data; boundary=%s" % BOUNDARY.decode("utf-8"), body=body
518+
)
519+
if resp.status != 200:
520+
raise ValueError(f"Failed to upload dataset from file: {resp.status} {resp.get_json()}")
521+
logger.debug("successfully uploaded with code %d", resp.status)
522+
449523
def project_create_or_get(self, name: str) -> str:
450524
path = "/api/unstable/llm-obs/v1/projects"
451525
resp = self.request(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
interactions:
2+
- request:
3+
body: "------------boundary------\r\nContent-Disposition: form-data; name=\"file\";
4+
filename=\"tmp.csv\"\r\nContent-Type: text/csv\r\n\r\ninput,expected_output,metadata\r\n\"{\"\"in0\"\":
5+
\"\"r0v1\"\", \"\"in1\"\": \"\"r0v2\"\", \"\"in2\"\": \"\"r0v3\"\"}\",\"{\"\"out0\"\":
6+
\"\"r0v4\"\", \"\"out1\"\": \"\"r0v5\"\"}\",\"{\"\"m0\"\": \"\"r0v6\"\"}\"\r\n\"{\"\"in0\"\":
7+
\"\"r1v1\"\", \"\"in1\"\": \"\"r1v2\"\", \"\"in2\"\": \"\"r1v3\"\"}\",\"{\"\"out0\"\":
8+
\"\"r1v4\"\", \"\"out1\"\": \"\"r1v5\"\"}\",\"{\"\"m0\"\": \"\"r1v6\"\"}\"\r\n\r\n------------boundary--------\r\n"
9+
headers:
10+
Accept:
11+
- '*/*'
12+
? !!python/object/apply:multidict._multidict.istr
13+
- Accept-Encoding
14+
: - identity
15+
Connection:
16+
- keep-alive
17+
Content-Length:
18+
- '433'
19+
? !!python/object/apply:multidict._multidict.istr
20+
- Content-Type
21+
: - multipart/form-data; boundary=----------boundary------
22+
User-Agent:
23+
- python-requests/2.32.3
24+
method: POST
25+
uri: https://api.datadoghq.com/api/unstable/llm-obs/v1/datasets/7ebcf701-2bd3-42d4-9dbd-01bb1169e66f/records/upload
26+
response:
27+
body:
28+
string: ''
29+
headers:
30+
content-length:
31+
- '0'
32+
content-security-policy:
33+
- frame-ancestors 'self'; report-uri https://logs.browser-intake-datadoghq.com/api/v2/logs?dd-api-key=pube4f163c23bbf91c16b8f57f56af9fc58&dd-evp-origin=content-security-policy&ddsource=csp-report&ddtags=site%3Adatadoghq.com
34+
content-type:
35+
- application/vnd.api+json
36+
date:
37+
- Tue, 02 Sep 2025 22:41:52 GMT
38+
strict-transport-security:
39+
- max-age=31536000; includeSubDomains; preload
40+
vary:
41+
- Accept-Encoding
42+
x-content-type-options:
43+
- nosniff
44+
x-frame-options:
45+
- SAMEORIGIN
46+
status:
47+
code: 200
48+
message: OK
49+
version: 1
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
interactions:
2+
- request:
3+
body: '{"data": {"type": "datasets", "attributes": {"type": "soft", "dataset_ids":
4+
["ed2f83c9-dd94-41d7-ac2b-544bbdbb5584"]}}}'
5+
headers:
6+
Accept:
7+
- '*/*'
8+
? !!python/object/apply:multidict._multidict.istr
9+
- Accept-Encoding
10+
: - identity
11+
Connection:
12+
- keep-alive
13+
Content-Length:
14+
- '119'
15+
? !!python/object/apply:multidict._multidict.istr
16+
- Content-Type
17+
: - application/json
18+
User-Agent:
19+
- python-requests/2.32.3
20+
method: POST
21+
uri: https://api.datadoghq.com/api/unstable/llm-obs/v1/datasets/delete
22+
response:
23+
body:
24+
string: '{"data":[{"id":"ed2f83c9-dd94-41d7-ac2b-544bbdbb5584","type":"datasets","attributes":{"author":{"id":"de473b30-eb9f-11e9-a77a-c7405862b8bd"},"created_at":"2025-09-03T23:16:13.87366Z","current_version":0,"deleted_at":"2025-09-03T23:16:14.119761Z","description":"A
25+
test dataset","name":"test-dataset-test_dataset_estimate_size","updated_at":"2025-09-03T23:16:13.87366Z"}}]}'
26+
headers:
27+
content-length:
28+
- '371'
29+
content-security-policy:
30+
- frame-ancestors 'self'; report-uri https://logs.browser-intake-datadoghq.com/api/v2/logs?dd-api-key=pube4f163c23bbf91c16b8f57f56af9fc58&dd-evp-origin=content-security-policy&ddsource=csp-report&ddtags=site%3Adatadoghq.com
31+
content-type:
32+
- application/vnd.api+json
33+
date:
34+
- Wed, 03 Sep 2025 23:16:14 GMT
35+
strict-transport-security:
36+
- max-age=31536000; includeSubDomains; preload
37+
vary:
38+
- Accept-Encoding
39+
x-content-type-options:
40+
- nosniff
41+
x-frame-options:
42+
- SAMEORIGIN
43+
status:
44+
code: 200
45+
message: OK
46+
version: 1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
interactions:
2+
- request:
3+
body: "------------boundary------\r\nContent-Disposition: form-data; name=\"file\";
4+
filename=\"tmp.csv\"\r\nContent-Type: text/csv\r\n\r\ninput,expected_output,metadata\r\n\"{\"\"in0\"\":
5+
\"\"r0v1\"\", \"\"in1\"\": \"\"r0v2\"\", \"\"in2\"\": \"\"r0v3\"\"}\",\"{\"\"out0\"\":
6+
\"\"r0v4\"\", \"\"out1\"\": \"\"r0v5\"\"}\",{}\r\n\"{\"\"in0\"\": \"\"r1v1\"\",
7+
\"\"in1\"\": \"\"r1v2\"\", \"\"in2\"\": \"\"r1v3\"\"}\",\"{\"\"out0\"\": \"\"r1v4\"\",
8+
\"\"out1\"\": \"\"r1v5\"\"}\",{}\r\n\r\n------------boundary--------\r\n"
9+
headers:
10+
Accept:
11+
- '*/*'
12+
? !!python/object/apply:multidict._multidict.istr
13+
- Accept-Encoding
14+
: - identity
15+
Connection:
16+
- keep-alive
17+
Content-Length:
18+
- '397'
19+
? !!python/object/apply:multidict._multidict.istr
20+
- Content-Type
21+
: - multipart/form-data; boundary=----------boundary------
22+
User-Agent:
23+
- python-requests/2.32.3
24+
method: POST
25+
uri: https://api.datadoghq.com/api/unstable/llm-obs/v1/datasets/e03e6dcc-1a71-49ed-82b0-0042d0f7e117/records/upload
26+
response:
27+
body:
28+
string: ''
29+
headers:
30+
content-length:
31+
- '0'
32+
content-security-policy:
33+
- frame-ancestors 'self'; report-uri https://logs.browser-intake-datadoghq.com/api/v2/logs?dd-api-key=pube4f163c23bbf91c16b8f57f56af9fc58&dd-evp-origin=content-security-policy&ddsource=csp-report&ddtags=site%3Adatadoghq.com
34+
content-type:
35+
- application/vnd.api+json
36+
date:
37+
- Tue, 02 Sep 2025 22:41:49 GMT
38+
strict-transport-security:
39+
- max-age=31536000; includeSubDomains; preload
40+
vary:
41+
- Accept-Encoding
42+
x-content-type-options:
43+
- nosniff
44+
x-frame-options:
45+
- SAMEORIGIN
46+
status:
47+
code: 200
48+
message: OK
49+
version: 1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
interactions:
2+
- request:
3+
body: "------------boundary------\r\nContent-Disposition: form-data; name=\"file\";
4+
filename=\"tmp.csv\"\r\nContent-Type: text/csv\r\n\r\ninput,expected_output,metadata\r\n\"{\"\"in0\"\":
5+
\"\"r0v1\"\", \"\"in1\"\": \"\"r0v2\"\", \"\"in2\"\": \"\"r0v3\"\"}\",\"{\"\"out0\"\":
6+
\"\"r0v4\"\", \"\"out1\"\": \"\"r0v5\"\"}\",\"{\"\"m0\"\": \"\"r0v6\"\"}\"\r\n\"{\"\"in0\"\":
7+
\"\"r1v1\"\", \"\"in1\"\": \"\"r1v2\"\", \"\"in2\"\": \"\"r1v3\"\"}\",\"{\"\"out0\"\":
8+
\"\"r1v4\"\", \"\"out1\"\": \"\"r1v5\"\"}\",\"{\"\"m0\"\": \"\"r1v6\"\"}\"\r\n\r\n------------boundary--------\r\n"
9+
headers:
10+
Accept:
11+
- '*/*'
12+
? !!python/object/apply:multidict._multidict.istr
13+
- Accept-Encoding
14+
: - identity
15+
Connection:
16+
- keep-alive
17+
Content-Length:
18+
- '433'
19+
? !!python/object/apply:multidict._multidict.istr
20+
- Content-Type
21+
: - multipart/form-data; boundary=----------boundary------
22+
User-Agent:
23+
- python-requests/2.32.3
24+
method: POST
25+
uri: https://api.datadoghq.com/api/unstable/llm-obs/v1/datasets/e03e6dcc-1a71-49ed-82b0-0042d0f7e117/records/upload
26+
response:
27+
body:
28+
string: ''
29+
headers:
30+
content-length:
31+
- '0'
32+
content-security-policy:
33+
- frame-ancestors 'self'; report-uri https://logs.browser-intake-datadoghq.com/api/v2/logs?dd-api-key=pube4f163c23bbf91c16b8f57f56af9fc58&dd-evp-origin=content-security-policy&ddsource=csp-report&ddtags=site%3Adatadoghq.com
34+
content-type:
35+
- application/vnd.api+json
36+
date:
37+
- Tue, 02 Sep 2025 22:41:41 GMT
38+
strict-transport-security:
39+
- max-age=31536000; includeSubDomains; preload
40+
vary:
41+
- Accept-Encoding
42+
x-content-type-options:
43+
- nosniff
44+
x-frame-options:
45+
- SAMEORIGIN
46+
status:
47+
code: 200
48+
message: OK
49+
version: 1

0 commit comments

Comments
 (0)