-
Notifications
You must be signed in to change notification settings - Fork 101
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
base: main
Are you sure you want to change the base?
Conversation
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; | ||
} |
There was a problem hiding this comment.
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.
// 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"; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
|
||
<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> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
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" |
There was a problem hiding this comment.
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.
As part of this, I had to build the protobuf file into a set of "json definitions" that I could more easily include in a javascript bundle vs the raw bytes of a proto file.
b3cd5d8
to
d529456
Compare
There was a problem hiding this 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. |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
https://github.com/Exafunction/codeium-react-code-editor/blob/main/LICENSE MIT license for the source
Introduces codeium ai autocomplete into the srcbook editor.
A few high level notes:
EDITOR_API_KEY
constant token is used. If a user authenticates with codeium, a token linked to their user account is used instead.