@windagency/valora-plugin-openrouter 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,33 @@
1
+ # @windagency/valora-plugin-openrouter
2
+
3
+ OpenRouter LLM provider — connects to openrouter.ai for unified access to hundreds of models via an OpenAI-compatible API.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ valora plugin add openrouter
9
+ ```
10
+
11
+ Then set `OPENROUTER_API_KEY` in your environment.
12
+
13
+ ## What it contributes
14
+
15
+ - `api.providers.register('openrouter', ...)` — an LLM provider with a real `validateModel` that calls `/v1/models` and caches the catalogue per session.
16
+
17
+ ## Permissions
18
+
19
+ - `code-exec` — required by the loader.
20
+ - `network` — justified by HTTPS calls to `openrouter.ai` via the OpenAI client.
21
+
22
+ ## Configuration
23
+
24
+ | Env var | Effect |
25
+ | -------------------------- | -------------------------------------------- |
26
+ | `OPENROUTER_API_KEY` | Required. Auth token for the OpenRouter API. |
27
+ | `OPENROUTER_DEFAULT_MODEL` | Default model when none is specified. |
28
+
29
+ The plugin uses a model prefix of `openrouter:`, e.g. `openrouter:anthropic/claude-sonnet-4.5`. `peerDependencies` includes `openai@^4.67.0` — Valora's host already provides this.
30
+
31
+ ## See also
32
+
33
+ - [Plugins user guide](../../documentation/user-guide/plugins.md)
package/dist/index.js ADDED
@@ -0,0 +1,176 @@
1
+ import { createRequire } from 'module'; const require = createRequire(import.meta.url);
2
+
3
+ // src/models.ts
4
+ var OPENROUTER_MODELS = {
5
+ CLAUDE_SONNET_4_5: "anthropic/claude-sonnet-4.5",
6
+ GEMMA_4_31B_FREE: "google/gemma-4-31b-it:free",
7
+ GPT_4O: "openai/gpt-4o",
8
+ LLAMA_3_3_70B: "meta-llama/llama-3.3-70b-instruct",
9
+ MISTRAL_LARGE: "mistralai/mistral-large"
10
+ };
11
+
12
+ // src/openrouter-provider.ts
13
+ import OpenAI from "openai";
14
+ var DEFAULT_BASE_URL = "https://openrouter.ai/api/v1";
15
+ var DEFAULT_MODEL = OPENROUTER_MODELS.GEMMA_4_31B_FREE;
16
+ var DEFAULT_REFERER = "https://github.com/windagency/valora.ai";
17
+ var DEFAULT_TITLE = "Valora";
18
+ var OpenRouterProvider = class {
19
+ name = "openrouter";
20
+ cachedModels = null;
21
+ client = null;
22
+ config;
23
+ constructor(config) {
24
+ this.config = config;
25
+ }
26
+ async complete(options) {
27
+ const model = options.model ?? DEFAULT_MODEL;
28
+ const response = await this.getClient().chat.completions.create({
29
+ max_tokens: options.max_tokens,
30
+ messages: this.mapMessages(options),
31
+ model,
32
+ stop: options.stop,
33
+ stream: false,
34
+ temperature: options.temperature,
35
+ tools: options.tools?.map((tool) => ({
36
+ function: { description: tool.description, name: tool.name, parameters: tool.parameters },
37
+ type: "function"
38
+ })),
39
+ top_p: options.top_p
40
+ });
41
+ const choice = response.choices[0];
42
+ if (!choice) throw new Error("OpenRouter returned no choices");
43
+ return {
44
+ content: choice.message.content ?? "",
45
+ finish_reason: choice.finish_reason,
46
+ model: response.model,
47
+ role: "assistant",
48
+ tool_calls: choice.message.tool_calls?.map((tc) => ({
49
+ arguments: this.parseArgs(tc.function.arguments),
50
+ id: tc.id,
51
+ name: tc.function.name
52
+ })),
53
+ usage: response.usage ? {
54
+ completion_tokens: response.usage.completion_tokens,
55
+ prompt_tokens: response.usage.prompt_tokens,
56
+ total_tokens: response.usage.total_tokens
57
+ } : void 0
58
+ };
59
+ }
60
+ getAlternativeModels(_currentModel) {
61
+ return [
62
+ OPENROUTER_MODELS.GEMMA_4_31B_FREE,
63
+ OPENROUTER_MODELS.CLAUDE_SONNET_4_5,
64
+ OPENROUTER_MODELS.GPT_4O,
65
+ OPENROUTER_MODELS.LLAMA_3_3_70B,
66
+ OPENROUTER_MODELS.MISTRAL_LARGE
67
+ ];
68
+ }
69
+ isConfigured() {
70
+ return this.resolveApiKey() !== void 0;
71
+ }
72
+ async streamComplete(options, onChunk) {
73
+ const model = options.model ?? DEFAULT_MODEL;
74
+ const stream = await this.getClient().chat.completions.create({
75
+ max_tokens: options.max_tokens,
76
+ messages: this.mapMessages(options),
77
+ model,
78
+ stop: options.stop,
79
+ stream: true,
80
+ temperature: options.temperature,
81
+ top_p: options.top_p
82
+ });
83
+ let fullContent = "";
84
+ let finishReason;
85
+ for await (const chunk of stream) {
86
+ const choice = chunk.choices[0];
87
+ if (choice?.delta?.content) {
88
+ fullContent += choice.delta.content;
89
+ onChunk(choice.delta.content);
90
+ }
91
+ if (choice?.finish_reason) finishReason = choice.finish_reason;
92
+ }
93
+ return { content: fullContent, finish_reason: finishReason, role: "assistant" };
94
+ }
95
+ async validateModel(model) {
96
+ if (!this.isConfigured()) return false;
97
+ try {
98
+ const catalogue = await this.loadCatalogue();
99
+ return catalogue.has(model);
100
+ } catch {
101
+ return false;
102
+ }
103
+ }
104
+ configString(key, fallback) {
105
+ const value = this.config[key];
106
+ return typeof value === "string" && value.length > 0 ? value : fallback;
107
+ }
108
+ getClient() {
109
+ const apiKey = this.resolveApiKey();
110
+ if (apiKey === void 0) {
111
+ throw new Error("OpenRouter API key missing \u2014 set OPENROUTER_API_KEY or config.apiKey");
112
+ }
113
+ this.client ??= new OpenAI({
114
+ apiKey,
115
+ baseURL: this.configString("baseUrl", DEFAULT_BASE_URL),
116
+ defaultHeaders: {
117
+ "HTTP-Referer": this.configString("httpReferer", DEFAULT_REFERER),
118
+ "X-Title": this.configString("xTitle", DEFAULT_TITLE)
119
+ },
120
+ maxRetries: 2
121
+ });
122
+ return this.client;
123
+ }
124
+ async loadCatalogue() {
125
+ if (this.cachedModels !== null) return this.cachedModels;
126
+ const response = await this.getClient().models.list();
127
+ const ids = /* @__PURE__ */ new Set();
128
+ for (const entry of response.data) {
129
+ if (typeof entry.id === "string") ids.add(entry.id);
130
+ }
131
+ this.cachedModels = ids;
132
+ return ids;
133
+ }
134
+ mapMessages(options) {
135
+ return options.messages.map((m) => ({ content: m.content, role: m.role }));
136
+ }
137
+ parseArgs(raw) {
138
+ try {
139
+ return JSON.parse(raw);
140
+ } catch {
141
+ return {};
142
+ }
143
+ }
144
+ resolveApiKey() {
145
+ const configKey = this.config["apiKey"];
146
+ if (typeof configKey === "string" && configKey.length > 0) {
147
+ return configKey;
148
+ }
149
+ const envKey = process.env["OPENROUTER_API_KEY"];
150
+ return typeof envKey === "string" && envKey.length > 0 ? envKey : void 0;
151
+ }
152
+ };
153
+
154
+ // src/index.ts
155
+ var descriptor = {
156
+ defaultModel: OPENROUTER_MODELS.GEMMA_4_31B_FREE,
157
+ description: "OpenRouter \u2014 unified gateway to hundreds of models via an OpenAI-compatible API",
158
+ envVars: { apiKey: "OPENROUTER_API_KEY", model: "OPENROUTER_DEFAULT_MODEL" },
159
+ helpText: "Set OPENROUTER_API_KEY. Use any model slug from openrouter.ai/models, e.g. openrouter:anthropic/claude-sonnet-4.5.",
160
+ label: "OpenRouter",
161
+ modelModes: [
162
+ { mode: "default", model: OPENROUTER_MODELS.GEMMA_4_31B_FREE },
163
+ { mode: "default", model: OPENROUTER_MODELS.CLAUDE_SONNET_4_5 },
164
+ { mode: "default", model: OPENROUTER_MODELS.GPT_4O },
165
+ { mode: "default", model: OPENROUTER_MODELS.LLAMA_3_3_70B },
166
+ { mode: "default", model: OPENROUTER_MODELS.MISTRAL_LARGE }
167
+ ],
168
+ modelPrefix: "openrouter:",
169
+ requiresApiKey: true
170
+ };
171
+ function register(api) {
172
+ api.providers.register("openrouter", OpenRouterProvider, descriptor);
173
+ }
174
+ export {
175
+ register
176
+ };
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@windagency/valora-plugin-openrouter",
3
+ "version": "1.0.0",
4
+ "description": "OpenRouter provider plugin for Valora — connects to openrouter.ai for unified access to hundreds of LLM models via an OpenAI-compatible API. Requires OPENROUTER_API_KEY.",
5
+ "keywords": [
6
+ "valora",
7
+ "valora-plugin",
8
+ "ai",
9
+ "llm",
10
+ "openrouter",
11
+ "api-gateway",
12
+ "cloud-llm",
13
+ "provider",
14
+ "open-source",
15
+ "typescript"
16
+ ],
17
+ "author": "Damien TIVELET <damien@wind-agency.com>",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/windagency/valora.ai"
21
+ },
22
+ "license": "MIT",
23
+ "type": "module",
24
+ "engines": {
25
+ "node": ">=22.0.0"
26
+ },
27
+ "volta": {
28
+ "node": "22.21.0",
29
+ "pnpm": "10.19.0"
30
+ },
31
+ "files": [
32
+ "valora-plugin.json",
33
+ "dist"
34
+ ],
35
+ "peerDependencies": {
36
+ "@windagency/valora": ">=0.1.0",
37
+ "openai": "^4.67.0"
38
+ },
39
+ "devDependencies": {
40
+ "esbuild": "^0.28.0",
41
+ "openai": "^4.67.0",
42
+ "@windagency/valora-plugin-api": "1.0.0"
43
+ },
44
+ "scripts": {
45
+ "build": "esbuild src/index.ts --bundle --platform=node --format=esm --outfile=dist/index.js --external:openai --banner:js=\"import { createRequire } from 'module'; const require = createRequire(import.meta.url);\"",
46
+ "clean": "rm -rf ./dist",
47
+ "lint": "eslint --color",
48
+ "lint:fix": "eslint --color --fix",
49
+ "beautify": "prettier --check \"**/*.+(js|jsx|ts|tsx|json|md|yml|yaml)\"",
50
+ "beautify:fix": "prettier --write \"**/*.+(js|jsx|ts|tsx|json|md|yml|yaml)\"",
51
+ "format": "pnpm beautify:fix && pnpm lint:fix",
52
+ "test": "vitest run"
53
+ }
54
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "valora-plugin-openrouter",
3
+ "version": "1.0.0",
4
+ "description": "OpenRouter provider — connects to openrouter.ai for unified access to models via an OpenAI-compatible API. Requires OPENROUTER_API_KEY.",
5
+ "engines": { "valora": ">=0.1.0" },
6
+ "contributes": ["code"],
7
+ "permissions": ["code-exec", "network"],
8
+ "codeEntrypoint": "dist/index.js"
9
+ }