From ba82ba26325e8cd91ce68b1288a8651cbf55b740 Mon Sep 17 00:00:00 2001 From: Ali BARIN Date: Wed, 22 Jan 2025 16:08:38 +0000 Subject: [PATCH] feat(openrouter): add app with create chat completion action --- .../actions/create-chat-completion/index.js | 157 ++++++++++++++++++ .../src/apps/openrouter/actions/index.js | 3 + .../src/apps/openrouter/assets/favicon.svg | 1 + .../backend/src/apps/openrouter/auth/index.js | 34 ++++ .../apps/openrouter/auth/is-still-verified.js | 6 + .../openrouter/auth/verify-credentials.js | 5 + .../apps/openrouter/common/add-auth-header.js | 9 + .../src/apps/openrouter/dynamic-data/index.js | 3 + .../dynamic-data/list-models/index.js | 17 ++ packages/backend/src/apps/openrouter/index.js | 20 +++ .../src/models/__snapshots__/app.test.js.snap | 1 + packages/docs/pages/.vitepress/config.js | 9 + .../docs/pages/apps/openrouter/actions.md | 12 ++ .../docs/pages/apps/openrouter/connection.md | 8 + packages/docs/pages/guide/available-apps.md | 1 + .../docs/pages/public/favicons/openrouter.svg | 1 + 16 files changed, 287 insertions(+) create mode 100644 packages/backend/src/apps/openrouter/actions/create-chat-completion/index.js create mode 100644 packages/backend/src/apps/openrouter/actions/index.js create mode 100644 packages/backend/src/apps/openrouter/assets/favicon.svg create mode 100644 packages/backend/src/apps/openrouter/auth/index.js create mode 100644 packages/backend/src/apps/openrouter/auth/is-still-verified.js create mode 100644 packages/backend/src/apps/openrouter/auth/verify-credentials.js create mode 100644 packages/backend/src/apps/openrouter/common/add-auth-header.js create mode 100644 packages/backend/src/apps/openrouter/dynamic-data/index.js create mode 100644 packages/backend/src/apps/openrouter/dynamic-data/list-models/index.js create mode 100644 packages/backend/src/apps/openrouter/index.js create mode 100644 packages/docs/pages/apps/openrouter/actions.md create mode 100644 packages/docs/pages/apps/openrouter/connection.md create mode 100644 packages/docs/pages/public/favicons/openrouter.svg diff --git a/packages/backend/src/apps/openrouter/actions/create-chat-completion/index.js b/packages/backend/src/apps/openrouter/actions/create-chat-completion/index.js new file mode 100644 index 00000000..8e983103 --- /dev/null +++ b/packages/backend/src/apps/openrouter/actions/create-chat-completion/index.js @@ -0,0 +1,157 @@ +import defineAction from '../../../../helpers/define-action.js'; + +const castFloatOrUndefined = (value) => { + return value === '' ? undefined : parseFloat(value); +}; + +export default defineAction({ + name: 'Create chat completion', + key: 'createChatCompletion', + description: 'Creates a chat completion.', + arguments: [ + { + label: 'Model', + key: 'model', + type: 'dropdown', + required: true, + variables: true, + source: { + type: 'query', + name: 'getDynamicData', + arguments: [ + { + name: 'key', + value: 'listModels', + }, + ], + }, + }, + { + label: 'Messages', + key: 'messages', + type: 'dynamic', + required: true, + description: + 'The prompt(s) to generate completions for, encoded as a list of dict with role and content.', + value: [{ role: 'system', body: '' }], + fields: [ + { + label: 'Role', + key: 'role', + type: 'dropdown', + required: true, + options: [ + { + label: 'System', + value: 'system', + }, + { + label: 'Assistant', + value: 'assistant', + }, + { + label: 'User', + value: 'user', + }, + ], + }, + { + label: 'Content', + key: 'content', + type: 'string', + required: true, + variables: true, + }, + ], + }, + { + label: 'Temperature', + key: 'temperature', + type: 'string', + required: false, + variables: true, + description: + 'What sampling temperature to use. Higher values mean the model will take more risk. Try 0.9 for more creative applications, and 0 for ones with a well-defined answer. We generally recommend altering this or Top P but not both.', + }, + { + label: 'Maximum tokens', + key: 'maxTokens', + type: 'string', + required: false, + variables: true, + description: `The maximum number of tokens to generate in the completion. The token count of your prompt plus max_tokens cannot exceed the model's context length.`, + }, + { + label: 'Stop sequences', + key: 'stopSequences', + type: 'dynamic', + required: false, + variables: true, + description: 'Stop generation if one of these tokens is detected', + fields: [ + { + label: 'Stop sequence', + key: 'stopSequence', + type: 'string', + required: false, + variables: true, + }, + ], + }, + { + label: 'Top P', + key: 'topP', + type: 'string', + required: false, + variables: true, + description: + 'Nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered. We generally recommend altering this or temperature but not both.', + }, + { + label: 'Frequency Penalty', + key: 'frequencyPenalty', + type: 'string', + required: false, + variables: true, + description: `Frequency_penalty penalizes the repetition of words based on their frequency in the generated text. A higher frequency penalty discourages the model from repeating words that have already appeared frequently in the output, promoting diversity and reducing repetition.`, + }, + { + label: 'Presence Penalty', + key: 'presencePenalty', + type: 'string', + required: false, + variables: true, + description: `Presence penalty determines how much the model penalizes the repetition of words or phrases. A higher presence penalty encourages the model to use a wider variety of words and phrases, making the output more diverse and creative.`, + }, + ], + + async run($) { + const nonEmptyStopSequences = $.step.parameters.stopSequences + .filter(({ stopSequence }) => stopSequence) + .map(({ stopSequence }) => stopSequence); + + const messages = $.step.parameters.messages.map((message) => ({ + role: message.role, + content: message.content, + })); + + const payload = { + model: $.step.parameters.model, + messages, + stop: nonEmptyStopSequences, + temperature: castFloatOrUndefined($.step.parameters.temperature), + max_tokens: castFloatOrUndefined($.step.parameters.maxTokens), + top_p: castFloatOrUndefined($.step.parameters.topP), + frequency_penalty: castFloatOrUndefined( + $.step.parameters.frequencyPenalty + ), + presence_penalty: castFloatOrUndefined($.step.parameters.presencePenalty), + }; + + const { data } = await $.http.post('/v1/chat/completions', payload); + + $.setActionItem({ + raw: data, + }); + }, +}); diff --git a/packages/backend/src/apps/openrouter/actions/index.js b/packages/backend/src/apps/openrouter/actions/index.js new file mode 100644 index 00000000..cc0e0532 --- /dev/null +++ b/packages/backend/src/apps/openrouter/actions/index.js @@ -0,0 +1,3 @@ +import createChatCompletion from './create-chat-completion/index.js'; + +export default [createChatCompletion]; diff --git a/packages/backend/src/apps/openrouter/assets/favicon.svg b/packages/backend/src/apps/openrouter/assets/favicon.svg new file mode 100644 index 00000000..e88f91bd --- /dev/null +++ b/packages/backend/src/apps/openrouter/assets/favicon.svg @@ -0,0 +1 @@ +OpenRouter \ No newline at end of file diff --git a/packages/backend/src/apps/openrouter/auth/index.js b/packages/backend/src/apps/openrouter/auth/index.js new file mode 100644 index 00000000..8d260a3c --- /dev/null +++ b/packages/backend/src/apps/openrouter/auth/index.js @@ -0,0 +1,34 @@ +import verifyCredentials from './verify-credentials.js'; +import isStillVerified from './is-still-verified.js'; + +export default { + fields: [ + { + key: 'screenName', + label: 'Screen Name', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: + 'Screen name of your connection to be used on Automatisch UI.', + clickToCopy: false, + }, + { + key: 'apiKey', + label: 'API Key', + type: 'string', + required: true, + readOnly: false, + value: null, + placeholder: null, + description: 'OpenRouter API key of your account.', + docUrl: 'https://automatisch.io/docs/openrouter#api-key', + clickToCopy: false, + }, + ], + + verifyCredentials, + isStillVerified, +}; diff --git a/packages/backend/src/apps/openrouter/auth/is-still-verified.js b/packages/backend/src/apps/openrouter/auth/is-still-verified.js new file mode 100644 index 00000000..3e6c9095 --- /dev/null +++ b/packages/backend/src/apps/openrouter/auth/is-still-verified.js @@ -0,0 +1,6 @@ +const isStillVerified = async ($) => { + await $.http.get('/v1/models'); + return true; +}; + +export default isStillVerified; diff --git a/packages/backend/src/apps/openrouter/auth/verify-credentials.js b/packages/backend/src/apps/openrouter/auth/verify-credentials.js new file mode 100644 index 00000000..7f43f884 --- /dev/null +++ b/packages/backend/src/apps/openrouter/auth/verify-credentials.js @@ -0,0 +1,5 @@ +const verifyCredentials = async ($) => { + await $.http.get('/v1/models'); +}; + +export default verifyCredentials; diff --git a/packages/backend/src/apps/openrouter/common/add-auth-header.js b/packages/backend/src/apps/openrouter/common/add-auth-header.js new file mode 100644 index 00000000..f9f5acba --- /dev/null +++ b/packages/backend/src/apps/openrouter/common/add-auth-header.js @@ -0,0 +1,9 @@ +const addAuthHeader = ($, requestConfig) => { + if ($.auth.data?.apiKey) { + requestConfig.headers.Authorization = `Bearer ${$.auth.data.apiKey}`; + } + + return requestConfig; +}; + +export default addAuthHeader; diff --git a/packages/backend/src/apps/openrouter/dynamic-data/index.js b/packages/backend/src/apps/openrouter/dynamic-data/index.js new file mode 100644 index 00000000..6db48046 --- /dev/null +++ b/packages/backend/src/apps/openrouter/dynamic-data/index.js @@ -0,0 +1,3 @@ +import listModels from './list-models/index.js'; + +export default [listModels]; diff --git a/packages/backend/src/apps/openrouter/dynamic-data/list-models/index.js b/packages/backend/src/apps/openrouter/dynamic-data/list-models/index.js new file mode 100644 index 00000000..a8e81538 --- /dev/null +++ b/packages/backend/src/apps/openrouter/dynamic-data/list-models/index.js @@ -0,0 +1,17 @@ +export default { + name: 'List models', + key: 'listModels', + + async run($) { + const response = await $.http.get('/v1/models'); + + const models = response.data.data.map((model) => { + return { + value: model.id, + name: model.id, + }; + }); + + return { data: models }; + }, +}; diff --git a/packages/backend/src/apps/openrouter/index.js b/packages/backend/src/apps/openrouter/index.js new file mode 100644 index 00000000..12a50672 --- /dev/null +++ b/packages/backend/src/apps/openrouter/index.js @@ -0,0 +1,20 @@ +import defineApp from '../../helpers/define-app.js'; +import addAuthHeader from './common/add-auth-header.js'; +import auth from './auth/index.js'; +import actions from './actions/index.js'; +import dynamicData from './dynamic-data/index.js'; + +export default defineApp({ + name: 'OpenRouter', + key: 'openrouter', + baseUrl: 'https://openrouter.ai', + apiBaseUrl: 'https://openrouter.ai/api', + iconUrl: '{BASE_URL}/apps/openrouter/assets/favicon.svg', + authDocUrl: '{DOCS_URL}/apps/openrouter/connection', + primaryColor: '#71717a', + supportsConnections: true, + beforeRequest: [addAuthHeader], + auth, + actions, + dynamicData, +}); diff --git a/packages/backend/src/models/__snapshots__/app.test.js.snap b/packages/backend/src/models/__snapshots__/app.test.js.snap index c96970fa..18b951e1 100644 --- a/packages/backend/src/models/__snapshots__/app.test.js.snap +++ b/packages/backend/src/models/__snapshots__/app.test.js.snap @@ -43,6 +43,7 @@ exports[`App model > list should have list of applications keys 1`] = ` "ntfy", "odoo", "openai", + "openrouter", "perplexity", "pipedrive", "placetel", diff --git a/packages/docs/pages/.vitepress/config.js b/packages/docs/pages/.vitepress/config.js index 6183c7bd..63a996a1 100644 --- a/packages/docs/pages/.vitepress/config.js +++ b/packages/docs/pages/.vitepress/config.js @@ -368,6 +368,15 @@ export default defineConfig({ { text: 'Connection', link: '/apps/openai/connection' }, ], }, + { + text: 'OpenRouter', + collapsible: true, + collapsed: true, + items: [ + { text: 'Actions', link: '/apps/openrouter/actions' }, + { text: 'Connection', link: '/apps/openrouter/connection' }, + ], + }, { text: 'Perplexity', collapsible: true, diff --git a/packages/docs/pages/apps/openrouter/actions.md b/packages/docs/pages/apps/openrouter/actions.md new file mode 100644 index 00000000..394a43a0 --- /dev/null +++ b/packages/docs/pages/apps/openrouter/actions.md @@ -0,0 +1,12 @@ +--- +favicon: /favicons/openrouter.svg +items: + - name: Create chat completion + desc: Creates a chat completion. +--- + + + + diff --git a/packages/docs/pages/apps/openrouter/connection.md b/packages/docs/pages/apps/openrouter/connection.md new file mode 100644 index 00000000..8abc070b --- /dev/null +++ b/packages/docs/pages/apps/openrouter/connection.md @@ -0,0 +1,8 @@ +# OpenRouter + +1. Go to [API Keys page](https://openrouter.ai/settings/keys) on OpenRouter. +2. Create a new key. +3. Paste the key into the `API Key` field in Automatisch. +4. Write any screen name to be displayed in Automatisch. +5. Click `Save`. +6. Start using OpenRouter integration with Automatisch! diff --git a/packages/docs/pages/guide/available-apps.md b/packages/docs/pages/guide/available-apps.md index bad1e765..0eb08bea 100644 --- a/packages/docs/pages/guide/available-apps.md +++ b/packages/docs/pages/guide/available-apps.md @@ -37,6 +37,7 @@ The following integrations are currently supported by Automatisch. - [Ntfy](/apps/ntfy/actions) - [Odoo](/apps/odoo/actions) - [OpenAI](/apps/openai/actions) +- [OpenRouter](/apps/openrouter/actions) - [Perplexity](/apps/perplexity/actions) - [Pipedrive](/apps/pipedrive/triggers) - [Placetel](/apps/placetel/triggers) diff --git a/packages/docs/pages/public/favicons/openrouter.svg b/packages/docs/pages/public/favicons/openrouter.svg new file mode 100644 index 00000000..e88f91bd --- /dev/null +++ b/packages/docs/pages/public/favicons/openrouter.svg @@ -0,0 +1 @@ +OpenRouter \ No newline at end of file