Skip to content

Sparshr04/litellm copilot integration#200

Open
Sparshr04 wants to merge 11 commits intodevelopfrom
sparshr04/litellm-copilot-integration
Open

Sparshr04/litellm copilot integration#200
Sparshr04 wants to merge 11 commits intodevelopfrom
sparshr04/litellm-copilot-integration

Conversation

@Sparshr04
Copy link
Contributor

@Sparshr04 Sparshr04 commented Mar 18, 2026

[Feature] Integrate LiteLLM and GitHub Copilot for streaming /chat

Description

This PR implements the backend architecture to route chat requests to GitHub Copilot's models (gpt-4o, text-embedding-3-small) using the litellm SDK, supporting SSE streaming and isolated user authentication.

Key Additions:

  • /chat Endpoint: Added api/handlers/v1/chat.py which accepts chat histories, handles optional RAG dataset context, and returns a Server-Sent Events (SSE) stream.
  • Async-Safe Token Injection: Refactored the LiteLLM implementation to pass the api_key explicitly via the function call rather than mutating os.environ, preventing cross-talk race conditions between concurrent users.
  • Device Flow Auth: Implemented GitHubDeviceFlowAuth to generate, poll, and store GitHub tokens securely in isolated user XDG directories (~/.extralit/data/users/{username}/...).
  • Environment Isolation: Extended LiteLLMContext to ensure any temporary files generated by the SDK are kept strictly within the authenticated user's directory.

Related Tickets & Documents

  • Refactor
  • Feature
  • Bug Fix
  • Optimization
  • Documentation Update

Steps to QA

  1. Ensure the server is running and you are authenticated with an Extralit JWT.
  2. Complete the GitHub Copilot Device flow via the /auth/github/login and /poll endpoints to generate a local github_token.json.
  3. Hit the POST /api/v1/chat endpoint with {"model": "copilot", "messages": [...], "stream": true}.
  4. Verify the response streams back chunk-by-chunk in SSE format (data: ...).

Copilot AI and others added 11 commits December 7, 2025 23:21
Co-authored-by: JonnyTran <4750391+JonnyTran@users.noreply.github.com>
Co-authored-by: JonnyTran <4750391+JonnyTran@users.noreply.github.com>
- Update `BaseSimpleTable` adapter to pass the full column configuration (including editor params) instead of just name/type.
- Update `RenderTable` logic to respect custom column settings (like `editor: "list"`) by prioritizing schema config over defaults.
- Add watcher to `columnsConfig` in `RenderTable` to ensure dropdown options update reactively when new files are uploaded.
- Add missing `$emit('cell-edited')` in `RenderTable`'s tabulator handler.
- Ensures parent components are notified of changes to trigger state updates like the unmapped files counter.
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements backend support for routing Extralit chat requests to GitHub Copilot models via LiteLLM (including GitHub OAuth Device Flow and SSE streaming), and adds frontend import-flow enhancements for PDF-only uploads.

