Skip to content
Merged
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,10 @@ Please see [SECURITY.md](docs/SECURITY.md) for our security policy and reporting

We welcome contributions! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to get started, our code of conduct, and the pull request process.

## Star History

[![Star History Chart](https://api.star-history.com/svg?repos=usnavy13/LibreCodeInterpreter&type=Date)](https://star-history.com/#usnavy13/LibreCodeInterpreter&Date)

## License

This project is licensed under the Apache License 2.0 - see the [LICENSE](LICENSE) file for details.
20 changes: 19 additions & 1 deletion src/api/files.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ async def upload_file(
# Note: Production API returns different format with fileId instead of id
return {
"message": "success",
"storage_session_id": session_id,
"session_id": session_id,
"files": [
{"filename": file["name"], "fileId": file["id"]}
Expand Down Expand Up @@ -249,7 +250,10 @@ async def upload_files_batch(
entity_id: Optional[str] = (
entity_id_raw if isinstance(entity_id_raw, str) and entity_id_raw else None
)
is_agent_file = entity_id is not None
kind_raw = form.get("kind")
is_agent_file = entity_id is not None or (
isinstance(kind_raw, str) and kind_raw in ("skill", "agent")
)

read_only_raw = form.get("read_only")
is_read_only = isinstance(read_only_raw, str) and read_only_raw.lower() in (
Expand Down Expand Up @@ -339,6 +343,7 @@ async def upload_files_batch(

return {
"message": message,
"storage_session_id": session_id,
"session_id": session_id,
"files": results,
"succeeded": succeeded,
Expand All @@ -353,6 +358,18 @@ async def list_files(
None,
description="Detail level: 'simple' for basic info, otherwise full details",
),
kind: Optional[str] = Query(
None,
description="Resource kind filter: 'skill', 'agent', or 'user'",
),
id: Optional[str] = Query(
None,
description="Resource id for scoped file listing",
),
version: Optional[int] = Query(
None,
description="Resource version (only meaningful when kind=skill)",
),
file_service: FileServiceDep = None,
):
"""List all files in a session with optional detail parameter - LibreChat compatible."""
Expand Down Expand Up @@ -408,6 +425,7 @@ async def list_files(
{
"name": f"{session_id}/{file_info.file_id}",
"id": file_info.file_id,
"storage_session_id": session_id,
"session_id": session_id,
"content": None, # Not returned in list
"size": file_info.size,
Expand Down
25 changes: 21 additions & 4 deletions src/models/exec.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,45 @@
from typing import Dict, List, Optional, Any

# Third-party imports
from pydantic import BaseModel, Field
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, computed_field


class FileRef(BaseModel):
"""File reference model for execution response."""

model_config = ConfigDict(populate_by_name=True)

id: str
name: str
path: Optional[str] = None # Make path optional
session_id: Optional[str] = None # Session ID for cross-message file persistence
path: Optional[str] = None
session_id: Optional[str] = None
inherited: Optional[bool] = None
entity_id: Optional[str] = None
resource_id: Optional[str] = None
kind: Optional[str] = None
version: Optional[int] = None
modified_from: Optional[Dict[str, str]] = None

@computed_field # type: ignore[prop-decorator]
@property
def storage_session_id(self) -> Optional[str]:
return self.session_id


class RequestFile(BaseModel):
"""Request file model."""

model_config = ConfigDict(populate_by_name=True)

id: str
session_id: str
session_id: str = Field(
validation_alias=AliasChoices("storage_session_id", "session_id"),
)
name: str
entity_id: Optional[str] = None
resource_id: Optional[str] = None
kind: Optional[str] = None
version: Optional[int] = None


class ExecRequest(BaseModel):
Expand Down
15 changes: 12 additions & 3 deletions src/models/programmatic.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

from typing import Any, Dict, List, Optional

from pydantic import BaseModel, Field, validator
from pydantic import AliasChoices, BaseModel, ConfigDict, Field, validator

SUPPORTED_PTC_LANGUAGES = {"py", "bash"}

Expand Down Expand Up @@ -51,12 +51,21 @@ class PTCFileInput(BaseModel):
"""File payload for PTC initial execution.

Matches the LibreChat/librechat-agents CodeEnvFile shape:
{session_id, id, name}
{storage_session_id, id, name}
"""

model_config = ConfigDict(populate_by_name=True)

id: str = Field(..., description="File identifier")
name: str = Field(..., description="Original filename for the referenced file")
session_id: str = Field(..., description="Source session for a referenced file")
session_id: str = Field(
...,
description="Source session for a referenced file",
validation_alias=AliasChoices("storage_session_id", "session_id"),
)
resource_id: Optional[str] = None
kind: Optional[str] = None
version: Optional[int] = None


class ProgrammaticExecRequest(BaseModel):
Expand Down
3 changes: 2 additions & 1 deletion src/services/orchestrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -786,7 +786,8 @@ async def _handle_generated_files(self, ctx: ExecutionContext) -> List[FileRef]:
if meta.get("modified_from_id"):
file_ref.modified_from = {
"id": meta["modified_from_id"],
"session_id": meta.get("modified_from_session_id") or "",
"storage_session_id": meta.get("modified_from_session_id")
or "",
}
generated.append(file_ref)
logger.debug(
Expand Down
10 changes: 5 additions & 5 deletions tests/functional/test_client_replay.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def _normalize_artifact_files(result: dict) -> list[dict]:
session_id = result["session_id"]
return [
{
"session_id": file_info.get("session_id") or session_id,
"storage_session_id": file_info.get("storage_session_id") or session_id,
"id": file_info["id"],
"name": file_info["name"],
}
Expand All @@ -36,7 +36,7 @@ async def _fetch_runtime_file_refs(
file_id = name_parts[1].split(".")[0] if len(name_parts) > 1 else ""
file_references.append(
{
"session_id": session_id,
"storage_session_id": session_id,
"id": file_id,
"name": file_info["metadata"]["original-filename"],
}
Expand Down Expand Up @@ -128,7 +128,7 @@ async def test_uploaded_files_follow_runtime_session_when_first_exec_has_no_outp
data={"entity_id": unique_entity_id},
)
assert upload.status_code == 200, upload.text
upload_session_id = upload.json()["session_id"]
upload_session_id = upload.json()["storage_session_id"]
upload_refs = await _fetch_runtime_file_refs(
async_client, auth_headers, upload_session_id
)
Expand Down Expand Up @@ -164,7 +164,7 @@ async def test_uploaded_files_survive_runtime_fallback_after_outputs_are_generat
data={"entity_id": unique_entity_id},
)
assert upload.status_code == 200, upload.text
upload_session_id = upload.json()["session_id"]
upload_session_id = upload.json()["storage_session_id"]
upload_refs = await _fetch_runtime_file_refs(
async_client, auth_headers, upload_session_id
)
Expand Down Expand Up @@ -298,7 +298,7 @@ async def test_ptc_fallback_refs_preserve_files_after_continuation(
)
assert upload.status_code == 200, upload.text
upload_result = upload.json()
session_id = upload_result["session_id"]
session_id = upload_result["storage_session_id"]

initial = await _start_ptc_like_runtime(
async_client,
Expand Down
4 changes: 2 additions & 2 deletions tests/functional/test_concurrent_file_exec.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ async def test_large_file_exec_does_not_block_concurrent_requests(
assert upload_resp.status_code == 200, f"Upload failed: {upload_resp.text}"

result = upload_resp.json()
session_id = result["session_id"]
session_id = result["storage_session_id"]
file_id = result["files"][0]["fileId"]
filename = result["files"][0]["filename"]

Expand All @@ -72,7 +72,7 @@ async def exec_with_file(idx: int) -> tuple:
"lang": "py",
"session_id": session_id,
"files": [
{"id": file_id, "session_id": session_id, "name": filename}
{"id": file_id, "storage_session_id": session_id, "name": filename}
],
},
timeout=60.0,
Expand Down
6 changes: 3 additions & 3 deletions tests/functional/test_exec_workflow.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ async def test_file_ref_does_not_leak_session_across_users(
)
assert upload.status_code == 200
upload_data = upload.json()
upload_session = upload_data["session_id"]
upload_session = upload_data["storage_session_id"]
file_id = upload_data["files"][0]["fileId"]
filename = upload_data["files"][0]["filename"]

Expand All @@ -187,7 +187,7 @@ async def test_file_ref_does_not_leak_session_across_users(
"files": [
{
"id": file_id,
"session_id": upload_session,
"storage_session_id": upload_session,
"name": filename,
}
],
Expand All @@ -208,7 +208,7 @@ async def test_file_ref_does_not_leak_session_across_users(
"files": [
{
"id": file_id,
"session_id": upload_session,
"storage_session_id": upload_session,
"name": filename,
}
],
Expand Down
34 changes: 17 additions & 17 deletions tests/functional/test_files.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ async def test_upload_single_file(
result = response.json()

assert result["message"] == "success"
assert "session_id" in result
assert "storage_session_id" in result
assert len(result["files"]) == 1
assert "fileId" in result["files"][0]
assert "filename" in result["files"][0]
Expand All @@ -50,10 +50,10 @@ async def test_librechat_upload_format(
assert response.json()["message"] == "success"

@pytest.mark.asyncio
async def test_upload_returns_session_id(
async def test_upload_returns_storage_session_id(
self, async_client, auth_headers, unique_entity_id
):
"""Upload response includes session_id."""
"""Upload response includes storage_session_id."""
files = {"files": ("test.txt", b"content", "text/plain")}
data = {"entity_id": unique_entity_id}

Expand All @@ -65,8 +65,8 @@ async def test_upload_returns_session_id(
)

result = response.json()
assert "session_id" in result
assert len(result["session_id"]) > 0
assert "storage_session_id" in result
assert len(result["storage_session_id"]) > 0

@pytest.mark.asyncio
async def test_upload_returns_file_info(
Expand Down Expand Up @@ -120,7 +120,7 @@ async def test_list_files_after_upload(
files=files,
data={"entity_id": unique_entity_id},
)
session_id = upload.json()["session_id"]
session_id = upload.json()["storage_session_id"]

# List files
response = await async_client.get(
Expand All @@ -145,7 +145,7 @@ async def test_list_files_detail_simple(
files=files,
data={"entity_id": unique_entity_id},
)
session_id = upload.json()["session_id"]
session_id = upload.json()["storage_session_id"]

# List with simple detail
response = await async_client.get(
Expand All @@ -170,7 +170,7 @@ async def test_list_files_detail_summary(
files=files,
data={"entity_id": unique_entity_id},
)
session_id = upload.json()["session_id"]
session_id = upload.json()["storage_session_id"]

# List with summary detail
response = await async_client.get(
Expand Down Expand Up @@ -204,7 +204,7 @@ async def test_detail_full_has_original_filename_metadata(
data={"entity_id": unique_entity_id},
)
assert upload.status_code == 200
session_id = upload.json()["session_id"]
session_id = upload.json()["storage_session_id"]

# Get full detail
response = await async_client.get(
Expand Down Expand Up @@ -237,7 +237,7 @@ async def test_detail_full_has_required_fields(
files=files,
data={"entity_id": unique_entity_id},
)
session_id = upload.json()["session_id"]
session_id = upload.json()["storage_session_id"]

response = await async_client.get(
f"/files/{session_id}?detail=full",
Expand Down Expand Up @@ -278,7 +278,7 @@ async def test_download_uploaded_file(
data={"entity_id": unique_entity_id},
)

session_id = upload.json()["session_id"]
session_id = upload.json()["storage_session_id"]
file_id = upload.json()["files"][0]["fileId"]

response = await async_client.get(
Expand Down Expand Up @@ -322,7 +322,7 @@ async def test_uploaded_file_readable_at_mnt_data(
)
assert upload.status_code == 200
upload_data = upload.json()
session_id = upload_data["session_id"]
session_id = upload_data["storage_session_id"]
file_id = upload_data["files"][0]["fileId"]
filename = upload_data["files"][0]["filename"]

Expand All @@ -341,7 +341,7 @@ async def test_uploaded_file_readable_at_mnt_data(
),
"lang": "py",
"session_id": session_id,
"files": [{"id": file_id, "session_id": session_id, "name": filename}],
"files": [{"id": file_id, "storage_session_id": session_id, "name": filename}],
},
)

Expand All @@ -366,7 +366,7 @@ async def test_uploaded_file_readable_via_relative_path(
data={"entity_id": unique_entity_id},
)
upload_data = upload.json()
session_id = upload_data["session_id"]
session_id = upload_data["storage_session_id"]
file_id = upload_data["files"][0]["fileId"]
filename = upload_data["files"][0]["filename"]

Expand All @@ -377,7 +377,7 @@ async def test_uploaded_file_readable_via_relative_path(
"code": f"print(open('{filename}').read())",
"lang": "py",
"session_id": session_id,
"files": [{"id": file_id, "session_id": session_id, "name": filename}],
"files": [{"id": file_id, "storage_session_id": session_id, "name": filename}],
},
)

Expand All @@ -400,7 +400,7 @@ async def test_upload_execute_generate_download(
data={"entity_id": unique_entity_id},
)
upload_data = upload.json()
session_id = upload_data["session_id"]
session_id = upload_data["storage_session_id"]
file_id = upload_data["files"][0]["fileId"]
filename = upload_data["files"][0]["filename"]

Expand All @@ -424,7 +424,7 @@ async def test_upload_execute_generate_download(
),
"lang": "py",
"session_id": session_id,
"files": [{"id": file_id, "session_id": session_id, "name": filename}],
"files": [{"id": file_id, "storage_session_id": session_id, "name": filename}],
},
)

Expand Down
2 changes: 1 addition & 1 deletion tests/functional/test_generated_artifacts.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ async def test_generated_file_is_reused_on_follow_up_execution(
"files": [
{
"id": generated_file["id"],
"session_id": generate_result["session_id"],
"storage_session_id": generate_result["session_id"],
"name": generated_file["name"],
}
],
Expand Down
Loading