Skip to content

Commit 1ec24f7

Browse files
committed
Fix staging R2 defaults, vendor role handling, and HF router migration
1 parent f57ea4b commit 1ec24f7

5 files changed

Lines changed: 1468 additions & 196 deletions

File tree

engine/src/providers.js

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,17 @@ function getGeminiText(json) {
101101
return parts.map((p) => p?.text || '').filter(Boolean).join(' ').trim();
102102
}
103103

104+
function getCohereText(json) {
105+
const direct = String(json?.text || '').trim();
106+
if (direct) return direct;
107+
const messageContent = Array.isArray(json?.message?.content) ? json.message.content : [];
108+
const parts = messageContent
109+
.map((item) => String(item?.text || '').trim())
110+
.filter(Boolean);
111+
if (parts.length) return parts.join('\n').trim();
112+
return '';
113+
}
114+
104115
function extractGeminiInlineImage(json) {
105116
const candidates = Array.isArray(json?.candidates) ? json.candidates : [];
106117
for (const candidate of candidates) {
@@ -144,7 +155,12 @@ function decodeDataUri(value) {
144155
function normalizeBaseUrl(value, fallback) {
145156
const raw = String(value || fallback || '').trim();
146157
if (!raw) return '';
147-
return raw.replace(/\/+$/, '');
158+
const normalized = raw.replace(/\/+$/, '');
159+
// Hugging Face deprecated api-inference endpoint now redirects to router API.
160+
if (/^https?:\/\/api-inference\.huggingface\.co$/i.test(normalized)) {
161+
return 'https://router.huggingface.co/hf-inference';
162+
}
163+
return normalized;
148164
}
149165

150166
function buildHuggingFaceModelUrl(providerConfig, model) {
@@ -249,6 +265,48 @@ async function generateTextWithProvider(providerConfig, prompt, runtimeConfig) {
249265
return text;
250266
}
251267

268+
if (provider === 'groq') {
269+
const apiKey = resolveProviderValue(providerConfig, 'api_key', 'GROQ_API_KEY');
270+
if (!apiKey) throw new Error('Missing GROQ_API_KEY for Groq text provider');
271+
const { json } = await fetchJson('https://api.groq.com/openai/v1/chat/completions', {
272+
method: 'POST',
273+
headers: {
274+
Authorization: `Bearer ${apiKey}`,
275+
'Content-Type': 'application/json'
276+
},
277+
body: JSON.stringify({
278+
model,
279+
messages: [{ role: 'user', content: prompt }],
280+
temperature: hasTemperature ? temperature : 0.3
281+
})
282+
}, timeoutMs, 'Groq text');
283+
const text = String(json?.choices?.[0]?.message?.content || '').trim();
284+
if (!text) throw new Error('Groq text response was empty');
285+
return text;
286+
}
287+
288+
if (provider === 'cohere') {
289+
const apiKey = resolveProviderValue(providerConfig, 'api_key', 'COHERE_API_KEY');
290+
if (!apiKey) throw new Error('Missing COHERE_API_KEY for Cohere text provider');
291+
const baseUrl = normalizeBaseUrl(resolveProviderValue(providerConfig, 'base_url', 'COHERE_BASE_URL'), 'https://api.cohere.com');
292+
const { json } = await fetchJson(`${baseUrl}/v2/chat`, {
293+
method: 'POST',
294+
headers: {
295+
Authorization: `Bearer ${apiKey}`,
296+
'Content-Type': 'application/json'
297+
},
298+
body: JSON.stringify({
299+
model,
300+
messages: [{ role: 'user', content: prompt }],
301+
temperature: hasTemperature ? temperature : 0.3,
302+
max_tokens: 2048
303+
})
304+
}, timeoutMs, 'Cohere text');
305+
const text = getCohereText(json);
306+
if (!text) throw new Error('Cohere text response was empty');
307+
return text;
308+
}
309+
252310
if (provider === 'cloudflare') {
253311
const accountId = resolveProviderValue(providerConfig, 'account_id', 'CLOUDFLARE_ACCOUNT_ID');
254312
const apiToken = resolveProviderValue(providerConfig, 'api_token', 'CLOUDFLARE_API_TOKEN');
@@ -299,6 +357,10 @@ async function generateTextWithProvider(providerConfig, prompt, runtimeConfig) {
299357
return text;
300358
}
301359

360+
if (provider === 'groq') {
361+
throw new Error('Groq image provider is not supported');
362+
}
363+
302364
throw new Error(`Unsupported text provider: ${provider}`);
303365
}
304366

engine/tests/providers-temperature.test.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,4 +75,26 @@ describe('provider text temperature wiring', () => {
7575
expect(body.parameters.max_new_tokens).toBe(512);
7676
expect(body.parameters.temperature).toBe(1.1);
7777
});
78+
79+
it('migrates deprecated HuggingFace base URL to router endpoint', async () => {
80+
process.env.HUGGINGFACE_INFERENCE_API_TOKEN = 'hf-1';
81+
const fetchMock = vi.fn().mockResolvedValue({
82+
ok: true,
83+
text: async () => JSON.stringify({ generated_text: 'ok' })
84+
});
85+
global.fetch = fetchMock;
86+
87+
await generateTextWithProvider(
88+
{
89+
provider: 'huggingface',
90+
model: 'mistralai/Mistral-7B-Instruct-v0.2',
91+
base_url: 'https://api-inference.huggingface.co'
92+
},
93+
'hello',
94+
{ timeout_ms: 1000 }
95+
);
96+
97+
const url = String(fetchMock.mock.calls[0][0] || '');
98+
expect(url).toContain('https://router.huggingface.co/hf-inference/models/');
99+
});
78100
});

telegram/scripts/deploy-render-webhook.js

Lines changed: 150 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,14 @@ async function verifyHuggingFaceKey(apiKey) {
103103
if (!res.ok) throw new Error(`Hugging Face token invalid (${res.status}): ${text.slice(0, 300)}`);
104104
}
105105

106+
async function verifyCohereKey(apiKey) {
107+
const { res, text } = await fetchTextWithTimeout('https://api.cohere.com/v1/models', {
108+
method: 'GET',
109+
headers: { Authorization: `Bearer ${apiKey}` }
110+
}, 30000, 'Cohere auth');
111+
if (!res.ok) throw new Error(`Cohere key invalid (${res.status}): ${text.slice(0, 300)}`);
112+
}
113+
106114
async function verifyFirecrawlKey(apiKey) {
107115
const { res, text } = await fetchTextWithTimeout('https://api.firecrawl.dev/v1/search', {
108116
method: 'POST',
@@ -139,6 +147,64 @@ async function verifyDriftbotKey(apiKey) {
139147
if (!res.ok) throw new Error(`Driftbot key invalid (${res.status}): ${text.slice(0, 300)}`);
140148
}
141149

150+
async function verifyBraveSearchKey(apiKey) {
151+
const { res, text } = await fetchTextWithTimeout('https://api.search.brave.com/res/v1/web/search?q=example.com&count=1', {
152+
method: 'GET',
153+
headers: {
154+
'X-Subscription-Token': apiKey,
155+
Accept: 'application/json'
156+
}
157+
}, 45000, 'Brave auth');
158+
if (!res.ok) throw new Error(`Brave key invalid (${res.status}): ${text.slice(0, 300)}`);
159+
}
160+
161+
async function verifyTavilyKey(apiKey) {
162+
const { res, text } = await fetchTextWithTimeout('https://api.tavily.com/search', {
163+
method: 'POST',
164+
headers: { 'Content-Type': 'application/json' },
165+
body: JSON.stringify({ api_key: apiKey, query: 'example.com', max_results: 1 })
166+
}, 45000, 'Tavily auth');
167+
if (!res.ok) throw new Error(`Tavily key invalid (${res.status}): ${text.slice(0, 300)}`);
168+
}
169+
170+
async function verifyExaKey(apiKey) {
171+
const { res, text } = await fetchTextWithTimeout('https://api.exa.ai/search', {
172+
method: 'POST',
173+
headers: {
174+
'Content-Type': 'application/json',
175+
'x-api-key': apiKey
176+
},
177+
body: JSON.stringify({ query: 'example.com', numResults: 1 })
178+
}, 45000, 'Exa auth');
179+
if (!res.ok) throw new Error(`Exa key invalid (${res.status}): ${text.slice(0, 300)}`);
180+
}
181+
182+
async function verifySerperKey(apiKey) {
183+
const { res, text } = await fetchTextWithTimeout('https://google.serper.dev/search', {
184+
method: 'POST',
185+
headers: {
186+
'Content-Type': 'application/json',
187+
'X-API-KEY': apiKey
188+
},
189+
body: JSON.stringify({ q: 'example.com', num: 1 })
190+
}, 45000, 'Serper auth');
191+
if (!res.ok) throw new Error(`Serper key invalid (${res.status}): ${text.slice(0, 300)}`);
192+
}
193+
194+
async function verifySerpApiKey(apiKey) {
195+
const { res, text } = await fetchTextWithTimeout(`https://serpapi.com/search.json?q=example.com&num=1&api_key=${encodeURIComponent(apiKey)}`, {
196+
method: 'GET'
197+
}, 45000, 'SerpAPI auth');
198+
if (!res.ok) throw new Error(`SerpAPI key invalid (${res.status}): ${text.slice(0, 300)}`);
199+
}
200+
201+
async function verifyGoogleKgKey(apiKey) {
202+
const { res, text } = await fetchTextWithTimeout(`https://kgsearch.googleapis.com/v1/entities:search?query=Tokyo&limit=1&key=${encodeURIComponent(apiKey)}`, {
203+
method: 'GET'
204+
}, 45000, 'Google KG auth');
205+
if (!res.ok) throw new Error(`Google KG key invalid (${res.status}): ${text.slice(0, 300)}`);
206+
}
207+
142208
async function verifyLlamaCloudKey(apiKey) {
143209
if (!String(apiKey || '').trim().startsWith('llx-')) {
144210
throw new Error('LlamaCloud key format invalid (expected llx-)');
@@ -169,6 +235,20 @@ async function verifyAssemblyAiKey(apiKey) {
169235
}
170236
}
171237

238+
async function verifyUnstructuredKey(apiKey) {
239+
const { res, text } = await fetchTextWithTimeout('https://api.unstructuredapp.io/general/v0/general', {
240+
method: 'POST',
241+
headers: {
242+
Accept: 'application/json',
243+
'unstructured-api-key': apiKey
244+
},
245+
body: new FormData()
246+
}, 30000, 'Unstructured auth');
247+
if (res.status === 401 || res.status === 403) {
248+
throw new Error(`Unstructured key invalid (${res.status}): ${text.slice(0, 300)}`);
249+
}
250+
}
251+
172252
async function verifyAllProviderCredentials(providerEnv, options = {}) {
173253
const strictAll = Boolean(options.strictAll);
174254
const checks = [];
@@ -199,6 +279,11 @@ async function verifyAllProviderCredentials(providerEnv, options = {}) {
199279
enabled: Boolean(key('HUGGINGFACE_INFERENCE_API_TOKEN')),
200280
run: () => verifyHuggingFaceKey(key('HUGGINGFACE_INFERENCE_API_TOKEN'))
201281
});
282+
checks.push({
283+
name: 'cohere',
284+
enabled: Boolean(key('COHERE_API_KEY')),
285+
run: () => verifyCohereKey(key('COHERE_API_KEY'))
286+
});
202287
checks.push({
203288
name: 'firecrawl',
204289
enabled: Boolean(key('FIRECRAWL_API_KEY')),
@@ -219,11 +304,47 @@ async function verifyAllProviderCredentials(providerEnv, options = {}) {
219304
enabled: Boolean(key('LLAMA_CLOUD_API_KEY')),
220305
run: () => verifyLlamaCloudKey(key('LLAMA_CLOUD_API_KEY'))
221306
});
307+
checks.push({
308+
name: 'unstructured',
309+
enabled: Boolean(key('UNSTRUCTURED_API_KEY')),
310+
run: () => verifyUnstructuredKey(key('UNSTRUCTURED_API_KEY'))
311+
});
222312
checks.push({
223313
name: 'assemblyai',
224314
enabled: Boolean(key('ASSEMBLYAI_API_KEY')),
225315
run: () => verifyAssemblyAiKey(key('ASSEMBLYAI_API_KEY'))
226316
});
317+
checks.push({
318+
name: 'brave',
319+
enabled: Boolean(key('BRAVE_SEARCH_API_KEY')),
320+
run: () => verifyBraveSearchKey(key('BRAVE_SEARCH_API_KEY'))
321+
});
322+
checks.push({
323+
name: 'tavily',
324+
enabled: Boolean(key('TAVILY_API_KEY')),
325+
run: () => verifyTavilyKey(key('TAVILY_API_KEY'))
326+
});
327+
checks.push({
328+
name: 'exa',
329+
enabled: Boolean(key('EXA_API_KEY')),
330+
optional: true,
331+
run: () => verifyExaKey(key('EXA_API_KEY'))
332+
});
333+
checks.push({
334+
name: 'serper',
335+
enabled: Boolean(key('SERPER_API_KEY')),
336+
run: () => verifySerperKey(key('SERPER_API_KEY'))
337+
});
338+
checks.push({
339+
name: 'serpapi',
340+
enabled: Boolean(key('SERPAPI_API_KEY')),
341+
run: () => verifySerpApiKey(key('SERPAPI_API_KEY'))
342+
});
343+
checks.push({
344+
name: 'googlekg',
345+
enabled: Boolean(key('GOOGLE_KG_API_KEY')),
346+
run: () => verifyGoogleKgKey(key('GOOGLE_KG_API_KEY'))
347+
});
227348

228349
const failures = [];
229350
for (const check of checks) {
@@ -235,7 +356,12 @@ async function verifyAllProviderCredentials(providerEnv, options = {}) {
235356
await check.run();
236357
console.log(`[deploy] provider credential check ok: ${check.name}`);
237358
} catch (error) {
238-
failures.push(`${check.name}: ${String(error?.message || error)}`);
359+
const message = `${check.name}: ${String(error?.message || error)}`;
360+
if (check.optional && !strictAll) {
361+
console.warn(`[deploy] provider credential check warning: ${message}`);
362+
continue;
363+
}
364+
failures.push(message);
239365
}
240366
}
241367
if (failures.length) {
@@ -305,7 +431,7 @@ function pickCloudflareAccountTokenCandidates(cfYaml) {
305431
return out;
306432
}
307433

308-
function resolveR2Config(args, cfYaml, awsYaml) {
434+
function resolveR2Config(args, cfYaml, awsYaml, serviceName = '') {
309435
const cfR2 = (cfYaml && cfYaml.r2) || {};
310436
const s3Clients = (cfR2 && cfR2.s3_clients) || {};
311437
const key2 = s3Clients.keypair_2 || {};
@@ -316,7 +442,11 @@ function resolveR2Config(args, cfYaml, awsYaml) {
316442
process.env.CLOUDFLARE_ACCOUNT_ID,
317443
cfYaml && cfYaml.account_id
318444
);
319-
const bucket = firstNonEmpty(args['r2-bucket'], process.env.R2_BUCKET, cfR2.bucket, 'web2comics-bot-data');
445+
const explicitBucket = firstNonEmpty(args['r2-bucket']);
446+
const isStageService = /stage/i.test(String(serviceName || ''));
447+
const stageBucketDefault = firstNonEmpty(args['r2-bucket-stage'], process.env.R2_BUCKET_STAGE, 'web2comics-bot-data-stage');
448+
const nonStageBucketDefault = firstNonEmpty(process.env.R2_BUCKET, cfR2.bucket, 'web2comics-bot-data');
449+
const bucket = explicitBucket || (isStageService ? stageBucketDefault : nonStageBucketDefault);
320450
const endpointRaw = firstNonEmpty(
321451
args['r2-endpoint'],
322452
process.env.R2_S3_ENDPOINT,
@@ -544,6 +674,7 @@ async function main() {
544674
}
545675
const envOnly = parseBool(preArgs['env-only'] || process.env.BOT_SECRETS_ENV_ONLY);
546676
loadEnvFiles([
677+
path.join(repoRoot, '.env.all'),
547678
path.join(repoRoot, '.env.local'),
548679
path.join(repoRoot, '.env.e2e.local'),
549680
path.join(repoRoot, '.crawler'),
@@ -602,13 +733,22 @@ async function main() {
602733
CLOUDFLARE_ACCOUNT_ID: cloudflareAccountId,
603734
CLOUDFLARE_API_TOKEN: cloudflareAiToken,
604735
HUGGINGFACE_INFERENCE_API_TOKEN: firstNonEmpty(args['huggingface-token'], process.env.HUGGINGFACE_INFERENCE_API_TOKEN),
736+
COHERE_API_KEY: firstNonEmpty(args['cohere-key'], process.env.COHERE_API_KEY),
605737
FIRECRAWL_API_KEY: firstNonEmpty(args['firecrawl-key'], process.env.FIRECRAWL_API_KEY),
606738
JINA_API_KEY: firstNonEmpty(args['jina-key'], process.env.JINA_API_KEY),
607-
DRIFTBOT_API_KEY: firstNonEmpty(args['driftbot-key'], process.env.DRIFTBOT_API_KEY),
739+
DRIFTBOT_API_KEY: firstNonEmpty(args['driftbot-key'], process.env.DRIFTBOT_API_KEY, process.env.DIFFBOT_API_KEY),
740+
DIFFBOT_API_KEY: firstNonEmpty(args['diffbot-key'], process.env.DIFFBOT_API_KEY),
741+
BRAVE_SEARCH_API_KEY: firstNonEmpty(args['brave-key'], process.env.BRAVE_SEARCH_API_KEY),
742+
TAVILY_API_KEY: firstNonEmpty(args['tavily-key'], process.env.TAVILY_API_KEY),
743+
EXA_API_KEY: firstNonEmpty(args['exa-key'], process.env.EXA_API_KEY),
744+
SERPER_API_KEY: firstNonEmpty(args['serper-key'], process.env.SERPER_API_KEY),
745+
SERPAPI_API_KEY: firstNonEmpty(args['serpapi-key'], process.env.SERPAPI_API_KEY),
746+
GOOGLE_KG_API_KEY: firstNonEmpty(args['googlekg-key'], process.env.GOOGLE_KG_API_KEY),
608747
LLAMA_CLOUD_API_KEY: firstNonEmpty(args['llama-cloud-key'], process.env.LLAMA_CLOUD_API_KEY, process.env.LLAMAPARSE_API_KEY),
748+
UNSTRUCTURED_API_KEY: firstNonEmpty(args['unstructured-key'], process.env.UNSTRUCTURED_API_KEY),
609749
ASSEMBLYAI_API_KEY: firstNonEmpty(args['assemblyai-key'], process.env.ASSEMBLYAI_API_KEY)
610750
};
611-
const resolvedR2 = resolveR2Config(args, cfYaml, awsYaml);
751+
const resolvedR2 = resolveR2Config(args, cfYaml, awsYaml, serviceName);
612752
const r2Env = {
613753
R2_S3_ENDPOINT: resolvedR2.endpoint,
614754
R2_BUCKET: resolvedR2.bucket,
@@ -653,11 +793,14 @@ async function main() {
653793
throw new Error('Missing Telegram bot token. Provide TELEGRAM_BOT_TOKEN (GitHub Secret) or --telegram-token');
654794
}
655795
const existingWebhookSecret = await getExistingWebhookSecret(telegramToken);
796+
const defaultWebhookSecret = /stage/i.test(String(serviceName || ''))
797+
? 'web2comics-render-webhook-secret-stage-v1'
798+
: 'web2comics-render-webhook-secret-v1';
656799
const webhookSecret = firstNonEmpty(
657800
args['webhook-secret'],
658801
process.env.TELEGRAM_WEBHOOK_SECRET,
659802
existingWebhookSecret,
660-
'web2comics-render-webhook-secret-v1'
803+
defaultWebhookSecret
661804
);
662805
const keyCheck = validateProviderEnv(providerEnv, requireAllKeys);
663806
if (!keyCheck.ok) {
@@ -856,6 +999,7 @@ async function main() {
856999
publicUrl,
8571000
webhookUrl,
8581001
webhookSecret,
1002+
r2Bucket: r2Env.R2_BUCKET,
8591003
telegramTestChatId,
8601004
notifyChatId
8611005
}, null, 2), 'utf8');

0 commit comments

Comments
 (0)