1
0

openRouterChat.ts 2.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. import { ChatbotBackend } from "@/types/backend";
  2. import { Message } from "./messages";
  3. /**
  4. * Gets a streaming chat response from OpenRouter API.
  5. * OpenRouter provides an OpenAI-compatible API with access to multiple models.
  6. */
  7. export async function getOpenRouterChatResponseStream(
  8. config: ChatbotBackend["openrouter"],
  9. messages: Message[],
  10. ): Promise<ReadableStream> {
  11. const apiKey = config?.openrouter_apikey;
  12. if (!apiKey) {
  13. throw new Error("OpenRouter API key is required");
  14. }
  15. const baseUrl = config.openrouter_url ?? "https://openrouter.ai/api/v1";
  16. const model = config.openrouter_model ?? "openai/gpt-3.5-turbo";
  17. const appUrl = "https://amica.arbius.ai";
  18. const response = await fetch(`${baseUrl}/chat/completions`, {
  19. method: "POST",
  20. headers: {
  21. Authorization: `Bearer ${apiKey}`,
  22. "Content-Type": "application/json",
  23. "HTTP-Referer": appUrl,
  24. "X-Title": "Amica Chat",
  25. },
  26. body: JSON.stringify({
  27. model,
  28. messages: messages.map(({ role, content }) => ({ role, content })),
  29. stream: true,
  30. }),
  31. });
  32. const reader = response.body?.getReader();
  33. if (!response.ok || !reader) {
  34. const error = await response.json();
  35. // Handle OpenRouter-specific error format
  36. if (error.error?.message) {
  37. throw new Error(`OpenRouter error: ${error.error.message}`);
  38. }
  39. throw new Error(`OpenRouter request failed with status ${response.status}`);
  40. }
  41. const stream = new ReadableStream({
  42. async start(controller: ReadableStreamDefaultController) {
  43. const decoder = new TextDecoder("utf-8");
  44. try {
  45. // sometimes the response is chunked, so we need to combine the chunks
  46. let combined = "";
  47. while (true) {
  48. const { done, value } = await reader.read();
  49. if (done) break;
  50. const data = decoder.decode(value);
  51. const chunks = data
  52. .split("data:")
  53. .filter((val) => !!val && val.trim() !== "[DONE]");
  54. for (const chunk of chunks) {
  55. // skip comments
  56. if (chunk.length > 0 && chunk[0] === ":") {
  57. continue;
  58. }
  59. combined += chunk;
  60. try {
  61. const json = JSON.parse(combined);
  62. const messagePiece = json.choices[0].delta.content;
  63. combined = "";
  64. if (!!messagePiece) {
  65. controller.enqueue(messagePiece);
  66. }
  67. } catch (error) {
  68. console.error(error);
  69. }
  70. }
  71. }
  72. } catch (error) {
  73. console.error(error);
  74. controller.error(error);
  75. } finally {
  76. reader?.releaseLock();
  77. controller.close();
  78. }
  79. },
  80. async cancel() {
  81. await reader?.cancel();
  82. reader?.releaseLock();
  83. },
  84. });
  85. return stream;
  86. }