1import { Actor, log } from 'apify';
2import axios from 'axios';
3import pRetry from 'p-retry';
4
5Actor.main(async () => {
6
7 const input = await Actor.getInput();
8 const {
9 api_key,
10 messages,
11 model,
12 response_format,
13 stop,
14 max_tokens,
15 temperature,
16 top_p,
17 top_k,
18 frequency_penalty,
19 presence_penalty,
20 repetition_penalty,
21 seed,
22 } = input;
23
24
25 log.info('Actor input parameters:', {
26 model,
27 response_format,
28 stop,
29 max_tokens,
30 temperature,
31 top_p,
32 top_k,
33 frequency_penalty,
34 presence_penalty,
35 repetition_penalty,
36 seed,
37 messages_count: Array.isArray(messages) ? messages.length : 0,
38 });
39
40
41
42
43
44 async function exitWithInputError(code, message) {
45 log.warning(message);
46 await Actor.pushData({ error: code, message });
47 await Actor.exit(`Skipped: ${message}`);
48 }
49
50
51 if (!api_key) {
52 await exitWithInputError('missing_api_key', 'Input required: provide "api_key" (your OpenRouter API key). Get one at https://openrouter.ai/keys. Example: {"api_key": "sk-or-v1-...", "model": "openai/gpt-4o-mini", "messages": ["Hello"]}');
53 return;
54 }
55 if (!model) {
56 await exitWithInputError('missing_model', 'Input required: provide "model" (an OpenRouter model ID). Example: "openai/gpt-4o-mini", "anthropic/claude-3.5-sonnet", or "openrouter/auto". Browse models at https://openrouter.ai/models');
57 return;
58 }
59 if (!Array.isArray(messages) || messages.length === 0) {
60 await exitWithInputError('missing_messages', 'Input required: provide "messages" as a non-empty array of strings. Example: {"messages": ["Summarize this text"]}');
61 return;
62 }
63
64 const apiUrl = 'https://openrouter.ai/api/v1/chat/completions';
65 const headers = {
66 Authorization: `Bearer ${api_key}`,
67 'Content-Type': 'application/json',
68 'HTTP-Referer': 'https://apify.com/superlativetech/open-router',
69 'X-Title': 'Apify x OpenRouter',
70 };
71
72
73 const payloadOpts = {};
74 if (response_format) payloadOpts.response_format = { type: response_format };
75 if (stop?.length) payloadOpts.stop = stop;
76 if (max_tokens !== undefined) payloadOpts.max_tokens = max_tokens;
77 if (temperature !== undefined) payloadOpts.temperature = parseFloat(temperature);
78 if (top_p !== undefined) payloadOpts.top_p = parseFloat(top_p);
79 if (top_k !== undefined) payloadOpts.top_k = parseInt(top_k, 10);
80 if (frequency_penalty !== undefined) payloadOpts.frequency_penalty = parseFloat(frequency_penalty);
81 if (presence_penalty !== undefined) payloadOpts.presence_penalty = parseFloat(presence_penalty);
82 if (repetition_penalty !== undefined) payloadOpts.repetition_penalty = parseFloat(repetition_penalty);
83 if (seed !== undefined) payloadOpts.seed = seed;
84
85
86
87
88
89 async function processMessage(text, index) {
90 const payload = { messages: [{ role: 'user', content: text }], model, ...payloadOpts };
91
92 const makeRequest = () => axios.post(apiUrl, payload, { headers, timeout: 60_000 }).then(res => res.data);
93
94 let data;
95 try {
96 data = await pRetry(makeRequest, {
97 retries: 2,
98 shouldRetry: (err) => {
99 const status = err?.response?.status;
100 if (status && status >= 400 && status < 500 && status !== 429) {
101 return false;
102 }
103 return true;
104 },
105 onFailedAttempt: err => {
106 const status = err?.response?.status;
107 log.warning(`Attempt #${err.attemptNumber} failed for message ${index}. ${err.retriesLeft} retries left.`, { status });
108 },
109 });
110 } catch (error) {
111 const status = error?.response?.status;
112 if (status === 401) {
113 throw new Error(`Invalid API key (HTTP 401). Please check your OpenRouter API key.`);
114 } else if (status === 402) {
115 throw new Error(`Insufficient credits (HTTP 402). Please add credits to your OpenRouter account.`);
116 } else if (status === 403) {
117 throw new Error(`Access denied (HTTP 403). Your API key may not have access to model "${model}".`);
118 }
119 throw error;
120 }
121
122 const { id, created, model: resModel, choices, usage } = data;
123 return {
124 id,
125 created: created ? new Date(created * 1000).toISOString() : null,
126 model: resModel,
127 content: choices?.[0]?.message?.content,
128 finish_reason: choices?.[0]?.finish_reason,
129 prompt_tokens: usage?.prompt_tokens,
130 completion_tokens: usage?.completion_tokens,
131 total_tokens: usage?.total_tokens,
132 };
133 }
134
135
136 const concurrency = 10;
137
138 for (let i = 0; i < messages.length; i += concurrency) {
139 const batch = messages.slice(i, i + concurrency);
140 const batchResults = await Promise.allSettled(
141 batch.map((text, idx) => processMessage(text, i + idx)),
142 );
143
144
145
146
147 for (const r of batchResults) {
148 if (r.status === 'rejected') {
149 const msg = r.reason?.message || '';
150 const code = msg.includes('HTTP 401') ? 'invalid_api_key'
151 : msg.includes('HTTP 402') ? 'insufficient_credits'
152 : msg.includes('HTTP 403') ? 'access_denied'
153 : null;
154 if (code) {
155 log.warning(msg);
156 await Actor.pushData({ error: code, message: msg });
157 await Actor.exit(`Stopped: ${msg}`);
158 return;
159 }
160 }
161 }
162
163 const results = [];
164 for (const r of batchResults) {
165 if (r.status === 'fulfilled') {
166 results.push(r.value);
167 } else {
168 log.error(`Message failed: ${r.reason?.message}`);
169 }
170 }
171
172 if (results.length > 0) {
173 await Actor.pushData(results);
174 }
175
176 log.info(`Processed ${Math.min(i + concurrency, messages.length)}/${messages.length} messages`);
177
178 }
179
180 log.info('Actor completed successfully.');
181});