mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
Merge branch 'release/v3.8.0' into upstream/translator-claude-thinking-placeholder
This commit is contained in:
@@ -200,7 +200,7 @@ Perform a **global impact assessment** to verify whether the PR changes are comp
|
||||
```
|
||||
|
||||
- **Fallback (ONLY for external forks without maintainer edit access):**
|
||||
Using `cherry-pick` instead of fixing the contributor's PR directly is a **LAST RESORT**. You MUST ALWAYS attempt to `git push` your fixes to their branch first.
|
||||
Using `cherry-pick` instead of fixing the contributor's PR directly is a **LAST RESORT**. You MUST ALWAYS attempt to `git push` your fixes to their branch first.
|
||||
**ONLY if `git push` explicitly fails with a permission/access error** (meaning the contributor unchecked "Allow edits from maintainers" or it's a locked fork), you may use `git cherry-pick` to bring their changes into the release branch and fix the issues locally.
|
||||
Even then, ensure you preserve the contributor's authorship (`git commit --author="Contributor Name <email>"` if creating new commits).
|
||||
Once you have integrated their work into the release branch, **DO NOT close their PR**. Leave it open so the contributor retains credit. Under NO CIRCUMSTANCES should you use `gh pr close`.
|
||||
|
||||
7
.github/workflows/claude-code-review.yml
vendored
7
.github/workflows/claude-code-review.yml
vendored
@@ -36,9 +36,8 @@ jobs:
|
||||
uses: anthropics/claude-code-action@v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
plugin_marketplaces: 'https://github.com/anthropics/claude-code.git'
|
||||
plugins: 'code-review@claude-code-plugins'
|
||||
prompt: '/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}'
|
||||
plugin_marketplaces: "https://github.com/anthropics/claude-code.git"
|
||||
plugins: "code-review@claude-code-plugins"
|
||||
prompt: "/code-review:code-review ${{ github.repository }}/pull/${{ github.event.pull_request.number }}"
|
||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||
# or https://code.claude.com/docs/en/cli-reference for available options
|
||||
|
||||
|
||||
1
.github/workflows/claude.yml
vendored
1
.github/workflows/claude.yml
vendored
@@ -47,4 +47,3 @@ jobs:
|
||||
# See https://github.com/anthropics/claude-code-action/blob/main/docs/usage.md
|
||||
# or https://code.claude.com/docs/en/cli-reference for available options
|
||||
# claude_args: '--allowed-tools Bash(gh pr *)'
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ Users managing multiple provider accounts (e.g., 20+ API keys or OAuth connectio
|
||||
5. Repeating for every account
|
||||
|
||||
This is:
|
||||
|
||||
- **Time-consuming**: O(n) confirm dialogs and API calls for n accounts
|
||||
- **Error-prone**: Easy to accidentally click the wrong account
|
||||
- **Tedious**: No way to quickly clean up stale or duplicate accounts
|
||||
@@ -35,15 +36,15 @@ Add a checkbox-based selection UI to the provider connections list:
|
||||
|
||||
### Files to Modify
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `src/app/(dashboard)/dashboard/providers/[id]/page.tsx` | Add batch delete state, select-all + per-row checkboxes, batch delete handler |
|
||||
| `src/app/api/providers/route.ts` | Add `DELETE /api/providers` with `POST` body `ids: string[]` for batch delete |
|
||||
| `src/lib/db/providers.ts` | Add `deleteProviderConnections(ids: string[])` batch DB function |
|
||||
| `src/i18n/messages/en.json` | Add i18n keys: `batchDeleteSelected`, `batchDeleteConfirm`, `batchDeleteSuccess` |
|
||||
| `src/i18n/messages/*.json` | Add i18n keys to all locale files |
|
||||
| `tests/unit/db-providers-crud.test.ts` | Add unit tests for batch delete DB function |
|
||||
| `tests/integration/api-routes-critical.test.ts` | Add integration test for batch delete API endpoint |
|
||||
| File | Change |
|
||||
| ------------------------------------------------------- | -------------------------------------------------------------------------------- |
|
||||
| `src/app/(dashboard)/dashboard/providers/[id]/page.tsx` | Add batch delete state, select-all + per-row checkboxes, batch delete handler |
|
||||
| `src/app/api/providers/route.ts` | Add `DELETE /api/providers` with `POST` body `ids: string[]` for batch delete |
|
||||
| `src/lib/db/providers.ts` | Add `deleteProviderConnections(ids: string[])` batch DB function |
|
||||
| `src/i18n/messages/en.json` | Add i18n keys: `batchDeleteSelected`, `batchDeleteConfirm`, `batchDeleteSuccess` |
|
||||
| `src/i18n/messages/*.json` | Add i18n keys to all locale files |
|
||||
| `tests/unit/db-providers-crud.test.ts` | Add unit tests for batch delete DB function |
|
||||
| `tests/integration/api-routes-critical.test.ts` | Add integration test for batch delete API endpoint |
|
||||
|
||||
### 1. DB Layer (`src/lib/db/providers.ts`)
|
||||
|
||||
@@ -60,9 +61,9 @@ export async function deleteProviderConnections(ids: string[]): Promise<number>
|
||||
|
||||
// Batch delete connections
|
||||
const placeholders = ids.map(() => "?").join(",");
|
||||
const result = db.prepare(
|
||||
`DELETE FROM provider_connections WHERE id IN (${placeholders})`
|
||||
).run(...ids);
|
||||
const result = db
|
||||
.prepare(`DELETE FROM provider_connections WHERE id IN (${placeholders})`)
|
||||
.run(...ids);
|
||||
|
||||
backupDbFile("pre-write");
|
||||
invalidateDbCache("connections");
|
||||
@@ -210,14 +211,14 @@ interface ConnectionRowProps {
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedIds.size === connections.length && connections.length > 0}
|
||||
ref={(el) => { if (el) el.indeterminate = selectedIds.size > 0 && selectedIds.size < connections.length; }}
|
||||
ref={(el) => {
|
||||
if (el) el.indeterminate = selectedIds.size > 0 && selectedIds.size < connections.length;
|
||||
}}
|
||||
onChange={handleToggleSelectAll}
|
||||
className="w-4 h-4 rounded border-border text-primary focus:ring-primary/30"
|
||||
/>
|
||||
<span className="text-sm font-medium text-text-muted">
|
||||
{selectedIds.size > 0
|
||||
? `${selectedIds.size} selected`
|
||||
: `${connections.length} accounts`}
|
||||
{selectedIds.size > 0 ? `${selectedIds.size} selected` : `${connections.length} accounts`}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
@@ -252,8 +253,10 @@ interface ConnectionRowProps {
|
||||
```typescript
|
||||
test("deleteProviderConnections deletes multiple connections", async () => {
|
||||
const ids = [
|
||||
(await createProviderConnection({ provider: "openai", name: "test-1", authType: "apikey" })).id!,
|
||||
(await createProviderConnection({ provider: "openai", name: "test-2", authType: "apikey" })).id!,
|
||||
(await createProviderConnection({ provider: "openai", name: "test-1", authType: "apikey" }))
|
||||
.id!,
|
||||
(await createProviderConnection({ provider: "openai", name: "test-2", authType: "apikey" }))
|
||||
.id!,
|
||||
];
|
||||
|
||||
const deleted = await deleteProviderConnections(ids);
|
||||
@@ -278,7 +281,7 @@ test("DELETE /api/providers — batch delete", async () => {
|
||||
const ids = [conn1.id, conn2.id];
|
||||
const res = await fetch("http://localhost:20128/api/providers", {
|
||||
method: "DELETE",
|
||||
headers: { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` },
|
||||
headers: { "Content-Type": "application/json", Authorization: `Bearer ${apiKey}` },
|
||||
body: JSON.stringify({ ids }),
|
||||
});
|
||||
|
||||
@@ -307,16 +310,17 @@ test("DELETE /api/providers — batch delete", async () => {
|
||||
|
||||
### Risks & Mitigations
|
||||
|
||||
| Risk | Mitigation |
|
||||
|------|------------|
|
||||
| User accidentally deletes wrong accounts | Require confirmation dialog with count |
|
||||
| Too many connections selected | Cap at 100 per batch; show error if exceeded |
|
||||
| Partial failure on batch delete | DB runs in transaction; all-or-nothing semantics |
|
||||
| Performance with large selections | Batch SQL with `IN (...)` clause is efficient up to 100 |
|
||||
| Risk | Mitigation |
|
||||
| ---------------------------------------- | ------------------------------------------------------- |
|
||||
| User accidentally deletes wrong accounts | Require confirmation dialog with count |
|
||||
| Too many connections selected | Cap at 100 per batch; show error if exceeded |
|
||||
| Partial failure on batch delete | DB runs in transaction; all-or-nothing semantics |
|
||||
| Performance with large selections | Batch SQL with `IN (...)` clause is efficient up to 100 |
|
||||
|
||||
### Coverage
|
||||
|
||||
Per repository rules, this change affects production code in `src/` → automated tests required:
|
||||
|
||||
- Unit test for `deleteProviderConnections()` in `tests/unit/db-providers-crud.test.ts`
|
||||
- Integration test for `DELETE /api/providers` batch endpoint in `tests/integration/api-routes-critical.test.ts`
|
||||
- Run `npm run test:coverage` — all 4 metrics must meet 60% minimum
|
||||
|
||||
@@ -52,8 +52,8 @@ AGENTS.md
|
||||
bun.lock
|
||||
|
||||
# Build artifacts (pre-built goes inside app/)
|
||||
.next/
|
||||
node_modules/
|
||||
/.next/
|
||||
/node_modules/
|
||||
|
||||
# Ignore large binary files and other build directories
|
||||
*.tgz
|
||||
|
||||
@@ -41,8 +41,10 @@ export class PollinationsExecutor extends BaseExecutor {
|
||||
return headers;
|
||||
}
|
||||
|
||||
transformRequest(model: string, body: any, _stream: boolean, _credentials: any): any {
|
||||
transformRequest(model: string, body: any, stream: boolean, _credentials: any): any {
|
||||
if (typeof body === "object" && body !== null) {
|
||||
body.model = model;
|
||||
body.stream = stream;
|
||||
body.jsonMode = true;
|
||||
}
|
||||
return body;
|
||||
|
||||
@@ -209,23 +209,24 @@ export interface TlsFetchOptions {
|
||||
proxyUrl?: string;
|
||||
}
|
||||
|
||||
import { resolveProxyForRequest } from "../utils/proxyFetch.ts";
|
||||
|
||||
/**
|
||||
* Resolve the proxy URL for a tls-client request. Per-call value wins;
|
||||
* otherwise we fall back to env. Returns undefined when no proxy is
|
||||
* configured (caller passes `undefined` through to tls-client-node, which
|
||||
* treats it as "no proxy").
|
||||
* otherwise we use the standard proxy fetch resolution which reads from
|
||||
* the dashboard AsyncLocalStorage context or falls back to env vars.
|
||||
*/
|
||||
function resolveProxyUrl(perCall: string | undefined): string | undefined {
|
||||
if (perCall && perCall.length > 0) return perCall;
|
||||
const fromEnv =
|
||||
process.env.OMNIROUTE_TLS_PROXY_URL ||
|
||||
process.env.HTTPS_PROXY ||
|
||||
process.env.https_proxy ||
|
||||
process.env.HTTP_PROXY ||
|
||||
process.env.http_proxy ||
|
||||
process.env.ALL_PROXY ||
|
||||
process.env.all_proxy;
|
||||
return fromEnv && fromEnv.length > 0 ? fromEnv : undefined;
|
||||
try {
|
||||
const proxyInfo = resolveProxyForRequest("https://chatgpt.com");
|
||||
if (proxyInfo && proxyInfo.proxyUrl) {
|
||||
return proxyInfo.proxyUrl;
|
||||
}
|
||||
} catch {
|
||||
// Ignore resolution errors
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export interface TlsFetchResult {
|
||||
|
||||
@@ -133,7 +133,7 @@ function resolveEnvProxyUrl(targetUrl) {
|
||||
return normalizeProxyUrl(proxyUrl, "environment proxy");
|
||||
}
|
||||
|
||||
function resolveProxyForRequest(targetUrl) {
|
||||
export function resolveProxyForRequest(targetUrl) {
|
||||
let target;
|
||||
try {
|
||||
target = new URL(targetUrl);
|
||||
|
||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -25,6 +25,7 @@
|
||||
"express": "^5.2.1",
|
||||
"fetch-socks": "^1.3.3",
|
||||
"fuse.js": "^7.3.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"http-proxy-middleware": "^3.0.5",
|
||||
"https-proxy-agent": "^9.0.0",
|
||||
"ioredis": "^5.10.1",
|
||||
@@ -83,7 +84,6 @@
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-config-next": "16.2.6",
|
||||
"glob": "^13.0.6",
|
||||
"gray-matter": "^4.0.3",
|
||||
"husky": "^9.1.7",
|
||||
"jsdom": "^29.1.1",
|
||||
"lint-staged": "^16.4.0",
|
||||
@@ -8258,7 +8258,6 @@
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz",
|
||||
"integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"bin": {
|
||||
"esparse": "bin/esparse.js",
|
||||
@@ -8460,7 +8459,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz",
|
||||
"integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"is-extendable": "^0.1.0"
|
||||
@@ -9101,7 +9099,6 @@
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz",
|
||||
"integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"js-yaml": "^3.13.1",
|
||||
@@ -9117,7 +9114,6 @@
|
||||
"version": "1.0.10",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
|
||||
"integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"sprintf-js": "~1.0.2"
|
||||
@@ -9127,7 +9123,6 @@
|
||||
"version": "3.14.2",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
|
||||
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"argparse": "^1.0.7",
|
||||
@@ -9859,7 +9854,6 @@
|
||||
"version": "0.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz",
|
||||
"integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -10607,7 +10601,6 @@
|
||||
"version": "6.0.3",
|
||||
"resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz",
|
||||
"integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -13914,7 +13907,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz",
|
||||
"integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"extend-shallow": "^2.0.1",
|
||||
@@ -14386,7 +14378,6 @@
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
|
||||
"integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/stable-hash": {
|
||||
@@ -14672,7 +14663,6 @@
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz",
|
||||
"integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Card, Button, Input, Modal, CardSkeleton, SegmentedControl } from "@/sh
|
||||
import { useCopyToClipboard } from "@/shared/hooks/useCopyToClipboard";
|
||||
import { useDisplayBaseUrl } from "@/shared/hooks";
|
||||
import { AI_PROVIDERS, getProviderByAlias } from "@/shared/constants/providers";
|
||||
import { getProviderDisplayName } from "@/lib/display/names";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
const BUILD_TIME_CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL || null;
|
||||
@@ -2482,7 +2483,7 @@ function EndpointSection({
|
||||
|
||||
const resolveProvider = (id) => AI_PROVIDERS[id] || getProviderByAlias(id);
|
||||
const providerColor = (id) => resolveProvider(id)?.color || "#888";
|
||||
const providerName = (id) => resolveProvider(id)?.name || id;
|
||||
const providerName = (id) => getProviderDisplayName(id, resolveProvider(id));
|
||||
const copyId = `endpoint_${path}`;
|
||||
|
||||
return (
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
import { useState, useEffect, useCallback } from "react";
|
||||
import { Card } from "@/shared/components";
|
||||
import { AI_PROVIDERS } from "@/shared/constants/providers";
|
||||
import { getProviderDisplayName } from "@/lib/display/names";
|
||||
import { useTranslations } from "next-intl";
|
||||
import TelemetryCard from "./TelemetryCard";
|
||||
|
||||
@@ -762,7 +763,7 @@ export default function HealthPage() {
|
||||
{unhealthy.map(([provider, cb]: [string, any]) => {
|
||||
const style = CB_STYLES[cb.state] || CB_STYLES.OPEN;
|
||||
const providerInfo = AI_PROVIDERS[provider];
|
||||
const displayName = providerInfo?.name || provider;
|
||||
const displayName = getProviderDisplayName(provider, providerInfo);
|
||||
return (
|
||||
<div
|
||||
key={provider}
|
||||
@@ -820,7 +821,7 @@ export default function HealthPage() {
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-2">
|
||||
{healthy.map(([provider]) => {
|
||||
const providerInfo = AI_PROVIDERS[provider];
|
||||
const displayName = providerInfo?.name || provider;
|
||||
const displayName = getProviderDisplayName(provider, providerInfo);
|
||||
return (
|
||||
<div
|
||||
key={provider}
|
||||
@@ -873,7 +874,7 @@ export default function HealthPage() {
|
||||
if (customName.length > 12) displayName += ` (${customName.slice(0, 8)}…)`;
|
||||
else if (customName) displayName += ` (${customName})`;
|
||||
} else {
|
||||
displayName = providerInfo?.name || providerId;
|
||||
displayName = getProviderDisplayName(providerId, providerInfo);
|
||||
}
|
||||
|
||||
return { providerId, displayName, providerInfo, connectionId, model };
|
||||
|
||||
@@ -1630,9 +1630,7 @@ export default function ProviderDetailPage() {
|
||||
: connection
|
||||
)
|
||||
);
|
||||
notify.success(
|
||||
enabled ? "Claude extra-usage blocking enabled" : "Claude extra-usage blocking disabled"
|
||||
);
|
||||
notify.success(enabled ? "Claude extra-usage blocked" : "Claude extra-usage allowed");
|
||||
} catch (error) {
|
||||
console.error("Error toggling Claude extra-usage policy:", error);
|
||||
notify.error("Failed to update Claude extra-usage policy");
|
||||
@@ -5377,7 +5375,7 @@ function ConnectionRow({
|
||||
<button
|
||||
onClick={() => onToggleClaudeExtraUsage?.(!claudeBlockExtraUsageEnabled)}
|
||||
className={`inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-xs font-medium transition-all cursor-pointer ${
|
||||
claudeBlockExtraUsageEnabled
|
||||
!claudeBlockExtraUsageEnabled
|
||||
? "bg-amber-500/15 text-amber-500 hover:bg-amber-500/25"
|
||||
: "bg-black/[0.03] dark:bg-white/[0.03] text-text-muted/50 hover:text-text-muted hover:bg-black/[0.06] dark:hover:bg-white/[0.06]"
|
||||
}`}
|
||||
@@ -5385,7 +5383,7 @@ function ConnectionRow({
|
||||
>
|
||||
<span className="material-symbols-outlined text-[13px]">payments</span>
|
||||
{t("claudeExtraUsageShort")}{" "}
|
||||
{claudeBlockExtraUsageEnabled ? t("toggleOnShort") : t("toggleOffShort")}
|
||||
{!claudeBlockExtraUsageEnabled ? t("toggleOnShort") : t("toggleOffShort")}
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
isGitLabDirectAccessDisabled,
|
||||
resolveGitLabOAuthBaseUrl,
|
||||
} from "@/lib/oauth/gitlab";
|
||||
import { providerAllowsOptionalApiKey } from "@/shared/constants/providers";
|
||||
|
||||
// OAuth provider test endpoints
|
||||
const OAUTH_TEST_CONFIG = {
|
||||
@@ -522,7 +523,8 @@ async function testOAuthConnection(connection: any) {
|
||||
* Test API key connection
|
||||
*/
|
||||
async function testApiKeyConnection(connection: any) {
|
||||
if (!connection.apiKey) {
|
||||
const requiresApiKey = !providerAllowsOptionalApiKey(connection.provider);
|
||||
if (requiresApiKey && !connection.apiKey) {
|
||||
const error = "Missing API key";
|
||||
return {
|
||||
valid: false,
|
||||
|
||||
@@ -4658,4 +4658,4 @@
|
||||
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
|
||||
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4836,4 +4836,4 @@
|
||||
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
|
||||
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4690,4 +4690,4 @@
|
||||
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
|
||||
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4658,4 +4658,4 @@
|
||||
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
|
||||
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4682,4 +4682,4 @@
|
||||
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
|
||||
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4658,4 +4658,4 @@
|
||||
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
|
||||
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4658,4 +4658,4 @@
|
||||
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
|
||||
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4951,4 +4951,4 @@
|
||||
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
|
||||
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4951,4 +4951,4 @@
|
||||
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
|
||||
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4951,4 +4951,4 @@
|
||||
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
|
||||
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4658,4 +4658,4 @@
|
||||
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
|
||||
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4658,4 +4658,4 @@
|
||||
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
|
||||
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4658,4 +4658,4 @@
|
||||
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
|
||||
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4951,4 +4951,4 @@
|
||||
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
|
||||
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4658,4 +4658,4 @@
|
||||
"agentFeaturesContextLengthErrorInteger": "Context length must be a valid integer",
|
||||
"agentFeaturesContextLengthErrorRange": "Context length must be between 1000 and 2000000"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
isAnthropicCompatibleProvider,
|
||||
isOpenAICompatibleProvider,
|
||||
isSelfHostedChatProvider,
|
||||
providerAllowsOptionalApiKey,
|
||||
} from "@/shared/constants/providers";
|
||||
import {
|
||||
SAFE_OUTBOUND_FETCH_PRESETS,
|
||||
@@ -2967,12 +2968,7 @@ async function validateMuseSparkWebProvider({ apiKey, providerSpecificData = {}
|
||||
}
|
||||
|
||||
export async function validateProviderApiKey({ provider, apiKey, providerSpecificData = {} }: any) {
|
||||
const requiresApiKey =
|
||||
provider !== "searxng-search" &&
|
||||
provider !== "petals" &&
|
||||
!isSelfHostedChatProvider(provider) &&
|
||||
!isOpenAICompatibleProvider(provider) &&
|
||||
!isAnthropicCompatibleProvider(provider);
|
||||
const requiresApiKey = !providerAllowsOptionalApiKey(provider);
|
||||
|
||||
if (!provider || (requiresApiKey && !apiKey)) {
|
||||
return { valid: false, error: "Provider and API key required", unsupported: false };
|
||||
|
||||
@@ -793,8 +793,9 @@ export const APIKEY_PROVIDERS = {
|
||||
color: "#4CAF50",
|
||||
textIcon: "PO",
|
||||
website: "https://pollinations.ai",
|
||||
hasFree: false,
|
||||
freeNote: "API key required. Spore tier: ~0.01 pollen/hour ($0.01/hr).",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"No API key required for free public endpoint. Optional Spore tier: ~0.01 pollen/hour.",
|
||||
},
|
||||
puter: {
|
||||
id: "puter",
|
||||
@@ -1913,6 +1914,16 @@ export function isSelfHostedChatProvider(providerId: unknown): boolean {
|
||||
return typeof providerId === "string" && SELF_HOSTED_CHAT_PROVIDER_IDS.has(providerId);
|
||||
}
|
||||
|
||||
export function providerAllowsOptionalApiKey(providerId: unknown): boolean {
|
||||
return (
|
||||
providerId === "searxng-search" ||
|
||||
providerId === "petals" ||
|
||||
isSelfHostedChatProvider(providerId) ||
|
||||
isOpenAICompatibleProvider(providerId) ||
|
||||
isAnthropicCompatibleProvider(providerId)
|
||||
);
|
||||
}
|
||||
|
||||
// ── System Providers (virtual, not user-connectable) ──────────────────────────
|
||||
export const SYSTEM_PROVIDERS = {
|
||||
auto: {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { providerAllowsOptionalApiKey, SELF_HOSTED_CHAT_PROVIDER_IDS } from "@/shared/constants/providers";
|
||||
|
||||
// ── Import test targets from connection test route ──────────────────────────
|
||||
|
||||
@@ -322,6 +323,46 @@ test("OAuth test config covers all expected providers", () => {
|
||||
}
|
||||
});
|
||||
|
||||
// ── testApiKeyConnection requiresApiKey Check ──────────────────────────────
|
||||
// Uses the centralized providerAllowsOptionalApiKey from providers.ts
|
||||
|
||||
test("testApiKeyConnection: searxng-search with empty API key does NOT require API key", () => {
|
||||
assert.equal(providerAllowsOptionalApiKey("searxng-search"), true);
|
||||
});
|
||||
|
||||
test("testApiKeyConnection: petals with empty API key does NOT require API key", () => {
|
||||
assert.equal(providerAllowsOptionalApiKey("petals"), true);
|
||||
});
|
||||
|
||||
test("testApiKeyConnection: self-hosted chat providers with empty API key do NOT require API key", () => {
|
||||
for (const provider of SELF_HOSTED_CHAT_PROVIDER_IDS) {
|
||||
assert.equal(
|
||||
providerAllowsOptionalApiKey(provider),
|
||||
true,
|
||||
`Expected ${provider} to not require API key`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("testApiKeyConnection: openai-compatible providers with empty API key do NOT require API key", () => {
|
||||
assert.equal(providerAllowsOptionalApiKey("openai-compatible-chat-test"), true);
|
||||
});
|
||||
|
||||
test("testApiKeyConnection: anthropic-compatible providers with empty API key do NOT require API key", () => {
|
||||
assert.equal(providerAllowsOptionalApiKey("anthropic-compatible-chat-test"), true);
|
||||
});
|
||||
|
||||
test("testApiKeyConnection: providers requiring an API key are correctly identified", () => {
|
||||
const providersThatRequireKeys = ["openai", "groq", "gemini", "unknown-provider"];
|
||||
for (const provider of providersThatRequireKeys) {
|
||||
assert.equal(
|
||||
providerAllowsOptionalApiKey(provider),
|
||||
false,
|
||||
`Expected ${provider} to require an API key`
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
test("Refreshable OAuth providers are correctly identified", () => {
|
||||
const refreshable = [
|
||||
"codex",
|
||||
|
||||
Reference in New Issue
Block a user