Changes:

  • Added GitHub Device Flow auth helpers + /auth/github/* endpoints for per-user token storage.
  • Added /chat SSE streaming endpoint with optional RAG retrieval via vector search + embeddings.
  • Updated frontend import file-upload view model and added Jest tests; added litellm dependency.

Reviewed changes

Copilot reviewed 10 out of 11 changed files in this pull request and generated 16 comments.

Show a summary per file
File Description
extralit-server/src/extralit_server/utils/litellm_context.py Adds a context manager intended to isolate LiteLLM’s config/temp behavior per user.
extralit-server/src/extralit_server/utils/auth_helpers.py Implements GitHub Device Flow initiation/polling and local token persistence.
extralit-server/src/extralit_server/api/routes.py Registers the new GitHub auth + chat routers in API v1.
extralit-server/src/extralit_server/api/handlers/v1/github_auth.py Adds /auth/github/status, /login, /poll, /logout endpoints.
extralit-server/src/extralit_server/api/handlers/v1/chat.py Adds /chat streaming endpoint and optional RAG context retrieval.
extralit-server/pyproject.toml Adds LiteLLM dependency and minor formatting cleanup.
extralit-frontend/components/features/import/file-upload/useImportFileUploadViewModel.ts Adds editable-table support for PDF-only uploads and auto-sync to bibData.
extralit-frontend/components/features/import/file-upload/useImportFileUploadViewModel.spec.ts Adds unit tests for the updated import file-upload view model.
extralit-frontend/components/features/import/file-upload/ImportFileUpload.vue Registers BaseSimpleTable for the new editable-table UI.
copilot_changes.txt Adds a file containing pasted diffs (appears to be an artifact, not app code).

Comment on lines +117 to +118
expect(filesCol?.editorParams?.multiselect).toBe(true);
expect(filesCol?.editorParams?.values).toHaveLength(2);
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test treats editorParams as an object (editorParams.multiselect/values), but in the view model it’s a function returning the params at edit-time. As written, the assertions will always fail. Update the test to call editorParams with a mocked cell and assert on the returned object instead.

Suggested change
expect(filesCol?.editorParams?.multiselect).toBe(true);
expect(filesCol?.editorParams?.values).toHaveLength(2);
const mockCell = {} as any;
const editorParams = filesCol?.editorParams
? filesCol.editorParams(mockCell)
: undefined;
expect(editorParams).toBeDefined();
expect(editorParams?.multiselect).toBe(true);
expect(editorParams?.values).toHaveLength(2);

Copilot uses AI. Check for mistakes.
Comment on lines +63 to +67
def _get_config_dir(self) -> Path:
"""Get the user-specific config directory for token storage."""
config_path = Path(settings.home_path) / "data" / "users" / self.username / "config"
config_path.mkdir(parents=True, exist_ok=True)
return config_path
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Building filesystem paths directly from username allows path traversal (e.g., usernames containing ../ or path separators) and could write tokens outside the intended per-user directory. Use a stable, sanitized identifier (e.g., user UUID) for the directory name, or strictly validate/escape usernames before using them in Path joins.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +59
diff --git a/extralit-frontend/components/base/base-render-table/RenderTable.vue b/extralit-frontend/components/base/base-render-table/RenderTable.vue
index ff6874489..ab73e5afb 100644
--- a/extralit-frontend/components/base/base-render-table/RenderTable.vue
+++ b/extralit-frontend/components/base/base-render-table/RenderTable.vue
@@ -150,6 +150,7 @@ export default {
// }
// },
// },
+
validation: {
handler(newValidation, oldValidation) {
if (this.isLoaded) {
@@ -196,7 +197,7 @@ export default {
var configs = this.tableJSON.schema.fields.map((column: DataFrameField) => {
const commonConfig = this.generateColumnConfig(column.name);
const editableConfig = this.generateColumnEditableConfig(column.name);
- return { ...commonConfig, ...editableConfig };
+ return { ...commonConfig, ...editableConfig, ...column };
});

if (!this.editable) {
@@ -913,6 +914,7 @@ export default {

this.tabulator.on("cellEdited", (cell: CellComponent) => {
this.updateTableJsonData();
+ this.$emit("cell-edited", cell);
// const rowPos: number | boolean = cell.getRow().getPosition();
// if (typeof rowPos != 'number' || rowPos < 0 || rowPos > this.tableJSON.data.length) return;
// this.$set(this.tableJSON.data[rowPos-1], cell.getColumn().getField(), cell.getValue());
diff --git a/extralit-frontend/components/base/base-simple-table/BaseSimpleTable.vue b/extralit-frontend/components/base/base-simple-table/BaseSimpleTable.vue
index 15a62bbc4..3a7e5435a 100644
--- a/extralit-frontend/components/base/base-simple-table/BaseSimpleTable.vue
+++ b/extralit-frontend/components/base/base-simple-table/BaseSimpleTable.vue
@@ -6,6 +6,7 @@
:editable="editable"
:hasValidValues="hasValidValues"
:questions="questions"
+ :validation="validation || validators"
@table-built="$emit('table-built')"
@row-click="(e, row) => $emit('row-click', e, row)"
@cell-edited="(cell) => $emit('cell-edited', cell)"
@@ -71,9 +72,11 @@ export default {
computed: {
// Convert simple data/columns to TableData format for RenderTable
computedTableJSON(): TableData {
+ // FIX 1: Use "...col" to preserve editor config (dropdowns), validators, and freezing
const fields = this.columns.map((col: any) => ({
name: col.field,
type: col.type || "string",
+ ...col // <--- THIS IS THE MAGIC FIX
}));

const schema = new DataFrameSchema(
@@ -89,7 +92,14 @@ export default {
null
);

- if (this.validation) {
+ // FIX 2: Handle both 'validation' and 'validators' props
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copilot_changes.txt appears to be a pasted diff of other files rather than source code used by the app. Keeping it in the repo will confuse future maintenance and inflate the codebase; it should likely be removed or moved to PR notes/attachments instead.

Suggested change
diff --git a/extralit-frontend/components/base/base-render-table/RenderTable.vue b/extralit-frontend/components/base/base-render-table/RenderTable.vue
index ff6874489..ab73e5afb 100644
--- a/extralit-frontend/components/base/base-render-table/RenderTable.vue
+++ b/extralit-frontend/components/base/base-render-table/RenderTable.vue
@@ -150,6 +150,7 @@ export default {
// }
// },
// },
+
validation: {
handler(newValidation, oldValidation) {
if (this.isLoaded) {
@@ -196,7 +197,7 @@ export default {
var configs = this.tableJSON.schema.fields.map((column: DataFrameField) => {
const commonConfig = this.generateColumnConfig(column.name);
const editableConfig = this.generateColumnEditableConfig(column.name);
- return { ...commonConfig, ...editableConfig };
+ return { ...commonConfig, ...editableConfig, ...column };
});
if (!this.editable) {
@@ -913,6 +914,7 @@ export default {
this.tabulator.on("cellEdited", (cell: CellComponent) => {
this.updateTableJsonData();
+ this.$emit("cell-edited", cell);
// const rowPos: number | boolean = cell.getRow().getPosition();
// if (typeof rowPos != 'number' || rowPos < 0 || rowPos > this.tableJSON.data.length) return;
// this.$set(this.tableJSON.data[rowPos-1], cell.getColumn().getField(), cell.getValue());
diff --git a/extralit-frontend/components/base/base-simple-table/BaseSimpleTable.vue b/extralit-frontend/components/base/base-simple-table/BaseSimpleTable.vue
index 15a62bbc4..3a7e5435a 100644
--- a/extralit-frontend/components/base/base-simple-table/BaseSimpleTable.vue
+++ b/extralit-frontend/components/base/base-simple-table/BaseSimpleTable.vue
@@ -6,6 +6,7 @@
:editable="editable"
:hasValidValues="hasValidValues"
:questions="questions"
+ :validation="validation || validators"
@table-built="$emit('table-built')"
@row-click="(e, row) => $emit('row-click', e, row)"
@cell-edited="(cell) => $emit('cell-edited', cell)"
@@ -71,9 +72,11 @@ export default {
computed: {
// Convert simple data/columns to TableData format for RenderTable
computedTableJSON(): TableData {
+ // FIX 1: Use "...col" to preserve editor config (dropdowns), validators, and freezing
const fields = this.columns.map((col: any) => ({
name: col.field,
type: col.type || "string",
+ ...col // <--- THIS IS THE MAGIC FIX
}));
const schema = new DataFrameSchema(
@@ -89,7 +92,14 @@ export default {
null
);
- if (this.validation) {
+ // FIX 2: Handle both 'validation' and 'validators' props
# This file previously contained a pasted git diff of frontend changes.
# It has been cleared because diffs belong in version control history and
# keeping them here confuses static analysis tools and future maintainers.
#
# If you need to document code review notes or Copilot suggestions,
# please use pull request descriptions or separate documentation files.

Copilot uses AI. Check for mistakes.
Comment on lines +1 to +10
diff --git a/extralit-frontend/components/base/base-render-table/RenderTable.vue b/extralit-frontend/components/base/base-render-table/RenderTable.vue
index ff6874489..ab73e5afb 100644
--- a/extralit-frontend/components/base/base-render-table/RenderTable.vue
+++ b/extralit-frontend/components/base/base-render-table/RenderTable.vue
@@ -150,6 +150,7 @@ export default {
// }
// },
// },
+
validation: {
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description is focused on GitHub Copilot/LiteLLM /chat streaming, but this change set also includes substantial, unrelated frontend import-table work (and an extra copilot_changes.txt diff artifact). Please either update the PR description/scope to cover these additions or split them into a separate PR to keep review and rollback risk manageable.

Copilot uses AI. Check for mistakes.
Comment on lines +138 to +142
# Poll with a short timeout (single attempt)
token_data = await github_auth.poll_for_token(
device_code=request.device_code,
interval=5,
timeout=10, # Just one quick check
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/auth/github/poll calls poll_for_token() which contains an internal sleep/retry loop; with timeout=10 and interval=5 this endpoint can block a request for up to ~10s each poll. It’s usually better for the HTTP endpoint to do a single token check per request (no sleeps) and let the client handle polling cadence, to avoid tying up server workers.

Suggested change
# Poll with a short timeout (single attempt)
token_data = await github_auth.poll_for_token(
device_code=request.device_code,
interval=5,
timeout=10, # Just one quick check
# Perform a single non-blocking check for the token (no internal sleep/retry)
token_data = await github_auth.poll_for_token(
device_code=request.device_code,
interval=0,
timeout=0,

Copilot uses AI. Check for mistakes.
class DeviceFlowResponse(BaseModel):
"""Response model for device flow initiation."""

device_code: str # <--- ADDED THIS FIELD
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The inline “<--- ADDED …” markers look like temporary review notes and should be removed before merging to keep the handler code clean.

Suggested change
device_code: str # <--- ADDED THIS FIELD
device_code: str

Copilot uses AI. Check for mistakes.
Comment on lines +114 to +122
# Fetch the dataset
dataset = await Dataset.get_by(db, id=dataset_id)
if not dataset:
_LOGGER.warning(f"Dataset {dataset_id} not found")
return None

# Get vector settings for this dataset (use first available or "default")
vector_settings = await VectorSettings.get_by(db, dataset_id=dataset.id)
if not vector_settings:
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

RAG retrieval loads the dataset directly via Dataset.get_by but never authorizes that current_user is allowed to read/search this dataset. This can leak embeddings/context from datasets the caller shouldn’t access. Load the dataset with the standard pattern and enforce DatasetPolicy.get(dataset) (or use a context method that already performs authorization) before running similarity search / fetching records.

Copilot uses AI. Check for mistakes.
Comment on lines +190 to +198
@router.post("/chat")
async def chat(
request: ChatRequest,
db: Annotated[AsyncSession, Depends(get_async_db)],
search_engine: Annotated[SearchEngine, Depends(get_search_engine)],
current_user: User = Depends(auth.get_current_user),
) -> StreamingResponse:
"""
Chat endpoint using LiteLLM with GitHub Copilot and optional RAG.
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR introduces new externally-facing behavior (Copilot device flow + /chat streaming + optional RAG) but there are no backend tests added alongside it. The server repo has extensive pytest coverage; adding tests (e.g., for auth status/login/poll flows with respx, and SSE streaming framing) would help prevent regressions.

Copilot uses AI. Check for mistakes.
Comment on lines +83 to +87
const pdfFileOptions = allPdfFileNames.value.map(name => ({
label: name,
value: name,
}));

Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pdfFileOptions is computed but never used, which adds noise and can confuse future maintenance. Either remove it or use it as the source for the files column’s editor options.

Suggested change
const pdfFileOptions = allPdfFileNames.value.map(name => ({
label: name,
value: name,
}));

Copilot uses AI. Check for mistakes.
Comment on lines +177 to +185
formatter: (cell: any) => {
const value = cell.getValue();
if (!value || (Array.isArray(value) && value.length === 0)) {
return '<span style="color: var(--fg-tertiary);">No files</span>';
}
const files = Array.isArray(value) ? value : [value];
const count = files.length;
return `<span title="${files.join(', ')}">${count} file${count !== 1 ? 's' : ''}</span>`;
},
Copy link

Copilot AI Mar 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The formatter builds an HTML string with unescaped file names inside an attribute (title="..."). Since file names come from user uploads, this can lead to HTML/attribute injection (XSS). Escape/encode file names before interpolating, or return a DOM element / use Tabulator’s safe text rendering APIs.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants