Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Codeium AI autocomplete integration #343

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open

Conversation

1egoman
Copy link
Collaborator

@1egoman 1egoman commented Oct 9, 2024

Introduces codeium ai autocomplete into the srcbook editor.

Screenshot 2024-10-10 at 3 22 20 PM

A few high level notes:

  • There is no way to enable / disable this feature currently.
  • The other cells are used as context when performing code generation, leading to more contextually relevant suggestions.
  • By default, the EDITOR_API_KEY constant token is used. If a user authenticates with codeium, a token linked to their user account is used instead.

Comment on lines 1 to 85
import protobuf from 'protobufjs';
import Long from 'long';

import languageServerProto from './language-server-proto';

// NOTE: this EDITOR_API_KEY value was just included as a raw string in
// @codeium/react-code-editor. This seems to not be a secret? See here:
// https://github.com/Exafunction/codeium-react-code-editor/blob/768e1b231c00e078c86bc19c8ede697a1e37ec75/src/components/CodeiumEditor/CompletionProvider.ts#L48
const EDITOR_API_KEY = 'd49954eb-cfba-4992-980f-d8fb37f0e942';

// NOTE: The below logic has been adapted from codeium's `@codeium/react-code-editor package. See here:
// https://github.com/Exafunction/codeium-react-code-editor/blob/768e1b231c00e078c86bc19c8ede697a1e37ec75/src/components/CodeiumEditor/CompletionProvider.ts#L147-L159
export async function runCodiumAiAutocomplete(
optionalApiKey: string | null,
source: string,
sourceLanguage: 'javascript' | 'typescript',
cursorOffset: number,
): Promise<CodiumCompletionResult> {
const protos = protobuf.Root.fromJSON(languageServerProto as protobuf.INamespace);
const GetCompletionsRequest = protos.lookupType('exa.language_server_pb.GetCompletionsRequest');
const Metadata = protos.lookupType('exa.codeium_common_pb.Metadata');
const DocumentInfo = protos.lookupType('exa.language_server_pb.Document');
const EditorOptions = protos.lookupType('exa.codeium_common_pb.EditorOptions');
const Language = protos.lookupEnum('exa.codeium_common_pb.Language');
const GetCompletionsResponse = protos.lookupType('exa.language_server_pb.GetCompletionsResponse');

const sessionId = `react-editor-${crypto.randomUUID()}`;
const apiKey = optionalApiKey ?? EDITOR_API_KEY;

const payload = {
otherDocuments: [],
metadata: Metadata.create({
ideName: 'web',
extensionVersion: '1.0.12',
apiKey,
ideVersion: 'unknown',
extensionName: '@codeium/react-code-editor',
sessionId,
}),
document: DocumentInfo.create({
text: source,
editorLanguage: sourceLanguage,
language: Language.getOption(sourceLanguage === 'javascript' ? 'JAVASCRIPT' : 'TYPESCRIPT'),
cursorOffset: Long.fromValue(cursorOffset),
lineEnding: '\n',
}),
editorOptions: EditorOptions.create({
tabSize: Long.fromValue(4),
insertSpaces: true,
}),
};

const requestData = GetCompletionsRequest.create(payload);
const buffer = GetCompletionsRequest.encode(requestData).finish();

const response = await fetch(
'https://web-backend.codeium.com/exa.language_server_pb.LanguageServerService/GetCompletions',
{
method: 'POST',
body: buffer,
headers: {
'Connect-Protocol-Version': '1',
'Content-Type': 'application/proto',
Authorization: `Basic ${apiKey}-${sessionId}`,
},
},
);

const responseBodyBytes = new Uint8Array(await response.arrayBuffer());
const responseBody = GetCompletionsResponse.decode(responseBodyBytes);

return responseBody.toJSON() as CodiumCompletionResult;
}
Copy link
Collaborator Author

@1egoman 1egoman Oct 10, 2024

Choose a reason for hiding this comment

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

Here's what the logic to kick off the codeium autocomplete request looks like. While complex, the protobuf serialization / deserialization logic is unfortunately required the query codeium.

Comment on lines +1 to +10
// NOTE: to generate a new set of json definitions, run:
// npm run generate-codeium-proto-json

// Copyright Exafunction, Inc.

syntax = "proto3";

package exa.language_server_pb;

import "codeium_common.proto";
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Here's the underlying protobuf definitions that are used to generate the codeium autocomplete request. These came from here: https://github.com/Exafunction/codeium-react-code-editor/blob/main/exa/language_server_pb/language_server.proto.

Note that to avoid including this raw text in the bundle that is sent to the client, I converted the protobuf file to json descriptiors (more about these here). These are a fair bit smaller and work just as well.

