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

WIP: Add provider for Anthropic Claude api #23

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
162 changes: 162 additions & 0 deletions src/anthropic.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { SSE } from "npm:[email protected]";
import { ChatMessage } from "./init.ts";
import { AbstractProvider, sseEvent, StreamChatOptions } from "./interfaces.ts";
import { syscall, editor } from "$sb/syscalls.ts";
import { base64Decode, base64Encode } from "$lib/crypto.ts";
import { ProxyFetchRequest } from "$common/proxy_fetch.ts";
// import "$sb/lib/native_fetch.ts";

type HttpHeaders = {
"Content-Type": string;
"Authorization"?: string;
"anthropic-version": string;
"x-api-key": string;
"anthropic-beta": string;
};

type ClaudeMessage = {
role: string;
content: string;
};

export class ClaudeProvider extends AbstractProvider {
name = "Claude";

constructor(
apiKey: string,
modelName: string,
) {
const baseUrl = "https://api.anthropic.com";
super("Claude", apiKey, baseUrl, modelName);
}

async chatWithAI(
{ messages, stream, onDataReceived }: StreamChatOptions,
): Promise<any> {
console.log("Starting chat with Claude: ", messages);
return await this.chatNoStream({ messages, stream, onDataReceived });
// if (stream) {
// return await this.streamChat({ messages, stream, onDataReceived });
// } else {
// // TODO: Implement non-streaming for claude
// console.error("Non-streaming chat not implemented for Claude.");
// }
}

async chatNoStream(options: StreamChatOptions): Promise<any> {
const { messages, onDataReceived } = options;
const claudeMessages: ClaudeMessage[] = messages.map((message: ChatMessage) => ({
role: message.role,
content: message.content,
}));

const headers: HttpHeaders = {
"Content-Type": "application/json",
"x-api-key": this.apiKey,
"anthropic-version": "2023-06-01",
"anthropic-beta": "messages-2023-12-15",
};

const body = JSON.stringify({
model: this.modelName,
messages: claudeMessages,
stream: false,
});

const fetchOptions: ProxyFetchRequest = {
method: "POST",
headers: headers,
base64Body: base64Encode(body),
};

// TODO: Neither fetch nor nativeFetch in work.. nativeFetch fails because of cors just like sse+streaming
console.log("Calling sandboxFetch.fetch with: ", fetchOptions);
const response = await syscall("sandboxFetch.fetch", `${this.baseUrl}/v1/messages`, fetchOptions);
const responseBody = JSON.parse(new TextDecoder().decode(base64Decode(response.base64Body)));

console.log("response from chatNoStream: ", response);
console.log("response json from chatNoStream: ", responseBody);

if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}

const data = await response.json();
if (onDataReceived) onDataReceived(data.content[0]?.text || "");
return data;
}

streamChat(options: StreamChatOptions) {
// TODO: Streaming doesn't work because of CORS. We could potentially proxy it through the server?
// ^ see https://github.com/anthropics/anthropic-sdk-typescript/issues/248 and the linked issues
const { messages, onDataReceived } = options;

console.log("ASDFASDFASDFASDF Entered streamChat");

try {
const sseUrl = `${this.baseUrl}/v1/messages`;

const headers: HttpHeaders = {
"Content-Type": "application/json",
"x-api-key": this.apiKey,
"anthropic-version": "2023-06-01",
"anthropic-beta": "messages-2023-12-15",
};

const claudeMessages: ClaudeMessage[] = messages.map((
message: ChatMessage,
) => ({
role: message.role,
content: message.content,
}));

const sseOptions = {
method: "POST",
headers: headers,
payload: JSON.stringify({
model: this.modelName,
messages: claudeMessages,
stream: true,
}),
withCredentials: true,
};

const source = new SSE(sseUrl, sseOptions);
let fullMsg = "";

source.addEventListener("message", (e: sseEvent) => {
try {
if (e.data == "[DONE]") {
source.close();
return fullMsg;
} else if (!e.data) {
console.error("Received empty message from Claude");
console.log("source: ", source);
} else {
const data = JSON.parse(e.data);
const msg = data.choices[0]?.content || "";
fullMsg += msg;
if (onDataReceived) onDataReceived(msg);
}
} catch (error) {
console.error("Error processing message event:", error, e.data);
}
});

source.addEventListener("end", () => {
source.close();
return fullMsg;
});

source.addEventListener("error", (e: sseEvent) => {
console.error("SSE error:", e);
source.close();
});

source.stream();
} catch (error) {
console.error("Error streaming from Claude chat endpoint:", error);
throw error;
}
}
}
5 changes: 5 additions & 0 deletions src/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { DallEProvider } from "./dalle.ts";
import { GeminiProvider } from "./gemini.ts";
import { ImageProviderInterface, ProviderInterface } from "./interfaces.ts";
import { OpenAIProvider } from "./openai.ts";
import { ClaudeProvider } from "./anthropic.ts";

export type ChatMessage = {
content: string;
Expand All @@ -20,6 +21,7 @@ export type ChatSettings = {
enum Provider {
OpenAI = "openai",
Gemini = "gemini",
Claude = "claude",
}

enum ImageProvider {
Expand Down Expand Up @@ -151,6 +153,9 @@ function setupAIProvider(model: ModelConfig) {
case Provider.Gemini:
currentAIProvider = new GeminiProvider(apiKey, model.modelName);
break;
case Provider.Claude:
currentAIProvider = new ClaudeProvider(apiKey, model.modelName);
break;
default:
throw new Error(
`Unsupported AI provider: ${model.provider}. Please configure a supported provider.`,
Expand Down
Loading