anthropic.js 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  1. /**
  2. * Anthropic provider — translates OpenAI chat completion requests
  3. * to Anthropic Messages API calls using native fetch().
  4. */
  5. export function createProvider(config) {
  6. const { id, apiKey, baseUrl, models } = config;
  7. const messagesUrl = baseUrl ? baseUrl.replace(/\/+$/, '') : 'https://api.anthropic.com/v1/messages';
  8. function buildHeaders() {
  9. return {
  10. 'Content-Type': 'application/json',
  11. 'x-api-key': apiKey,
  12. 'anthropic-version': '2023-06-01'
  13. };
  14. }
  15. function translateMessages(openAiMessages) {
  16. const systemParts = [];
  17. const messages = [];
  18. for (const msg of openAiMessages) {
  19. if (msg.role === 'system') {
  20. systemParts.push(msg.content);
  21. } else {
  22. messages.push({ role: msg.role, content: msg.content });
  23. }
  24. }
  25. const system = systemParts.length > 0 ? systemParts.join('\n\n') : undefined;
  26. return { system, messages };
  27. }
  28. function buildBody(openAiMessages, modelId, options, stream) {
  29. const { system, messages } = translateMessages(openAiMessages);
  30. const body = {
  31. model: modelId,
  32. messages,
  33. max_tokens: options.max_tokens ?? 4096,
  34. stream
  35. };
  36. if (system !== undefined) {
  37. body.system = system;
  38. }
  39. if (options.temperature !== undefined) {
  40. body.temperature = options.temperature;
  41. }
  42. if (options.top_p !== undefined) {
  43. body.top_p = options.top_p;
  44. }
  45. return body;
  46. }
  47. async function request(openAiMessages, modelId, options, stream) {
  48. const body = buildBody(openAiMessages, modelId, options, stream);
  49. const headers = buildHeaders();
  50. let response;
  51. try {
  52. response = await fetch(messagesUrl, {
  53. method: 'POST',
  54. headers,
  55. body: JSON.stringify(body)
  56. });
  57. } catch (error) {
  58. throw new Error(`Anthropic provider request failed: ${error.message}`);
  59. }
  60. if (!response.ok) {
  61. let errorBody = '';
  62. try { errorBody = await response.text(); } catch { /* ignore */ }
  63. throw new Error(
  64. `Anthropic provider returned ${response.status}${errorBody ? `: ${errorBody}` : ''}`
  65. );
  66. }
  67. return response;
  68. }
  69. async function chat(openAiMessages, localModelId, options = {}) {
  70. const providerModelId = models[localModelId];
  71. const response = await request(openAiMessages, providerModelId, options, false);
  72. const anthropicResponse = await response.json();
  73. return {
  74. id: anthropicResponse.id,
  75. object: 'chat.completion',
  76. created: Math.floor(Date.now() / 1000),
  77. model: localModelId,
  78. choices: [
  79. {
  80. index: 0,
  81. message: {
  82. role: 'assistant',
  83. content: anthropicResponse.content[0].text
  84. },
  85. finish_reason: 'stop'
  86. }
  87. ],
  88. usage: {
  89. prompt_tokens: anthropicResponse.usage.input_tokens,
  90. completion_tokens: anthropicResponse.usage.output_tokens,
  91. total_tokens: anthropicResponse.usage.input_tokens + anthropicResponse.usage.output_tokens
  92. }
  93. };
  94. }
  95. async function* chatStream(openAiMessages, localModelId, options = {}) {
  96. const providerModelId = models[localModelId];
  97. const response = await request(openAiMessages, providerModelId, options, true);
  98. const reader = response.body.getReader();
  99. const decoder = new TextDecoder();
  100. let buffer = '';
  101. try {
  102. while (true) {
  103. const { done, value } = await reader.read();
  104. if (done) break;
  105. buffer += decoder.decode(value, { stream: true });
  106. const lines = buffer.split('\n');
  107. buffer = lines.pop(); // keep incomplete line in buffer
  108. for (const line of lines) {
  109. const trimmed = line.trim();
  110. if (!trimmed || !trimmed.startsWith('data:')) continue;
  111. const dataStr = trimmed.slice(5).trim();
  112. if (dataStr === '[DONE]') continue;
  113. let data;
  114. try {
  115. data = JSON.parse(dataStr);
  116. } catch {
  117. continue;
  118. }
  119. if (data.type === 'content_block_delta' && data.delta?.text) {
  120. yield {
  121. id: `chunk-${Date.now()}`,
  122. object: 'chat.completion.chunk',
  123. created: Math.floor(Date.now() / 1000),
  124. model: localModelId,
  125. choices: [
  126. {
  127. index: 0,
  128. delta: { content: data.delta.text },
  129. finish_reason: null
  130. }
  131. ]
  132. };
  133. } else if (data.type === 'message_stop') {
  134. yield {
  135. id: `chunk-${Date.now()}`,
  136. object: 'chat.completion.chunk',
  137. created: Math.floor(Date.now() / 1000),
  138. model: localModelId,
  139. choices: [
  140. {
  141. index: 0,
  142. delta: {},
  143. finish_reason: 'stop'
  144. }
  145. ]
  146. };
  147. }
  148. }
  149. }
  150. } finally {
  151. reader.releaseLock();
  152. }
  153. }
  154. return { id, chat, chatStream };
  155. }