Another option I considered but ultimately decided against given the complexity of introducing was an alternate protobuf library that does code generation at build time, like buf. If protobufs were going to be used everywhere and definitions were going to be changing often, I think it would be a lot of sense, but given this is only used here that seemed like overkill.

Copy link
Contributor

Choose a reason for hiding this comment

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

@1egoman are we okay with the copyright on this?

Comment on lines +216 to +289

<h3 className="text-md pb-2">Codeium AI Autocomplete</h3>
<div className="flex flex-col">
<div className="opacity-70 text-sm pb-1">
By default, Codeium uses a public api token with limited capabilities. Optionally,
sign in to remove rate limits:
</div>

{codeiumApiKey ? (
<div
className="flex flex-col p-3 border rounded-sm"
onMouseEnter={() => setCodeiumApiKeyHovering(true)}
onMouseLeave={() => setCodeiumApiKeyHovering(false)}
>
<div className="opacity-70 text-sm pb-2">Signed in! Codeium API Key:</div>
<div className="flex justify-between items-center gap-2">
<div className="text-left align-middle relative w-full">
<Input
name="codeiumApiKey"
type={codeiumApiKeyVisible ? 'text' : 'password'}
value={codeiumApiKey}
disabled
className="disabled:opacity-100 disabled:cursor-text group-hover:border-border group-focus-within:border-border pr-8"
/>
{codeiumApiKeyVisible ? (
<EyeOffIcon
size={14}
className={cn(
'absolute right-3 top-2.5 cursor-pointer opacity-80 bg-background',
!codeiumApiKeyHovering && 'hidden',
)}
onClick={() => setCodeiumApiKeyVisible(false)}
/>
) : (
<EyeIcon
size={14}
className={cn(
'absolute right-3 top-2.5 cursor-pointer opacity-80 bg-background',
!codeiumApiKeyHovering && 'hidden',
)}
onClick={() => setCodeiumApiKeyVisible(true)}
/>
)}
</div>

<Button
variant="secondary"
onClick={() => {
updateConfigContext({ codeiumApiKey: null })
.then(() => {
toast.success('Detached Codeium API key.');
})
.catch((err) => {
console.error('Error detaching Codeium API key:', err);
toast.error('Error detaching Codeium API key!');
});
}}
>
Detach
</Button>
</div>
</div>
) : (
<div className="flex justify-center items-center p-3 h-[64px] border rounded-sm">
<Button asChild variant="secondary">
<Link
to={`https://www.codeium.com/profile?response_type=token&redirect_uri=${codeiumCallbackUrl}&state=a&scope=openid%20profile%20email&redirect_parameters_type=query`}
>
Authenticate with Codeium
</Link>
</Button>
</div>
)}
</div>
Copy link
Collaborator Author

@1egoman 1egoman Oct 10, 2024

Choose a reason for hiding this comment

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

I added a new interface element to the settings page to facilitate a user authenticating with codeium:

Screenshot 2024-10-10 at 3 07 50 PM

Note that even if a user does not sign in, the autocomplete logic still works, because it falls back to that EDITOR_API_KEY value.

Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: Could add a link to their sign in page

@@ -10,23 +10,28 @@
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"format": "prettier --write .",
"preview": "vite preview",
"check-types": "tsc"
"check-types": "tsc",
"generate-codeium-proto-json": "node --eval 'console.log(\"export default\", JSON.stringify(require(\"protobufjs\").loadSync(\"src/lib/ai-autocomplete/language_server.proto\").toJSON(), null, 2));' > src/lib/ai-autocomplete/languageServerProto.ts"
Copy link
Collaborator Author

@1egoman 1egoman Oct 10, 2024

Choose a reason for hiding this comment

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

I added this npm task to take the language_server.proto file and convert it into the json descriptors data that is required by the client.

Note that currently this generated json descriptors file is committed. I could potentially set this up to generate fresh when starting up the dev server / building for release, but as this in practice will rarely / more likely never change, it seemed like setting up more complex build infrastructure here would be less useful than it otherwise would be.

@1egoman 1egoman marked this pull request as ready for review October 10, 2024 19:51
@1egoman 1egoman changed the title Codeium AI authcomplete integration Codeium AI autocomplete integration Oct 11, 2024
Copy link
Contributor

@benjreinhart benjreinhart left a comment

Choose a reason for hiding this comment

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

Nit: I would prefer the if the protos we're in their own folder named something like "generated"

@@ -0,0 +1,166 @@
// Copyright Exafunction, Inc.
Copy link
Contributor

Choose a reason for hiding this comment

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

@1egoman do you know if we're able to pull this in given the copyright?

Copy link
Contributor

Choose a reason for hiding this comment

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

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.

4 participants