feat(openrouter): add app with create chat completion action

This commit is contained in:
Ali BARIN
2025-01-22 16:08:38 +00:00
parent 2163b0864d
commit ba82ba2632
16 changed files with 287 additions and 0 deletions

View File

@@ -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,
});
},
});

View File

@@ -0,0 +1,3 @@
import createChatCompletion from './create-chat-completion/index.js';
export default [createChatCompletion];

View File

@@ -0,0 +1 @@
<svg width="100%" height="100%" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" class="w-4 rounded-full" fill="#71717a" stroke="#71717a" aria-label="Logo"><g clip-path="url(#clip0_205_3)"><path d="M3 248.945C18 248.945 76 236 106 219C136 202 136 202 198 158C276.497 102.293 332 120.945 423 120.945" stroke-width="90"></path><path d="M511 121.5L357.25 210.268L357.25 32.7324L511 121.5Z"></path><path d="M0 249C15 249 73 261.945 103 278.945C133 295.945 133 295.945 195 339.945C273.497 395.652 329 377 420 377" stroke-width="90"></path><path d="M508 376.445L354.25 287.678L354.25 465.213L508 376.445Z"></path></g><title style="display:none">OpenRouter</title><defs><clipPath id="clip0_205_3"><rect width="512" height="512" fill="white"></rect></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 773 B

View File

@@ -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,
};

View File

@@ -0,0 +1,6 @@
const isStillVerified = async ($) => {
await $.http.get('/v1/models');
return true;
};
export default isStillVerified;

View File

@@ -0,0 +1,5 @@
const verifyCredentials = async ($) => {
await $.http.get('/v1/models');
};
export default verifyCredentials;

View File

@@ -0,0 +1,9 @@
const addAuthHeader = ($, requestConfig) => {
if ($.auth.data?.apiKey) {
requestConfig.headers.Authorization = `Bearer ${$.auth.data.apiKey}`;
}
return requestConfig;
};
export default addAuthHeader;

View File

@@ -0,0 +1,3 @@
import listModels from './list-models/index.js';
export default [listModels];

View File

@@ -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 };
},
};

View File

@@ -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,
});

View File

@@ -43,6 +43,7 @@ exports[`App model > list should have list of applications keys 1`] = `
"ntfy",
"odoo",
"openai",
"openrouter",
"perplexity",
"pipedrive",
"placetel",

View File

@@ -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,

View File

@@ -0,0 +1,12 @@
---
favicon: /favicons/openrouter.svg
items:
- name: Create chat completion
desc: Creates a chat completion.
---
<script setup>
import CustomListing from '../../components/CustomListing.vue'
</script>
<CustomListing />

View File

@@ -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!

View File

@@ -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)

View File

@@ -0,0 +1 @@
<svg width="100%" height="100%" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg" class="w-4 rounded-full" fill="#71717a" stroke="#71717a" aria-label="Logo"><g clip-path="url(#clip0_205_3)"><path d="M3 248.945C18 248.945 76 236 106 219C136 202 136 202 198 158C276.497 102.293 332 120.945 423 120.945" stroke-width="90"></path><path d="M511 121.5L357.25 210.268L357.25 32.7324L511 121.5Z"></path><path d="M0 249C15 249 73 261.945 103 278.945C133 295.945 133 295.945 195 339.945C273.497 395.652 329 377 420 377" stroke-width="90"></path><path d="M508 376.445L354.25 287.678L354.25 465.213L508 376.445Z"></path></g><title style="display:none">OpenRouter</title><defs><clipPath id="clip0_205_3"><rect width="512" height="512" fill="white"></rect></clipPath></defs></svg>

After

Width:  |  Height:  |  Size: 773 B