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 if (!api_key) {
42 await Actor.fail('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"]}');
43 return;
44 }
45 if (!model) {
46 await Actor.fail('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');
47 return;
48 }
49 if (!Array.isArray(messages) || messages.length === 0) {
50 await Actor.fail('Input required: provide "messages" as a non-empty array of strings. Example: {"messages": ["Summarize this text"]}');
51 return;
52 }
53
54 const apiUrl = 'https://openrouter.ai/api/v1/chat/completions';
55 const headers = {
56 Authorization: `Bearer ${api_key}`,
57 'Content-Type': 'application/json',
58 'HTTP-Referer': 'https://apify.com/superlativetech/open-router',
59 'X-Title': 'Apify x OpenRouter',
60 };
61
62
63 const payloadOpts = {};
64 if (response_format) payloadOpts.response_format = { type: response_format };
65 if (stop?.length) payloadOpts.stop = stop;
66 if (max_tokens !== undefined) payloadOpts.max_tokens = max_tokens;
67 if (temperature !== undefined) payloadOpts.temperature = parseFloat(temperature);
68 if (top_p !== undefined) payloadOpts.top_p = parseFloat(top_p);
69 if (top_k !== undefined) payloadOpts.top_k = parseInt(top_k, 10);
70 if (frequency_penalty !== undefined) payloadOpts.frequency_penalty = parseFloat(frequency_penalty);
71 if (presence_penalty !== undefined) payloadOpts.presence_penalty = parseFloat(presence_penalty);
72 if (repetition_penalty !== undefined) payloadOpts.repetition_penalty = parseFloat(repetition_penalty);
73 if (seed !== undefined) payloadOpts.seed = seed;
74
75
76
77
78
79 async function processMessage(text, index) {
80 const payload = { messages: [{ role: 'user', content: text }], model, ...payloadOpts };
81
82 const makeRequest = () => axios.post(apiUrl, payload, { headers, timeout: 60_000 }).then(res => res.data);
83
84 let data;
85 try {
86 data = await pRetry(makeRequest, {
87 retries: 2,
88 shouldRetry: (err) => {
89 const status = err?.response?.status;
90 if (status && status >= 400 && status < 500 && status !== 429) {
91 return false;
92 }
93 return true;
94 },
95 onFailedAttempt: err => {
96 const status = err?.response?.status;
97 log.warning(`Attempt #${err.attemptNumber} failed for message ${index}. ${err.retriesLeft} retries left.`, { status });
98 },
99 });
100 } catch (error) {
101 const status = error?.response?.status;
102 if (status === 401) {
103 throw new Error(`Invalid API key (HTTP 401). Please check your OpenRouter API key.`);
104 } else if (status === 402) {
105 throw new Error(`Insufficient credits (HTTP 402). Please add credits to your OpenRouter account.`);
106 } else if (status === 403) {
107 throw new Error(`Access denied (HTTP 403). Your API key may not have access to model "${model}".`);
108 }
109 throw error;
110 }
111
112 const { id, created, model: resModel, choices, usage } = data;
113 return {
114 id,
115 created: created ? new Date(created * 1000).toISOString() : null,
116 model: resModel,
117 content: choices?.[0]?.message?.content,
118 finish_reason: choices?.[0]?.finish_reason,
119 prompt_tokens: usage?.prompt_tokens,
120 completion_tokens: usage?.completion_tokens,
121 total_tokens: usage?.total_tokens,
122 };
123 }
124
125
126 const concurrency = 10;
127 let chargeLimitReached = false;
128
129 for (let i = 0; i < messages.length; i += concurrency) {
130 const batch = messages.slice(i, i + concurrency);
131 const batchResults = await Promise.allSettled(
132 batch.map((text, idx) => processMessage(text, i + idx)),
133 );
134
135
136 for (const r of batchResults) {
137 if (r.status === 'rejected') {
138 const msg = r.reason?.message || '';
139 if (msg.includes('HTTP 401') || msg.includes('HTTP 402') || msg.includes('HTTP 403')) {
140 throw r.reason;
141 }
142 }
143 }
144
145 const results = [];
146 for (const r of batchResults) {
147 if (r.status === 'fulfilled') {
148 results.push(r.value);
149 if (!chargeLimitReached) {
150 try {
151 const chargeResult = await Actor.charge({ eventName: 'openrouter-message', count: 1 });
152 if (chargeResult?.eventChargeLimitReached) {
153 chargeLimitReached = true;
154 }
155 } catch (chargeErr) {
156 log.warning(`PPE charge failed: ${chargeErr.message}`);
157 }
158 }
159 } else {
160 log.error(`Message failed: ${r.reason?.message}`);
161 }
162 }
163
164 if (results.length > 0) {
165 await Actor.pushData(results);
166 }
167
168 log.info(`Processed ${Math.min(i + concurrency, messages.length)}/${messages.length} messages`);
169
170 if (chargeLimitReached) {
171 log.info(`User spending limit reached after ${Math.min(i + concurrency, messages.length)} messages. Stopping.`);
172 break;
173 }
174 }
175
176 log.info('Actor completed successfully.');
177});