From 10db46b55133db8b2fd29cc9485f23768bd1041d Mon Sep 17 00:00:00 2001 From: Luan Cazarine Date: Mon, 14 Apr 2025 14:58:57 -0300 Subject: [PATCH 1/4] [Components] openrouter #15025 Actions - Send Completion Request - Send Chat Completion Request - Retrieve Available Models --- .../retrieve-available-models.mjs | 20 ++ .../send-chat-completion-request.mjs | 190 ++++++++++++++++++ .../send-completion-request.mjs | 189 +++++++++++++++++ components/openrouter/common/constants.mjs | 5 + components/openrouter/common/utils.mjs | 24 +++ components/openrouter/openrouter.app.mjs | 164 ++++++++++++++- components/openrouter/package.json | 5 +- 7 files changed, 592 insertions(+), 5 deletions(-) create mode 100644 components/openrouter/actions/retrieve-available-models/retrieve-available-models.mjs create mode 100644 components/openrouter/actions/send-chat-completion-request/send-chat-completion-request.mjs create mode 100644 components/openrouter/actions/send-completion-request/send-completion-request.mjs create mode 100644 components/openrouter/common/constants.mjs create mode 100644 components/openrouter/common/utils.mjs diff --git a/components/openrouter/actions/retrieve-available-models/retrieve-available-models.mjs b/components/openrouter/actions/retrieve-available-models/retrieve-available-models.mjs new file mode 100644 index 0000000000000..8fa38ab895a62 --- /dev/null +++ b/components/openrouter/actions/retrieve-available-models/retrieve-available-models.mjs @@ -0,0 +1,20 @@ +import openrouter from "../../openrouter.app.mjs"; + +export default { + key: "openrouter-retrieve-available-models", + name: "Retrieve Available Models", + version: "0.0.1", + description: "Returns a list of models available through the API. [See the documentation](https://openrouter.ai/docs/api-reference/list-available-models)", + type: "action", + props: { + openrouter, + }, + async run({ $ }) { + const response = await this.openrouter.listModels({ + $, + }); + + $.export("$summary", `Successfully retrieved ${response.data.length} available model(s)!`); + return response; + }, +}; diff --git a/components/openrouter/actions/send-chat-completion-request/send-chat-completion-request.mjs b/components/openrouter/actions/send-chat-completion-request/send-chat-completion-request.mjs new file mode 100644 index 0000000000000..b3d21880b59cd --- /dev/null +++ b/components/openrouter/actions/send-chat-completion-request/send-chat-completion-request.mjs @@ -0,0 +1,190 @@ +import { ConfigurationError } from "@pipedream/platform"; +import { parseObject } from "../../common/utils.mjs"; +import openrouter from "../../openrouter.app.mjs"; + +export default { + key: "openrouter-send-chat-completion-request", + name: "Send Chat Completion Request", + version: "0.0.1", + description: "Send a chat completion request to a selected model. [See the documentation](https://openrouter.ai/docs/api-reference/chat-completion)", + type: "action", + props: { + openrouter, + model: { + propDefinition: [ + openrouter, + "model", + ], + }, + messages: { + type: "string[]", + label: "Messages", + description: "A list of objects containing role and content. E.g. **{\"role\":\"user\", \"content\":\"text\"}**. [See the documentation](https://openrouter.ai/docs/api-reference/chat-completion#request.body.messages) for further details.", + }, + maxTokens: { + propDefinition: [ + openrouter, + "maxTokens", + ], + }, + temperature: { + propDefinition: [ + openrouter, + "temperature", + ], + }, + seed: { + propDefinition: [ + openrouter, + "seed", + ], + }, + topP: { + propDefinition: [ + openrouter, + "topP", + ], + }, + topK: { + propDefinition: [ + openrouter, + "topK", + ], + }, + frequencyPenalty: { + propDefinition: [ + openrouter, + "frequencyPenalty", + ], + }, + presencePenalty: { + propDefinition: [ + openrouter, + "presencePenalty", + ], + }, + repetitionPenalty: { + propDefinition: [ + openrouter, + "repetitionPenalty", + ], + }, + logitBias: { + propDefinition: [ + openrouter, + "logitBias", + ], + }, + togLogprobs: { + propDefinition: [ + openrouter, + "togLogprobs", + ], + }, + minP: { + propDefinition: [ + openrouter, + "minP", + ], + }, + topA: { + propDefinition: [ + openrouter, + "topA", + ], + }, + transforms: { + propDefinition: [ + openrouter, + "transforms", + ], + }, + models: { + propDefinition: [ + openrouter, + "model", + ], + type: "string[]", + label: "Models", + description: "Alternate list of models for routing overrides.", + }, + sort: { + propDefinition: [ + openrouter, + "sort", + ], + }, + effort: { + propDefinition: [ + openrouter, + "effort", + ], + }, + reasoningMaxTokens: { + propDefinition: [ + openrouter, + "reasoningMaxTokens", + ], + }, + exclude: { + propDefinition: [ + openrouter, + "exclude", + ], + }, + }, + async run({ $ }) { + if (this.effort && this.reasoningMaxTokens) { + throw new ConfigurationError("**Reasoning Effort** and **Reasoning Max Tokens** cannot be used simultaneously."); + } + const data = { + model: this.model, + messages: parseObject(this.prompt), + stream: false, + maxTokens: this.maxTokens, + temperature: this.temperature && parseFloat(this.temperature), + seed: this.seed, + topP: this.topP && parseFloat(this.topP), + topK: this.topK, + frequencyPenalty: this.frequencyPenalty && parseFloat(this.frequencyPenalty), + presencePenalty: this.presencePenalty && parseFloat(this.presencePenalty), + repetitionPenalty: this.repetitionPenalty && parseFloat(this.repetitionPenalty), + logitBias: this.logitBias, + togLogprobs: this.togLogprobs, + minP: this.minP && parseFloat(this.minP), + topA: this.topA && parseFloat(this.topA), + transforms: this.transforms, + models: this.models, + provider: this.provider, + reasoning: this.reasoning, + }; + if (this.sort) { + data.provider = { + sort: this.sort, + }; + } + const reasoning = {}; + if (this.effort) { + reasoning.effort = this.effort; + } + if (this.reasoningMaxTokens) { + reasoning.max_tokens = parseFloat(this.reasoningMaxTokens); + } + if (this.exclude) { + reasoning.exclude = this.exclude; + } + if (Object.entries(reasoning).length) { + data.reasoning = reasoning; + } + const response = await this.openrouter.sendChatCompetionRequest({ + $, + data, + timeout: 1000 * 60 * 5, + }); + if (response.error) { + throw new ConfigurationError(response.error.message); + } + $.export("$summary", `A new chat completion request with Id: ${response.id} was successfully created!`); + return response; + }, +}; diff --git a/components/openrouter/actions/send-completion-request/send-completion-request.mjs b/components/openrouter/actions/send-completion-request/send-completion-request.mjs new file mode 100644 index 0000000000000..338de06b7d372 --- /dev/null +++ b/components/openrouter/actions/send-completion-request/send-completion-request.mjs @@ -0,0 +1,189 @@ +import { ConfigurationError } from "@pipedream/platform"; +import openrouter from "../../openrouter.app.mjs"; + +export default { + key: "openrouter-send-completion-request", + name: "Send Completion Request", + version: "0.0.1", + description: "Send a completion request to a selected model (text-only format) [See the documentation](https://openrouter.ai/docs/api-reference/completions)", + type: "action", + props: { + openrouter, + model: { + propDefinition: [ + openrouter, + "model", + ], + }, + prompt: { + type: "string", + label: "Prompt", + description: "The text prompt to complete.", + }, + maxTokens: { + propDefinition: [ + openrouter, + "maxTokens", + ], + }, + temperature: { + propDefinition: [ + openrouter, + "temperature", + ], + }, + seed: { + propDefinition: [ + openrouter, + "seed", + ], + }, + topP: { + propDefinition: [ + openrouter, + "topP", + ], + }, + topK: { + propDefinition: [ + openrouter, + "topK", + ], + }, + frequencyPenalty: { + propDefinition: [ + openrouter, + "frequencyPenalty", + ], + }, + presencePenalty: { + propDefinition: [ + openrouter, + "presencePenalty", + ], + }, + repetitionPenalty: { + propDefinition: [ + openrouter, + "repetitionPenalty", + ], + }, + logitBias: { + propDefinition: [ + openrouter, + "logitBias", + ], + }, + togLogprobs: { + propDefinition: [ + openrouter, + "togLogprobs", + ], + }, + minP: { + propDefinition: [ + openrouter, + "minP", + ], + }, + topA: { + propDefinition: [ + openrouter, + "topA", + ], + }, + transforms: { + propDefinition: [ + openrouter, + "transforms", + ], + }, + models: { + propDefinition: [ + openrouter, + "model", + ], + type: "string[]", + label: "Models", + description: "Alternate list of models for routing overrides.", + }, + sort: { + propDefinition: [ + openrouter, + "sort", + ], + }, + effort: { + propDefinition: [ + openrouter, + "effort", + ], + }, + reasoningMaxTokens: { + propDefinition: [ + openrouter, + "reasoningMaxTokens", + ], + }, + exclude: { + propDefinition: [ + openrouter, + "exclude", + ], + }, + }, + async run({ $ }) { + if (this.effort && this.reasoningMaxTokens) { + throw new ConfigurationError("**Reasoning Effort** and **Reasoning Max Tokens** cannot be used simultaneously."); + } + const data = { + model: this.model, + prompt: this.prompt, + stream: false, + maxTokens: this.maxTokens, + temperature: this.temperature && parseFloat(this.temperature), + seed: this.seed, + topP: this.topP && parseFloat(this.topP), + topK: this.topK, + frequencyPenalty: this.frequencyPenalty && parseFloat(this.frequencyPenalty), + presencePenalty: this.presencePenalty && parseFloat(this.presencePenalty), + repetitionPenalty: this.repetitionPenalty && parseFloat(this.repetitionPenalty), + logitBias: this.logitBias, + togLogprobs: this.togLogprobs, + minP: this.minP && parseFloat(this.minP), + topA: this.topA && parseFloat(this.topA), + transforms: this.transforms, + models: this.models, + provider: this.provider, + reasoning: this.reasoning, + }; + if (this.sort) { + data.provider = { + sort: this.sort, + }; + } + const reasoning = {}; + if (this.effort) { + reasoning.effort = this.effort; + } + if (this.reasoningMaxTokens) { + reasoning.max_tokens = parseFloat(this.reasoningMaxTokens); + } + if (this.exclude) { + reasoning.exclude = this.exclude; + } + if (Object.entries(reasoning).length) { + data.reasoning = reasoning; + } + const response = await this.openrouter.sendCompetionRequest({ + $, + data, + timeout: 1000 * 60 * 5, + }); + if (response.error) { + throw new ConfigurationError(response.error.message); + } + $.export("$summary", `A new completion request with Id: ${response.id} was successfully created!`); + return response; + }, +}; diff --git a/components/openrouter/common/constants.mjs b/components/openrouter/common/constants.mjs new file mode 100644 index 0000000000000..ca0e1ca1c2470 --- /dev/null +++ b/components/openrouter/common/constants.mjs @@ -0,0 +1,5 @@ +export const EFFORT_OPTIONS = [ + "high", + "medium", + "low", +]; diff --git a/components/openrouter/common/utils.mjs b/components/openrouter/common/utils.mjs new file mode 100644 index 0000000000000..dcc9cc61f6f41 --- /dev/null +++ b/components/openrouter/common/utils.mjs @@ -0,0 +1,24 @@ +export const parseObject = (obj) => { + if (!obj) return undefined; + + if (Array.isArray(obj)) { + return obj.map((item) => { + if (typeof item === "string") { + try { + return JSON.parse(item); + } catch (e) { + return item; + } + } + return item; + }); + } + if (typeof obj === "string") { + try { + return JSON.parse(obj); + } catch (e) { + return obj; + } + } + return obj; +}; diff --git a/components/openrouter/openrouter.app.mjs b/components/openrouter/openrouter.app.mjs index 65fe721b9dc29..ca10041dbaf26 100644 --- a/components/openrouter/openrouter.app.mjs +++ b/components/openrouter/openrouter.app.mjs @@ -1,11 +1,167 @@ +import { axios } from "@pipedream/platform"; +import { EFFORT_OPTIONS } from "./common/constants.mjs"; + export default { type: "app", app: "openrouter", - propDefinitions: {}, + propDefinitions: { + model: { + type: "string", + label: "Model", + description: "The model ID to use.", + async options() { + const { data } = await this.listModels(); + + return data.map(({ + id: value, name: label, + }) => ({ + label, + value, + })); + }, + }, + maxTokens: { + type: "integer", + label: "Max Tokens", + description: "Maximum number of tokens. **(range: [1, context_length))**.", + min: 1, + optional: true, + }, + temperature: { + type: "string", + label: "Temperature", + description: "Sampling temperature. **(range: [0, 2])**.", + optional: true, + }, + seed: { + type: "integer", + label: "Seed", + description: "Seed for deterministic outputs.", + optional: true, + }, + topP: { + type: "string", + label: "Top P", + description: "Top-p sampling value. **(range: (0, 1])**.", + optional: true, + }, + topK: { + type: "integer", + label: "Top K", + description: "Top-k sampling value. **(range: [1, Infinity))**.", + min: 1, + optional: true, + }, + frequencyPenalty: { + type: "string", + label: "Frequency Penalty", + description: "Frequency penalty. **(range: [-2, 2])**.", + optional: true, + }, + presencePenalty: { + type: "string", + label: "Presence Penalty", + description: "Presence penalty. **(range: [-2, 2])**.", + optional: true, + }, + repetitionPenalty: { + type: "string", + label: "Repetition Penalty", + description: "Repetition penalty. **(range: (0, 2])**.", + optional: true, + }, + logitBias: { + type: "string[]", + label: "Logit Bias", + description: "A list of token IDs to bias values.", + optional: true, + }, + togLogprobs: { + type: "integer", + label: "Top Logprobs", + description: "Number of top log probabilities to return.", + optional: true, + }, + minP: { + type: "string", + label: "Min P", + description: "Minimum probability threshold. **(range: [0, 1])**.", + optional: true, + }, + topA: { + type: "integer", + label: "Top A", + description: "Alternate top sampling parameter. **(range: [0, 1])**.", + optional: true, + }, + transforms: { + type: "string[]", + label: "Transforms", + description: "List of prompt transforms (OpenRouter-only).", + optional: true, + }, + sort: { + type: "string", + label: "Sort", + description: "Sort preference (e.g., price, throughput).", + optional: true, + }, + effort: { + type: "string", + label: "Reasoning Effort", + description: "OpenAI-style reasoning effort setting.", + options: EFFORT_OPTIONS, + optional: true, + }, + reasoningMaxTokens: { + type: "string", + label: "Reasoning Max Tokens", + description: "Non-OpenAI-style reasoning effort setting. Cannot be used simultaneously with effort.", + optional: true, + }, + exclude: { + type: "boolean", + label: "Reasoning Exclude", + description: "Whether to exclude reasoning from the response", + optional: true, + }, + }, methods: { - // this.$auth contains connected account data - authKeys() { - console.log(Object.keys(this.$auth)); + _apiUrl() { + return "https://openrouter.ai/api/v1"; + }, + _getHeaders() { + return { + "Authorization": `Bearer ${this.$auth.api_key}`, + }; + }, + _makeRequest({ + $ = this, path, ...opts + }) { + return axios($, { + url: `${this._apiUrl()}/${path}`, + headers: this._getHeaders(), + ...opts, + }); + }, + listModels() { + return this._makeRequest({ + path: "models", + }); + }, + sendChatCompetionRequest(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "chat/completions", + ...opts, + }); + }, + sendCompetionRequest(opts = {}) { + return this._makeRequest({ + method: "POST", + path: "completions", + ...opts, + }); }, }, }; diff --git a/components/openrouter/package.json b/components/openrouter/package.json index 002d31f9fe829..232d8628fff79 100644 --- a/components/openrouter/package.json +++ b/components/openrouter/package.json @@ -1,6 +1,6 @@ { "name": "@pipedream/openrouter", - "version": "0.0.1", + "version": "0.1.0", "description": "Pipedream OpenRouter Components", "main": "openrouter.app.mjs", "keywords": [ @@ -11,5 +11,8 @@ "author": "Pipedream (https://pipedream.com/)", "publishConfig": { "access": "public" + }, + "dependencies": { + "@pipedream/platform": "^3.0.3" } } From 65b682aff43b13989629fb5206dcaaf16943c73b Mon Sep 17 00:00:00 2001 From: Luan Cazarine Date: Mon, 14 Apr 2025 15:02:50 -0300 Subject: [PATCH 2/4] pnpm update --- pnpm-lock.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ddf106d676e64..9ffde83790658 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8998,7 +8998,11 @@ importers: specifier: ^2.19.5 version: 2.19.5 - components/openrouter: {} + components/openrouter: + dependencies: + '@pipedream/platform': + specifier: ^3.0.3 + version: 3.0.3 components/opensea: dependencies: From 32ea72c6e52cfee3d17d21d10ba50b49c5360bc1 Mon Sep 17 00:00:00 2001 From: Luan Cazarine Date: Mon, 14 Apr 2025 15:29:31 -0300 Subject: [PATCH 3/4] fix field name --- .../send-chat-completion-request.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/openrouter/actions/send-chat-completion-request/send-chat-completion-request.mjs b/components/openrouter/actions/send-chat-completion-request/send-chat-completion-request.mjs index b3d21880b59cd..01d0380502515 100644 --- a/components/openrouter/actions/send-chat-completion-request/send-chat-completion-request.mjs +++ b/components/openrouter/actions/send-chat-completion-request/send-chat-completion-request.mjs @@ -139,7 +139,7 @@ export default { } const data = { model: this.model, - messages: parseObject(this.prompt), + messages: parseObject(this.messages), stream: false, maxTokens: this.maxTokens, temperature: this.temperature && parseFloat(this.temperature), From 87fe0c9f37f91115fc518a27c8fe09ab6594a710 Mon Sep 17 00:00:00 2001 From: Luan Cazarine Date: Mon, 14 Apr 2025 15:41:27 -0300 Subject: [PATCH 4/4] remove unnecessary props --- .../send-chat-completion-request.mjs | 2 -- .../actions/send-completion-request/send-completion-request.mjs | 2 -- 2 files changed, 4 deletions(-) diff --git a/components/openrouter/actions/send-chat-completion-request/send-chat-completion-request.mjs b/components/openrouter/actions/send-chat-completion-request/send-chat-completion-request.mjs index 01d0380502515..c25660f8abf02 100644 --- a/components/openrouter/actions/send-chat-completion-request/send-chat-completion-request.mjs +++ b/components/openrouter/actions/send-chat-completion-request/send-chat-completion-request.mjs @@ -155,8 +155,6 @@ export default { topA: this.topA && parseFloat(this.topA), transforms: this.transforms, models: this.models, - provider: this.provider, - reasoning: this.reasoning, }; if (this.sort) { data.provider = { diff --git a/components/openrouter/actions/send-completion-request/send-completion-request.mjs b/components/openrouter/actions/send-completion-request/send-completion-request.mjs index 338de06b7d372..fb64cb636bc55 100644 --- a/components/openrouter/actions/send-completion-request/send-completion-request.mjs +++ b/components/openrouter/actions/send-completion-request/send-completion-request.mjs @@ -154,8 +154,6 @@ export default { topA: this.topA && parseFloat(this.topA), transforms: this.transforms, models: this.models, - provider: this.provider, - reasoning: this.reasoning, }; if (this.sort) { data.provider = {