Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 5 additions & 18 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,6 @@
"command": "sweep.triggerNextEdit",
"title": "Sweep: Trigger Next Edit Suggestion"
},
{
"command": "sweep.setApiKey",
"title": "Sweep: Set API Key"
},
{
"command": "sweep.togglePrivacyMode",
"title": "Sweep: Toggle Privacy Mode"
},
{
"command": "sweep.toggleEnabled",
"title": "Sweep: Toggle Enabled"
Expand Down Expand Up @@ -104,22 +96,12 @@
"configuration": {
"title": "Sweep Next Edit",
"properties": {
"sweep.apiKey": {
"type": "string",
"default": "",
"description": "API key for authentication"
},
"sweep.enabled": {
"type": "boolean",
"default": true,
"scope": "window",
"description": "Enable or disable Sweep Next Edit suggestions"
},
"sweep.privacyMode": {
"type": "boolean",
"default": false,
"description": "When enabled completions will not be trained on"
},
"sweep.maxContextFiles": {
"type": "number",
"default": 5,
Expand All @@ -145,6 +127,11 @@
"type": "number",
"default": 0,
"description": "Unix timestamp (ms) until which autocomplete is snoozed (0 disables snooze)"
},
"sweep.localPort": {
"type": "number",
"default": 8081,
"description": "Port for the local autocomplete server"
}
}
}
Expand Down
171 changes: 33 additions & 138 deletions src/api/client.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,8 @@
import * as http from "node:http";
import * as https from "node:https";
import * as os from "node:os";
import * as zlib from "node:zlib";
import * as vscode from "vscode";
import type { ZodType } from "zod";
import { config } from "~/core/config.ts";
import {
DEFAULT_API_ENDPOINT,
DEFAULT_METRICS_ENDPOINT,
} from "~/core/constants.ts";
import type { LocalAutocompleteServer } from "~/services/local-server.ts";
import { toUnixPath } from "~/utils/path.ts";
import {
isFileTooLarge,
Expand All @@ -20,8 +14,6 @@ import {
truncateRetrievalChunk,
} from "./retrieval-chunks.ts";
import {
type AutocompleteMetricsRequest,
AutocompleteMetricsRequestSchema,
type AutocompleteRequest,
AutocompleteRequestSchema,
type AutocompleteResponse,
Expand Down Expand Up @@ -54,26 +46,16 @@ const MAX_CLIPBOARD_LINES = 20;
const MAX_DIAGNOSTICS = 50;

export class ApiClient {
private apiUrl: string;
private metricsUrl: string;

constructor(
apiUrl: string = DEFAULT_API_ENDPOINT,
metricsUrl: string = DEFAULT_METRICS_ENDPOINT,
) {
this.apiUrl = apiUrl;
this.metricsUrl = metricsUrl;
private localServer: LocalAutocompleteServer;

constructor(localServer: LocalAutocompleteServer) {
this.localServer = localServer;
}

async getAutocomplete(
input: AutocompleteInput,
signal?: AbortSignal,
): Promise<AutocompleteResult[] | null> {
const apiKey = this.apiKey;
if (!apiKey) {
return null;
}

const documentText = input.document.getText();
if (isFileTooLarge(documentText) || isFileTooLarge(input.originalContent)) {
console.log("[Sweep] Skipping autocomplete request: file too large", {
Expand All @@ -94,20 +76,29 @@ export class ApiClient {
return null;
}

const compressed = await this.compress(JSON.stringify(parsedRequest.data));
let response: AutocompleteResponse;
try {
await this.localServer.ensureServerRunning();
} catch (error) {
console.error("[Sweep] Failed to start local server:", error);
return null;
}

const localUrl = `${this.localServer.getServerUrl()}/backend/next_edit_autocomplete`;
try {
response = await this.sendRequest(
compressed,
apiKey,
JSON.stringify(parsedRequest.data),
localUrl,
AutocompleteResponseSchema,
signal,
);
this.localServer.reportSuccess();
} catch (error) {
if ((error as Error).name === "AbortError") {
return null;
}
console.error("[Sweep] API request failed:", error);
console.error("[Sweep] Local API request failed:", error);
this.localServer.reportFailure();
return null;
}

Expand Down Expand Up @@ -147,30 +138,6 @@ export class ApiClient {
return results;
}

async trackAutocompleteMetrics(
request: AutocompleteMetricsRequest,
): Promise<void> {
const apiKey = this.apiKey;
if (!apiKey) {
return;
}

const parsedRequest = AutocompleteMetricsRequestSchema.safeParse(request);
if (!parsedRequest.success) {
console.error(
"[Sweep] Invalid metrics data:",
parsedRequest.error.message,
);
return;
}

await this.sendMetricsRequest(JSON.stringify(parsedRequest.data), apiKey);
}

get apiKey(): string | null {
return config.apiKey;
}

private async buildRequest(
input: AutocompleteInput,
): Promise<AutocompleteRequest> {
Expand Down Expand Up @@ -213,7 +180,6 @@ export class ApiClient {
editor_diagnostics: editorDiagnostics,
recent_user_actions: userActions,
use_bytes: true,
privacy_mode_enabled: config.privacyMode,
};
}

Expand Down Expand Up @@ -492,24 +458,9 @@ export class ApiClient {
);
}

private compress(data: string): Promise<Buffer> {
return new Promise((resolve, reject) => {
zlib.brotliCompress(
Buffer.from(data, "utf-8"),
{
params: {
[zlib.constants.BROTLI_PARAM_QUALITY]: 11,
[zlib.constants.BROTLI_PARAM_LGWIN]: 22,
},
},
(error, result) => (error ? reject(error) : resolve(result)),
);
});
}

private sendRequest<T>(
body: Buffer,
apiKey: string,
body: string,
url: string,
schema: ZodType<T>,
signal?: AbortSignal,
): Promise<T> {
Expand All @@ -522,37 +473,31 @@ export class ApiClient {
fn();
};

const url = new URL(this.apiUrl);
const isHttps = url.protocol === "https:";
const defaultPort = isHttps ? 443 : 80;

const parsedUrl = new URL(url);
const options: http.RequestOptions = {
hostname: url.hostname,
port: url.port || defaultPort,
path: `${url.pathname}${url.search}`,
hostname: parsedUrl.hostname,
port: parsedUrl.port || 80,
path: `${parsedUrl.pathname}${parsedUrl.search}`,
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
"Content-Encoding": "br",
"Content-Length": body.length,
"Content-Length": Buffer.byteLength(body),
},
};

const transport = isHttps ? https : http;
const req = transport.request(options, (res) => {
const req = http.request(options, (res) => {
let data = "";
res.on("data", (chunk) => {
data += chunk.toString();
});
res.on("end", () => {
if (res.statusCode !== 200) {
console.error(
`[Sweep] API request failed with status ${res.statusCode}: ${data}`,
`[Sweep] Local request failed with status ${res.statusCode}: ${data}`,
);
finish(() =>
reject(
new Error(`API request failed with status ${res.statusCode}`),
new Error(`Local request failed with status ${res.statusCode}`),
),
);
return;
Expand All @@ -563,22 +508,24 @@ export class ApiClient {
if (!parsed.success) {
finish(() =>
reject(
new Error(`Invalid API response: ${parsed.error.message}`),
new Error(`Invalid local response: ${parsed.error.message}`),
),
);
return;
}
finish(() => resolve(parsed.data));
} catch {
finish(() =>
reject(new Error("Failed to parse API response JSON")),
reject(new Error("Failed to parse local response JSON")),
);
}
});
});

const onError = (error: Error) => {
finish(() => reject(new Error(`API request error: ${error.message}`)));
finish(() =>
reject(new Error(`Local request error: ${error.message}`)),
);
};

const onAbort = () => {
Expand Down Expand Up @@ -609,56 +556,4 @@ export class ApiClient {
req.end();
});
}

private sendMetricsRequest(body: string, apiKey: string): Promise<void> {
return new Promise((resolve, reject) => {
const url = new URL(this.metricsUrl);
const isHttps = url.protocol === "https:";
const defaultPort = isHttps ? 443 : 80;

const options: http.RequestOptions = {
hostname: url.hostname,
port: url.port || defaultPort,
path: url.pathname,
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
"Content-Length": Buffer.byteLength(body),
},
};

const transport = isHttps ? https : http;
const req = transport.request(options, (res) => {
let data = "";
res.on("data", (chunk) => {
data += chunk.toString();
});
res.on("end", () => {
if (
!res.statusCode ||
res.statusCode < 200 ||
res.statusCode >= 300
) {
console.error(
`[Sweep] Metrics request failed with status ${res.statusCode}: ${data}`,
);
reject(
new Error(
`Metrics request failed with status ${res.statusCode}: ${data}`,
),
);
return;
}
resolve();
});
});

req.on("error", (error) =>
reject(new Error(`Metrics request error: ${error.message}`)),
);
req.write(body);
req.end();
});
}
}
2 changes: 0 additions & 2 deletions src/api/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@ export const AutocompleteRequestSchema = z.object({
editor_diagnostics: z.array(EditorDiagnosticSchema),
recent_user_actions: z.array(UserActionSchema),
use_bytes: z.boolean(),
privacy_mode_enabled: z.boolean(),
});

export const AutocompleteResponseSchema = z.object({
Expand Down Expand Up @@ -103,7 +102,6 @@ export const AutocompleteMetricsRequestSchema = z.object({
lifespan: z.number().optional(),
debug_info: z.string(),
device_id: z.string(),
privacy_mode_enabled: z.boolean(),
num_definitions_retrieved: z.number().optional(),
num_usages_retrieved: z.number().optional(),
});
Expand Down
Loading