From 96867a1b3c36cad9237da2c170b781ae4f7f1190 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 08:41:22 +0000 Subject: [PATCH 1/4] feat: rewrite Requesty n8n community node using AI Node SDK Complete rewrite of the Requesty n8n community node using the official @n8n/ai-node-sdk and n8n-node CLI tooling. Changes: - Replace old node with new LmChatRequesty using supplyModel() pattern - Update credentials to use RequestyApi with Bearer token auth - Add dynamic model loading from /models endpoint (531+ models) - Support all OpenAI-compatible parameters (temperature, maxTokens, etc.) - Add CI workflow for lint/build checks - Add publish workflow for npm releases with provenance - Update to MIT license and proper package metadata Tested end-to-end with n8n v2.22.6: - OpenAI (gpt-4o-mini) and Anthropic (claude-sonnet-4) via Requesty - Tool calling with Calculator (233 tokens, 2 model calls) - Streaming SSE responses - Dynamic model dropdown loading - Parameter options (temperature, max_tokens, top_p, etc.) Co-Authored-By: thibault --- .editorconfig | 12 - .eslintrc.js | 53 --- .eslintrc.prepublish.js | 5 - .github/workflows/ci.yml | 33 ++ .github/workflows/publish.yml | 72 +++++ .gitignore | 4 +- .npmignore | 2 - .prettierrc.js | 7 - CODE_OF_CONDUCT.md | 76 ----- LICENSE.md => LICENSE | 2 +- README.md | 92 ++++-- README_TEMPLATE.md | 27 -- credentials/RequestyApi.credentials.ts | 40 ++- eslint.config.mjs | 3 + gulpfile.js | 24 -- icons/requesty.svg | 4 + index.js | 12 - jest.config.js | 14 - nodes/LmChatRequesty/LmChatRequesty.node.ts | 159 +++++++++ nodes/Requesty/Requesty.node.ts | 338 -------------------- nodes/Requesty/requesty.svg | 5 - package.json | 72 ++--- tsconfig.json | 26 +- tslint.json | 6 - 24 files changed, 414 insertions(+), 674 deletions(-) delete mode 100644 .editorconfig delete mode 100644 .eslintrc.js delete mode 100644 .eslintrc.prepublish.js create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/publish.yml delete mode 100644 .npmignore delete mode 100644 .prettierrc.js delete mode 100644 CODE_OF_CONDUCT.md rename LICENSE.md => LICENSE (97%) delete mode 100644 README_TEMPLATE.md create mode 100644 eslint.config.mjs delete mode 100644 gulpfile.js create mode 100644 icons/requesty.svg delete mode 100644 index.js delete mode 100644 jest.config.js create mode 100644 nodes/LmChatRequesty/LmChatRequesty.node.ts delete mode 100644 nodes/Requesty/Requesty.node.ts delete mode 100644 nodes/Requesty/requesty.svg delete mode 100644 tslint.json diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 4039ff1..0000000 --- a/.editorconfig +++ /dev/null @@ -1,12 +0,0 @@ -root = true - -[*] -charset = utf-8 -end_of_line = lf -indent_size = 2 -indent_style = space -insert_final_newline = true -trim_trailing_whitespace = true - -[*.md] -trim_trailing_whitespace = false diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 7f0589f..0000000 --- a/.eslintrc.js +++ /dev/null @@ -1,53 +0,0 @@ -/** - * @type {import('@types/eslint').ESLint.ConfigData} - */ -module.exports = { - root: true, - - env: { - browser: true, - es6: true, - node: true, - }, - - parser: '@typescript-eslint/parser', - - parserOptions: { - project: ['./tsconfig.json'], - sourceType: 'module', - extraFileExtensions: ['.json'], - tsconfigRootDir: __dirname, - }, - - ignorePatterns: ['.eslintrc.js', '**/*.js', '**/node_modules/**', '**/dist/**'], - - overrides: [ - { - files: ['package.json'], - plugins: ['eslint-plugin-n8n-nodes-base'], - extends: ['plugin:n8n-nodes-base/community'], - rules: { - 'n8n-nodes-base/community-package-json-name-still-default': 'off', - }, - }, - { - files: ['./credentials/**/*.ts', './src/**/*.ts'], - plugins: ['eslint-plugin-n8n-nodes-base'], - extends: ['plugin:n8n-nodes-base/credentials'], - rules: { - 'n8n-nodes-base/cred-class-field-documentation-url-missing': 'off', - 'n8n-nodes-base/cred-class-field-documentation-url-miscased': 'off', - }, - }, - { - files: ['./nodes/**/*.ts', './src/**/*.ts'], - plugins: ['eslint-plugin-n8n-nodes-base'], - extends: ['plugin:n8n-nodes-base/nodes'], - rules: { - 'n8n-nodes-base/node-execute-block-missing-continue-on-fail': 'off', - 'n8n-nodes-base/node-resource-description-filename-against-convention': 'off', - 'n8n-nodes-base/node-param-fixed-collection-type-unsorted-items': 'off', - }, - }, - ], -}; diff --git a/.eslintrc.prepublish.js b/.eslintrc.prepublish.js deleted file mode 100644 index 9fb96c2..0000000 --- a/.eslintrc.prepublish.js +++ /dev/null @@ -1,5 +0,0 @@ -module.exports = { - extends: [ - 'n8n/prepublish', - ], -}; diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f035fa3 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,33 @@ +name: CI + +on: + push: + branches: [main] + pull_request: + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + lint-and-build: + name: Lint & Build + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 'lts/*' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint + + - name: Build + run: npm run build diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..dfbdcd7 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,72 @@ +# Publishes this n8n community node package to npm on every version tag push. +# +# Starting May 1 2026, n8n requires all community nodes to be published via +# GitHub Actions with npm provenance statements. This workflow satisfies that +# requirement. Provenance lets anyone cryptographically verify that a package +# was built by this exact workflow, from this exact repository and commit. +# +# ─── ONE-TIME SETUP ──────────────────────────────────────────────────────────── +# +# Option A — OIDC Trusted Publishing (recommended, no long-lived secrets): +# 1. Log in to npmjs.com and open your package's settings. +# 2. Under "Publish access" → "Trusted Publishers", click "Add a publisher". +# 3. Select GitHub Actions and fill in: +# Repository owner: requestyai +# Repository name: n8n-requesty +# Workflow name: publish.yml +# Environment: (leave blank unless you use GitHub Environments) +# 4. Leave the NPM_TOKEN secret unset in this repository. GitHub's OIDC +# token is used instead — no secret ever touches your repo settings. +# +# Option B — npm Automation Token (fallback): +# 1. On npmjs.com: Access Tokens → Generate New Token → Granular Access Token. +# Scope it to this package with "Read and write" publish permission. +# 2. In GitHub: Settings → Secrets and variables → Actions → New secret. +# Name it NPM_TOKEN and paste the token value. +# +# Both options work with --provenance. Provenance is signed by GitHub's OIDC +# infrastructure regardless of how npm authentication is handled. +# +# ─── RELEASE PROCESS ─────────────────────────────────────────────────────────── +# +# Run the following command locally to start an interactive release: +# +# npm run release +# +# This will lint, build, prompt for a version bump, update the changelog, +# commit, tag, and push — which triggers this workflow to publish to npm. + +name: Publish + +on: + push: + tags: + - '*.*.*' + +jobs: + publish: + name: Publish to npm + runs-on: ubuntu-latest + + permissions: + id-token: write + contents: read + + steps: + - uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 'lts/*' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Release + run: | + [ -n "$NPM_TOKEN" ] && npm config set //registry.npmjs.org/:_authToken "$NPM_TOKEN" + npm run release + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 3bdd52e..ea4b729 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ node_modules/ dist/ -.DS_Store +pnpm-lock.yaml +package-lock.json +*.tsbuildinfo diff --git a/.npmignore b/.npmignore deleted file mode 100644 index d506e8d..0000000 --- a/.npmignore +++ /dev/null @@ -1,2 +0,0 @@ -credentials/ -*.test.ts diff --git a/.prettierrc.js b/.prettierrc.js deleted file mode 100644 index 1ec6b20..0000000 --- a/.prettierrc.js +++ /dev/null @@ -1,7 +0,0 @@ -module.exports = { - trailingComma: 'es5', - tabWidth: 4, - semi: true, - singleQuote: true, - printWidth: 120, -}; diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md deleted file mode 100644 index e005677..0000000 --- a/CODE_OF_CONDUCT.md +++ /dev/null @@ -1,76 +0,0 @@ -## Contributor Covenant Code of Conduct - -### Our Pledge - -In the interest of fostering an open and welcoming environment, we as -contributors and maintainers pledge to making participation in our project and -our community a harassment-free experience for everyone, regardless of age, body -size, disability, ethnicity, sex characteristics, gender identity and expression, -level of experience, education, socio-economic status, nationality, personal -appearance, race, religion, or sexual identity and orientation. - -### Our Standards - -Examples of behavior that contributes to creating a positive environment -include: - -* Using welcoming and inclusive language -* Being respectful of differing viewpoints and experiences -* Gracefully accepting constructive criticism -* Focusing on what is best for the community -* Showing empathy towards other community members - -Examples of unacceptable behavior by participants include: - -* The use of sexualized language or imagery and unwelcome sexual attention or - advances -* Trolling, insulting/derogatory comments, and personal or political attacks -* Public or private harassment -* Publishing others' private information, such as a physical or electronic - address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a - professional setting - -### Our Responsibilities - -Project maintainers are responsible for clarifying the standards of acceptable -behavior and are expected to take appropriate and fair corrective action in -response to any instances of unacceptable behavior. - -Project maintainers have the right and responsibility to remove, edit, or -reject comments, commits, code, wiki edits, issues, and other contributions -that are not aligned to this Code of Conduct, or to ban temporarily or -permanently any contributor for other behaviors that they deem inappropriate, -threatening, offensive, or harmful. - -### Scope - -This Code of Conduct applies both within project spaces and in public spaces -when an individual is representing the project or its community. Examples of -representing a project or community include using an official project e-mail -address, posting via an official social media account, or acting as an appointed -representative at an online or offline event. Representation of a project may be -further defined and clarified by project maintainers. - -### Enforcement - -Instances of abusive, harassing, or otherwise unacceptable behavior may be -reported by contacting the project team at [INSERT EMAIL ADDRESS]. All -complaints will be reviewed and investigated and will result in a response that -is deemed necessary and appropriate to the circumstances. The project team is -obligated to maintain confidentiality with regard to the reporter of an incident. -Further details of specific enforcement policies may be posted separately. - -Project maintainers who do not follow or enforce the Code of Conduct in good -faith may face temporary or permanent repercussions as determined by other -members of the project's leadership. - -### Attribution - -This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, -available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html - -[homepage]: https://www.contributor-covenant.org - -For answers to common questions about this code of conduct, see -https://www.contributor-covenant.org/faq diff --git a/LICENSE.md b/LICENSE similarity index 97% rename from LICENSE.md rename to LICENSE index 8dfcede..f91f541 100644 --- a/LICENSE.md +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2023 n8n.io +Copyright (c) 2024 Requesty Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 8d3af9b..b632146 100644 --- a/README.md +++ b/README.md @@ -1,43 +1,83 @@ -# n8n-requesty +# n8n-nodes-requesty -An n8n community node for Requesty AI integration, allowing you to interact with various AI models through a unified API. +An n8n community node for using [Requesty](https://requesty.ai)-hosted chat models in your n8n workflows. -[![Follow on X](https://img.shields.io/twitter/follow/ThibaultJaigu?style=social&logo=twitter)](https://x.com/ThibaultJaigu) +Requesty is a unified AI gateway providing access to 300+ models from OpenAI, Anthropic, Google, Meta, Mistral, and more — all through a single OpenAI-compatible API with intelligent routing, automatic fallbacks, and cost optimization. -## Features - -- Chat with AI models through Requesty's API -- Customize parameters like temperature and max tokens -- View model descriptions and pricing information +[Installation](#installation) | [Credentials](#credentials) | [Usage](#usage) | [Resources](#resources) ## Installation -Install via n8n's Community Nodes: +Follow the [installation guide](https://docs.n8n.io/integrations/community-nodes/installation/) in the n8n community nodes documentation. + +In your n8n instance, go to **Settings > Community Nodes** and install: -1. Go to "Settings" > "Community Nodes" -2. Install `n8n-requesty` +``` +@requestyai/n8n-nodes-requesty +``` -## Configuration +## Credentials -1. Sign up at [Requesty](https://app.requesty.ai/sign-up) -2. Generate an API key at [Getting Started](https://app.requesty.ai/getting-started) -3. Create a credential of type 'Requesty API' in n8n -4. Enter your API key +1. Sign up at [app.requesty.ai](https://app.requesty.ai/sign-up) +2. Go to **Getting Started** and generate an API key at [app.requesty.ai/getting-started](https://app.requesty.ai/getting-started) +3. In n8n, create a new **Requesty API** credential and paste your key ## Usage -1. Add the Requesty node to your workflow -2. Select 'Chat' operation -3. Choose an AI model -4. Optional: Set a system prompt -5. Enter your message -6. Adjust parameters as needed -7. Execute to get the AI response +The **Requesty Chat Model** node connects to any of the 300+ models available through Requesty's unified gateway. Use it anywhere n8n accepts a chat model — e.g., the AI Agent node, Basic LLM Chain, or any AI workflow. + +Once your API key is saved, the **Model** dropdown auto-populates with all available models. You can also set it to a model ID directly using an expression (e.g., `anthropic/claude-sonnet-4-20250514`, `openai/gpt-4o`). + +### Configuration Options + +| Option | Default | Description | +|--------|---------|-------------| +| Temperature | 0.7 | Controls randomness (0 = deterministic, 2 = very random) | +| Maximum Tokens | -1 | Max tokens to generate (-1 for no limit) | +| Top P | 1 | Nucleus sampling probability mass | +| Frequency Penalty | 0 | Penalizes token repetition (-2 to 2) | +| Presence Penalty | 0 | Penalizes already-seen tokens (-2 to 2) | + +### Key Features + +- **300+ Models**: Access models from OpenAI, Anthropic, Google, Meta, Mistral, Cohere, and more +- **Intelligent Routing**: Automatic fallbacks and load balancing across providers +- **Cost Optimization**: Track spending and optimize model selection +- **OpenAI-Compatible**: Drop-in replacement for any OpenAI-compatible integration +- **Zero Data Retention**: Optional ZDR-compliant model filtering + +## Resources + +- [Requesty Documentation](https://docs.requesty.ai) +- [Requesty Model Library](https://requesty.ai/models) +- [n8n community nodes documentation](https://docs.n8n.io/integrations/#community-nodes) + +## Development + +```bash +# Install dependencies +npm install + +# Build the node +npm run build + +# Run in development mode (starts n8n with hot reload) +npm run dev + +# Lint +npm run lint +``` + +## Publishing + +This package uses GitHub Actions with npm provenance for publishing. To release: -## Support +```bash +npm run release +``` -For issues or questions, please [open an issue](https://github.com/requestyai/n8n-requesty/issues). +This will lint, build, prompt for a version bump, commit, tag, and push — triggering the publish workflow. ## License -[MIT License](LICENSE.md) +[MIT](LICENSE) diff --git a/README_TEMPLATE.md b/README_TEMPLATE.md deleted file mode 100644 index eba5f4d..0000000 --- a/README_TEMPLATE.md +++ /dev/null @@ -1,27 +0,0 @@ -# {{NODE_NAME}} - -[![n8n.io](https://img.shields.io/badge/powered%20by-n8n.io-green.svg?style=for-the-badge)](https://n8n.io) - -## Description - -Describe your node here. - -## Credentials - -Describe the credentials needed here. - -## Installation - -Follow the [installation guide](https://docs.n8n.io/integrations/creating-nodes/create-nodes/) in the n8n community nodes documentation. - -## Usage - -Describe how to use your node here. - -## Resources - -* [n8n community nodes documentation](https://docs.n8n.io/integrations/creating-nodes/) - -## License - -[MIT](LICENSE.md) diff --git a/credentials/RequestyApi.credentials.ts b/credentials/RequestyApi.credentials.ts index 8508938..b3d61cf 100644 --- a/credentials/RequestyApi.credentials.ts +++ b/credentials/RequestyApi.credentials.ts @@ -1,21 +1,47 @@ -import { - ICredentialType, - INodeProperties, +import type { + ICredentialDataDecryptedObject, + ICredentialTestRequest, + ICredentialType, + IHttpRequestOptions, + INodeProperties, + Icon, } from 'n8n-workflow'; export class RequestyApi implements ICredentialType { name = 'requestyApi'; + displayName = 'Requesty API'; - documentationUrl = 'https://github.com/requestyai/n8n-requesty'; + + icon: Icon = 'file:../icons/requesty.svg'; + + documentationUrl = 'https://docs.requesty.ai'; + properties: INodeProperties[] = [ { displayName: 'API Key', name: 'apiKey', type: 'string', - typeOptions: { - password: true, - }, + typeOptions: { password: true }, + required: true, default: '', + description: + 'Your Requesty API key. Find it at app.requesty.ai/getting-started', }, ]; + + test: ICredentialTestRequest = { + request: { + baseURL: 'https://router.requesty.ai/v1', + url: '/models', + }, + }; + + async authenticate( + credentials: ICredentialDataDecryptedObject, + requestOptions: IHttpRequestOptions, + ): Promise { + requestOptions.headers ??= {}; + requestOptions.headers['Authorization'] = `Bearer ${credentials.apiKey}`; + return requestOptions; + } } diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000..ad811a0 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,3 @@ +import { config } from '@n8n/node-cli/eslint'; + +export default config; diff --git a/gulpfile.js b/gulpfile.js deleted file mode 100644 index a576e2f..0000000 --- a/gulpfile.js +++ /dev/null @@ -1,24 +0,0 @@ -const { src, dest, series } = require('gulp'); -const replace = require('gulp-replace'); -const rename = require('gulp-rename'); -const path = require('path'); - -const packageJson = require('./package.json'); - -function copyReadme() { - return src('README_TEMPLATE.md') - .pipe(replace('{{NODE_NAME}}', packageJson.n8n.name)) - .pipe(rename('README.md')) - .pipe(dest('.')); -} - -function copyAssets() { - return src('nodes/**/requesty.svg') - .pipe(dest(file => { - // Construct the destination path relative to the 'dist' directory - const relativePath = path.relative('nodes', file.path); - return path.join('dist', relativePath); - })); -} - -exports.default = series(copyReadme, copyAssets); diff --git a/icons/requesty.svg b/icons/requesty.svg new file mode 100644 index 0000000..d15d9d2 --- /dev/null +++ b/icons/requesty.svg @@ -0,0 +1,4 @@ + + + + diff --git a/index.js b/index.js deleted file mode 100644 index 3754dff..0000000 --- a/index.js +++ /dev/null @@ -1,12 +0,0 @@ -const { RequestyNode } = require('./dist/nodes/Requesty/Requesty.node'); -const { RequestyApi } = require('./dist/credentials/RequestyApi.credentials'); - -module.exports = { - nodes: [ - RequestyNode - ], - credentials: [ - RequestyApi - ], - version: require('./package.json').version, -}; diff --git a/jest.config.js b/jest.config.js deleted file mode 100644 index 0217a00..0000000 --- a/jest.config.js +++ /dev/null @@ -1,14 +0,0 @@ -module.exports = { - preset: 'ts-jest', - testEnvironment: 'node', - testMatch: ['**/*.test.ts'], - moduleFileExtensions: ['ts', 'js', 'json'], - transform: { - '^.+\\.ts$': 'ts-jest', - }, - globals: { - 'ts-jest': { - tsconfig: 'tsconfig.json', - }, - }, -}; diff --git a/nodes/LmChatRequesty/LmChatRequesty.node.ts b/nodes/LmChatRequesty/LmChatRequesty.node.ts new file mode 100644 index 0000000..6b01449 --- /dev/null +++ b/nodes/LmChatRequesty/LmChatRequesty.node.ts @@ -0,0 +1,159 @@ +import type { INodeType, INodeTypeDescription, ISupplyDataFunctions } from 'n8n-workflow'; +import { NodeConnectionTypes } from 'n8n-workflow'; +import { supplyModel } from '@n8n/ai-node-sdk'; + +type ModelOptions = { + temperature?: number; + maxTokens?: number; + topP?: number; + frequencyPenalty?: number; + presencePenalty?: number; +}; + +export class LmChatRequesty implements INodeType { + description: INodeTypeDescription = { + displayName: 'Requesty Chat Model', + name: 'lmChatRequesty', + icon: 'file:../../icons/requesty.svg', + group: ['transform'], + version: [1], + description: 'Use 300+ AI models through Requesty unified gateway', + subtitle: '={{$parameter["model"]}}', + defaults: { + name: 'Requesty Chat Model', + }, + codex: { + categories: ['assistant'], + subcategories: { + AI: ['Language Models', 'Root Nodes'], + 'Language Models': ['Chat Models (Recommended)'], + }, + resources: { + primaryDocumentation: [ + { + url: 'https://docs.requesty.ai', + }, + ], + }, + }, + inputs: [], + outputs: [NodeConnectionTypes.AiLanguageModel], + outputNames: ['Model'], + credentials: [ + { + name: 'requestyApi', + required: true, + }, + ], + requestDefaults: { + ignoreHttpStatusErrors: true, + baseURL: 'https://router.requesty.ai/v1', + }, + properties: [ + { + displayName: 'Model', + name: 'model', + type: 'options', + description: + 'The model to use. Choose from the list, or specify a model ID using an expression.', + typeOptions: { + loadOptions: { + routing: { + request: { + method: 'GET', + url: '/models', + }, + output: { + postReceive: [ + { type: 'rootProperty', properties: { property: 'data' } }, + { + type: 'setKeyValue', + properties: { + name: '={{$responseItem.id}}', + value: '={{$responseItem.id}}', + }, + }, + { type: 'sort', properties: { key: 'name' } }, + ], + }, + }, + }, + }, + default: '', + }, + { + displayName: 'Options', + name: 'options', + placeholder: 'Add Option', + description: 'Additional options to add', + type: 'collection', + default: {}, + options: [ + { + displayName: 'Frequency Penalty', + name: 'frequencyPenalty', + default: 0, + typeOptions: { maxValue: 2, minValue: -2, numberPrecision: 1 }, + description: + 'Penalizes new tokens based on their existing frequency in the text so far, decreasing the likelihood of repetition', + type: 'number', + }, + { + displayName: 'Maximum Tokens', + name: 'maxTokens', + default: -1, + typeOptions: { minValue: -1 }, + description: + 'The maximum number of tokens to generate in the response. Set to -1 for no limit.', + type: 'number', + }, + { + displayName: 'Presence Penalty', + name: 'presencePenalty', + default: 0, + typeOptions: { maxValue: 2, minValue: -2, numberPrecision: 1 }, + description: + 'Penalizes new tokens based on whether they appear in the text so far, increasing the likelihood of talking about new topics', + type: 'number', + }, + { + displayName: 'Sampling Temperature', + name: 'temperature', + default: 0.7, + typeOptions: { maxValue: 2, minValue: 0, numberPrecision: 1 }, + description: + 'Controls randomness: Lowering results in less random completions. As the temperature approaches zero, the model will become deterministic and repetitive.', + type: 'number', + }, + { + displayName: 'Top P', + name: 'topP', + default: 1, + typeOptions: { maxValue: 1, minValue: 0, numberPrecision: 1 }, + description: + 'An alternative to sampling with temperature, called nucleus sampling. The model considers tokens with top_p probability mass.', + type: 'number', + }, + ], + }, + ], + }; + + async supplyData(this: ISupplyDataFunctions, itemIndex: number) { + const credentials = await this.getCredentials('requestyApi'); + const model = this.getNodeParameter('model', itemIndex) as string; + const options = this.getNodeParameter('options', itemIndex, {}) as ModelOptions; + + return supplyModel(this, { + type: 'openai', + baseUrl: 'https://router.requesty.ai/v1', + apiKey: credentials.apiKey as string, + model, + temperature: options.temperature, + maxTokens: options.maxTokens, + topP: options.topP, + frequencyPenalty: options.frequencyPenalty, + presencePenalty: options.presencePenalty, + }); + } +} diff --git a/nodes/Requesty/Requesty.node.ts b/nodes/Requesty/Requesty.node.ts deleted file mode 100644 index f0f8f7a..0000000 --- a/nodes/Requesty/Requesty.node.ts +++ /dev/null @@ -1,338 +0,0 @@ -import type { - IDataObject, - IExecuteFunctions, - IHttpRequestMethods, - ILoadOptionsFunctions, - INodeExecutionData, - INodePropertyOptions, - INodeType, - INodeTypeDescription, - IRequestOptions, -} from 'n8n-workflow'; -import { NodeOperationError } from 'n8n-workflow'; - -interface IRequestyModel { - id: string; - name: string; - description?: string; - context_length: number; - pricing: { - prompt: string; - completion: string; - }; -} - -interface IRequestyResponse extends IDataObject { - id: string; - model: string; - created: number; - object: string; - usage: { - prompt_tokens: number; - completion_tokens: number; - total_tokens: number; - }; - choices: Array<{ - message: { - role: string; - content: string; - }; - finish_reason: string; - index: number; - }>; -} - -export class Requesty implements INodeType { - description: INodeTypeDescription = { - displayName: 'Requesty', - name: 'requesty', - icon: 'file:requesty.svg', - group: ['transform'], - version: 1, - subtitle: '={{$parameter["operation"]}}', - description: 'Interact with Requesty API', - defaults: { - name: 'Requesty', - }, - inputs: '={{["main"]}}', - outputs: '={{["main"]}}', - credentials: [ - { - name: 'requestyApi', - required: true, - }, - ], - properties: [ - { - displayName: 'Operation', - name: 'operation', - type: 'options', - noDataExpression: true, - options: [ - { - name: 'Chat', - value: 'chat', - description: 'Send a chat message', - action: 'Send a chat message', - }, - ], - default: 'chat', - }, - { - displayName: 'Model Name or ID', - name: 'model', - type: 'options', - noDataExpression: true, - typeOptions: { - loadOptionsMethod: 'getModels', - }, - required: true, - default: '', - description: - 'Choose from the list, or specify an ID using an expression', - }, - { - displayName: 'System Prompt', - name: 'system_prompt', - type: 'string', - typeOptions: { - rows: 4, - }, - default: '', - description: 'System message to set the behavior of the assistant', - placeholder: 'You are a helpful assistant...', - }, - { - displayName: 'Message', - name: 'message', - type: 'string', - typeOptions: { - rows: 4, - }, - default: '', - description: 'The message to send to the chat model', - required: true, - }, - { - displayName: 'Temperature', - name: 'temperature', - type: 'number', - default: 0.9, - description: 'What sampling temperature to use', - }, - { - displayName: 'Additional Fields', - name: 'additionalFields', - type: 'collection', - placeholder: 'Add Field', - default: {}, - options: [ - { - displayName: 'Frequency Penalty', - name: 'frequency_penalty', - type: 'number', - default: 0, - description: - 'Number between -2.0 and 2.0. Positive values penalize new tokens based on their existing frequency.', - }, - { - displayName: 'Max Tokens', - name: 'max_tokens', - type: 'number', - default: 1000, - description: 'The maximum number of tokens to generate', - }, - { - displayName: 'Presence Penalty', - name: 'presence_penalty', - type: 'number', - default: 0, - description: - 'Number between -2.0 and 2.0. Positive values penalize new tokens based on whether they appear in the text so far.', - }, - { - displayName: 'Top P', - name: 'top_p', - type: 'number', - default: 1, - description: 'An alternative to sampling with temperature, called nucleus sampling', - }, - ], - }, - ], - }; - - methods = { - loadOptions: { - async getModels(this: ILoadOptionsFunctions): Promise { - const credentials = await this.getCredentials('requestyApi'); - const options: IRequestOptions = { - url: 'https://router.requesty.ai/v1/models', - headers: { - Authorization: `Bearer ${credentials.apiKey}`, - 'HTTP-Referer': 'https://github.com/requestyai/n8n-requesty', - 'X-Title': 'n8n Requesty Node', - 'Content-Type': 'application/json', - }, - method: 'GET' as IHttpRequestMethods, - json: true, - }; - - try { - const response = await this.helpers.request(options); - - // Debug log to help investigate issues - console.log('Requesty API Response:', JSON.stringify(response, null, 2)); - - if (!response?.data || !Array.isArray(response.data)) { - throw new NodeOperationError(this.getNode(), 'Invalid response format from Requesty API'); - } - - // Create a safer description function - const formatDescription = (model: IRequestyModel): string => { - try { - const desc = model.description || 'No description available'; - let pricingInfo = ''; - - if (model.pricing && typeof model.pricing === 'object') { - try { - const promptPrice = parseFloat(String(model.pricing.prompt)) * 1000000; - const completionPrice = parseFloat(String(model.pricing.completion)) * 1000000; - - if (!isNaN(promptPrice) && !isNaN(completionPrice)) { - pricingInfo = ` - Price: $${promptPrice.toFixed( - 2 - )}/1M tokens (prompt), $${completionPrice.toFixed(2)}/1M tokens (completion)`; - } - } catch (e) { - console.log('Error parsing pricing for model:', model.id, e); - } - } - - return (desc + pricingInfo).trim(); - } catch (e) { - console.error('Error formatting description for model:', model.id, e); - return 'Error formatting description'; - } - }; - - // Filter and map the models with less strict validation - const models = response.data - .filter((model: IRequestyModel) => { - // Only require ID to be present - const valid = Boolean(model.id); - if (!valid) { - console.log('Filtering out invalid model (missing ID):', model); - } - return valid; - }) - .map((model: IRequestyModel) => ({ - // Use ID as name if name is missing - name: model.name || model.id, - value: model.id, - description: formatDescription(model), - })) - .sort((a: INodePropertyOptions, b: INodePropertyOptions) => a.name.localeCompare(b.name)); - - if (models.length === 0) { - throw new NodeOperationError(this.getNode(), 'No models found in Requesty API response'); - } - - console.log('Processed models:', models); - return models; - } catch (error) { - console.error('Error loading models from Requesty API:', error); - throw new NodeOperationError(this.getNode(), `Failed to load models: ${(error as Error).message}`); - } - }, - }, - }; - - async execute(this: IExecuteFunctions): Promise { - const items = this.getInputData(); - const returnData: INodeExecutionData[] = []; - - const credentials = await this.getCredentials('requestyApi'); - if (!credentials?.apiKey) { - throw new NodeOperationError(this.getNode(), 'No valid API key provided'); - } - - for (let i = 0; i < items.length; i++) { - try { - const operation = this.getNodeParameter('operation', i) as string; - const model = this.getNodeParameter('model', i) as string; - const systemPrompt = this.getNodeParameter('system_prompt', i, '') as string; - const message = this.getNodeParameter('message', i) as string; - const temperature = this.getNodeParameter('temperature', i) as number; - const additionalFields = this.getNodeParameter('additionalFields', i) as IDataObject; - - if (operation === 'chat') { - const messages = []; - - // Add system message if provided - if (systemPrompt) { - messages.push({ - role: 'system', - content: systemPrompt, - }); - } - - // Add user message - messages.push({ - role: 'user', - content: message, - }); - - const requestBody = { - model, - messages, - temperature, - ...additionalFields, - }; - - const options: IRequestOptions = { - url: 'https://router.requesty.ai/v1/chat/completions', - headers: { - Authorization: `Bearer ${credentials.apiKey}`, - 'HTTP-Referer': 'https://github.com/requestyai/n8n-requesty', - 'X-Title': 'n8n Requesty Node', - 'Content-Type': 'application/json', - }, - method: 'POST' as IHttpRequestMethods, - body: requestBody, - json: true, - }; - - const response = await this.helpers.request(options); - - if (!response?.choices?.[0]?.message?.content) { - throw new NodeOperationError(this.getNode(), 'Invalid response format from Requesty API'); - } - - const typedResponse = response as IRequestyResponse; - const messageContent = typedResponse.choices[0].message.content.trim(); - - returnData.push({ - json: { - response: messageContent, - }, - pairedItem: { item: i }, - }); - } - } catch (error) { - if (this.continueOnFail()) { - returnData.push({ - json: { - error: (error as Error).message, - }, - pairedItem: { item: i }, - }); - continue; - } - throw error; - } - } - - return [returnData]; - } -} diff --git a/nodes/Requesty/requesty.svg b/nodes/Requesty/requesty.svg deleted file mode 100644 index 33bfe08..0000000 --- a/nodes/Requesty/requesty.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/package.json b/package.json index fd562a9..b0f00a7 100644 --- a/package.json +++ b/package.json @@ -1,59 +1,37 @@ { - "name": "n8n-requesty", - "version": "0.0.1", - "description": "n8n node for Requesty API integration", - "keywords": [ - "n8n-community-node-package", - "n8n", - "requesty", - "ai" - ], + "name": "@requestyai/n8n-nodes-requesty", + "version": "1.0.0", + "description": "n8n community node for Requesty AI chat model integration", "license": "MIT", - "homepage": "https://x.com/ThibaultJaigu", - "author": { - "name": "Thibault Jaigu", - "url": "https://x.com/ThibaultJaigu" - }, + "homepage": "https://requesty.ai", + "keywords": ["n8n-community-node-package"], + "author": { "name": "Requesty", "email": "support@requesty.ai" }, "repository": { "type": "git", - "url": "git+https://github.com/requestyai/n8n-requesty.git" + "url": "https://github.com/requestyai/n8n-requesty.git" }, - "main": "index.js", "scripts": { - "prebuild": "rm -rf dist && echo 'Cleaned dist directory'", - "build": "echo 'Starting build...' && npx tsc --project tsconfig.json && echo 'TypeScript compilation complete' && mkdir -p dist/nodes/Requesty && echo 'Created directories' && cp nodes/Requesty/requesty.svg dist/nodes/Requesty/ && echo 'Copied SVG' && ls -la dist/nodes/Requesty/", - "dev": "tsc --watch", - "format": "prettier nodes credentials --write", - "lint": "eslint ./nodes ./credentials package.json", - "lintfix": "eslint ./nodes ./credentials package.json --fix", - "prepublishOnly": "npm run build && npm run lint", - "test": "jest" + "build": "n8n-node build", + "dev": "n8n-node dev", + "lint": "n8n-node lint", + "lint:fix": "n8n-node lint --fix", + "release": "n8n-node release", + "prepublishOnly": "n8n-node prerelease" }, - "files": [ - "dist", - "README.md" - ], + "files": ["dist"], + "publishConfig": { "access": "public" }, "n8n": { "n8nNodesApiVersion": 1, - "credentials": [ - "dist/credentials/RequestyApi.credentials.js" - ], - "nodes": [ - "dist/nodes/Requesty/Requesty.node.js" - ] + "aiNodeSdkVersion": 1, + "strict": true, + "credentials": ["dist/credentials/RequestyApi.credentials.js"], + "nodes": ["dist/nodes/LmChatRequesty/LmChatRequesty.node.js"] }, "devDependencies": { - "@types/express": "^4.17.21", - "@types/jest": "^29.5.13", - "@types/request-promise-native": "~1.0.15", - "@typescript-eslint/parser": "~5.45", - "eslint": "^8.0.1", - "eslint-plugin-n8n-nodes-base": "^1.16.3", - "jest": "^29.7.0", - "n8n-core": "^1.59.1", - "n8n-workflow": "^1.59.1", - "prettier": "^2.8.8", - "ts-jest": "^29.2.5", - "typescript": "~4.8.4" - } + "@n8n/node-cli": "*", + "eslint": "9.29.0", + "prettier": "3.6.2", + "typescript": "5.9.2" + }, + "peerDependencies": { "n8n-workflow": "*", "ai-node-sdk": "*" } } diff --git a/tsconfig.json b/tsconfig.json index 7e06b8b..ddf3670 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,18 +4,22 @@ "module": "commonjs", "moduleResolution": "node", "target": "es2019", - "outDir": "./dist", - "rootDir": ".", - "types": ["node", "jest"], - "esModuleInterop": true, - "skipLibCheck": true, + "lib": ["es2019", "es2020", "es2022.error"], + "removeComments": true, + "useUnknownInCatchVariables": false, "forceConsistentCasingInFileNames": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noUnusedLocals": true, + "strictNullChecks": true, + "preserveConstEnums": true, + "esModuleInterop": true, "resolveJsonModule": true, - "baseUrl": ".", - "paths": { - "n8n-workflow": ["node_modules/n8n-workflow"] - } + "incremental": true, + "declaration": true, + "sourceMap": true, + "skipLibCheck": true, + "outDir": "./dist/" }, - "include": ["credentials/**/*", "nodes/**/*", "package.json"], - "exclude": ["node_modules", "dist"] + "include": ["credentials/**/*", "nodes/**/*", "nodes/**/*.json", "package.json"] } diff --git a/tslint.json b/tslint.json deleted file mode 100644 index de1f16a..0000000 --- a/tslint.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "extends": "n8n/tslint.json", - "rules": { - "no-console": false - } -} From 5ed5c2650ba4b10747a54e5b586aab97bf224905 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 08:55:11 +0000 Subject: [PATCH 2/4] fix(ci): fix peer dep name and remove lockfile cache requirement - Rename ai-node-sdk -> @n8n/ai-node-sdk in peerDependencies - Use npm install --ignore-scripts instead of npm ci (no lockfile needed) - Remove cache: npm since no lockfile is committed Co-Authored-By: thibault --- .github/workflows/ci.yml | 3 +-- package.json | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f035fa3..6acc166 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,10 +21,9 @@ jobs: uses: actions/setup-node@v4 with: node-version: 'lts/*' - cache: 'npm' - name: Install dependencies - run: npm ci + run: npm install --ignore-scripts - name: Lint run: npm run lint diff --git a/package.json b/package.json index b0f00a7..ab2c9e0 100644 --- a/package.json +++ b/package.json @@ -33,5 +33,5 @@ "prettier": "3.6.2", "typescript": "5.9.2" }, - "peerDependencies": { "n8n-workflow": "*", "ai-node-sdk": "*" } + "peerDependencies": { "n8n-workflow": "*", "@n8n/ai-node-sdk": "*" } } From 70b3f1a18402eff62e1d1eb0fce80de2352a88aa Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 08:57:15 +0000 Subject: [PATCH 3/4] fix(ci): use ai-node-sdk (required by n8n lint) + legacy-peer-deps n8n's @n8n/community-nodes/valid-peer-dependencies lint rule requires exactly 'ai-node-sdk' as the peer dep name. This package isn't on npm (provided by n8n at runtime), so CI uses --legacy-peer-deps to skip resolution. Co-Authored-By: thibault --- .github/workflows/ci.yml | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6acc166..0ee2eb7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: node-version: 'lts/*' - name: Install dependencies - run: npm install --ignore-scripts + run: npm install --legacy-peer-deps - name: Lint run: npm run lint diff --git a/package.json b/package.json index ab2c9e0..b0f00a7 100644 --- a/package.json +++ b/package.json @@ -33,5 +33,5 @@ "prettier": "3.6.2", "typescript": "5.9.2" }, - "peerDependencies": { "n8n-workflow": "*", "@n8n/ai-node-sdk": "*" } + "peerDependencies": { "n8n-workflow": "*", "ai-node-sdk": "*" } } From 8d4c2da02e95cf98ef9eab7266b6b6a46a889b97 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Tue, 2 Jun 2026 08:59:43 +0000 Subject: [PATCH 4/4] fix(ci): add n8n-workflow as devDependency for TypeScript compilation Co-Authored-By: thibault --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index b0f00a7..540ada2 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ }, "devDependencies": { "@n8n/node-cli": "*", + "n8n-workflow": "*", "eslint": "9.29.0", "prettier": "3.6.2", "typescript": "5.9.2"