mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-20 06:02:14 +03:00
feat(providers): add Replicate as free provider (#2364)
Integrated into release/v3.8.0
This commit is contained in:
@@ -2684,6 +2684,43 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
],
|
||||
},
|
||||
|
||||
replicate: {
|
||||
id: "replicate",
|
||||
alias: "rep",
|
||||
format: "openai",
|
||||
executor: "default",
|
||||
baseUrl: "https://openai-proxy.replicate.com/v1/chat/completions",
|
||||
modelsUrl: "https://openai-proxy.replicate.com/v1/models",
|
||||
authType: "apikey",
|
||||
authHeader: "Authorization",
|
||||
authPrefix: "Bearer",
|
||||
passthroughModels: true,
|
||||
defaultContextLength: 128000,
|
||||
models: [
|
||||
{
|
||||
id: "meta/meta-llama-3.1-405b-instruct",
|
||||
name: "Llama 3.1 405B Instruct (Free)",
|
||||
contextLength: 128000,
|
||||
},
|
||||
{
|
||||
id: "meta/meta-llama-3.1-70b-instruct",
|
||||
name: "Llama 3.1 70B Instruct (Free)",
|
||||
contextLength: 128000,
|
||||
},
|
||||
{
|
||||
id: "mistralai/mixtral-8x7b-instruct-v0.1",
|
||||
name: "Mixtral 8x7B Instruct (Free)",
|
||||
contextLength: 32768,
|
||||
},
|
||||
{
|
||||
id: "deepseek-ai/deepseek-r1",
|
||||
name: "DeepSeek R1 (Free)",
|
||||
contextLength: 65536,
|
||||
supportsReasoning: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
hackclub: {
|
||||
id: "hackclub",
|
||||
alias: "hc",
|
||||
|
||||
@@ -182,9 +182,7 @@ async function resolveFreshClaudeConnection(connectionId: string): Promise<Claud
|
||||
}
|
||||
|
||||
if (connection.authType !== "oauth") {
|
||||
throw new ClaudeAuthFileError(
|
||||
"Only OAuth Claude connections support credentials.json export"
|
||||
);
|
||||
throw new ClaudeAuthFileError("Only OAuth Claude connections support credentials.json export");
|
||||
}
|
||||
|
||||
if (!shouldRefreshClaudeConnection(connection)) {
|
||||
|
||||
@@ -119,18 +119,15 @@ export async function enrichWithLoadCodeAssist(
|
||||
const timer = setTimeout(() => controller.abort(), 8000);
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
"https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist",
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${parsed.accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ metadata: { ideType: "GEMINI_CLI", platform: "linux" } }),
|
||||
signal: controller.signal,
|
||||
}
|
||||
);
|
||||
const response = await fetch("https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${parsed.accessToken}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ metadata: { ideType: "GEMINI_CLI", platform: "linux" } }),
|
||||
signal: controller.signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return { ...parsed, projectId: null };
|
||||
@@ -197,7 +194,7 @@ export async function createConnectionFromAuthFile(
|
||||
...toRecord(existing.providerSpecificData),
|
||||
scope: enriched.scope,
|
||||
tokenType: enriched.tokenType,
|
||||
projectId: enriched.projectId ?? (toRecord(existing.providerSpecificData).projectId),
|
||||
projectId: enriched.projectId ?? toRecord(existing.providerSpecificData).projectId,
|
||||
importedAt: new Date().toISOString(),
|
||||
},
|
||||
});
|
||||
|
||||
@@ -887,6 +887,20 @@ export const APIKEY_PROVIDERS = {
|
||||
passthroughModels: true,
|
||||
authHint: "No auth required. API accepts any non-empty string as key for identification.",
|
||||
},
|
||||
replicate: {
|
||||
id: "replicate",
|
||||
alias: "rep",
|
||||
name: "Replicate",
|
||||
icon: "auto_awesome",
|
||||
color: "#3B82F6",
|
||||
textIcon: "RE",
|
||||
website: "https://replicate.com",
|
||||
hasFree: true,
|
||||
freeNote:
|
||||
"Free community models — Llama 3.1, Mixtral, DeepSeek R1. Passthrough for SDXL, Whisper, MusicGen.",
|
||||
passthroughModels: true,
|
||||
authHint: "Get API token at replicate.com/account/api-tokens",
|
||||
},
|
||||
hackclub: {
|
||||
id: "hackclub",
|
||||
alias: "hc",
|
||||
|
||||
@@ -161,11 +161,19 @@ function parseAndValidateClaudeAuth(raw: unknown) {
|
||||
const refreshToken = toNonEmptyString(oauthBlock.refreshToken);
|
||||
|
||||
if (!accessToken) {
|
||||
throw new ParseError("accessToken is missing or empty in claudeAiOauth", 400, "missing_access_token");
|
||||
throw new ParseError(
|
||||
"accessToken is missing or empty in claudeAiOauth",
|
||||
400,
|
||||
"missing_access_token"
|
||||
);
|
||||
}
|
||||
|
||||
if (!refreshToken) {
|
||||
throw new ParseError("refreshToken is missing or empty in claudeAiOauth", 400, "missing_refresh_token");
|
||||
throw new ParseError(
|
||||
"refreshToken is missing or empty in claudeAiOauth",
|
||||
400,
|
||||
"missing_refresh_token"
|
||||
);
|
||||
}
|
||||
|
||||
let expiresAt: string | null = null;
|
||||
|
||||
@@ -232,7 +232,10 @@ test("shouldRefreshClaudeConnection returns false when expiresAt > now + 10min",
|
||||
|
||||
test("shouldRefreshClaudeConnection returns true when accessToken absent", () => {
|
||||
assert.equal(
|
||||
shouldRefreshClaudeConnection({ accessToken: null, expiresAt: new Date(Date.now() + 3600 * 1000).toISOString() }),
|
||||
shouldRefreshClaudeConnection({
|
||||
accessToken: null,
|
||||
expiresAt: new Date(Date.now() + 3600 * 1000).toISOString(),
|
||||
}),
|
||||
true
|
||||
);
|
||||
});
|
||||
@@ -276,7 +279,12 @@ test("write read-modify-write merges claudeAiOauth while preserving mcpOAuth", (
|
||||
claudeAiOauth: { accessToken: "old-at", refreshToken: "old-rt", expiresAt: 0, scopes: [] },
|
||||
};
|
||||
|
||||
const newOauthBlock = { accessToken: "new-at", refreshToken: "new-rt", expiresAt: 1768000000000, scopes: ["user:inference"] };
|
||||
const newOauthBlock = {
|
||||
accessToken: "new-at",
|
||||
refreshToken: "new-rt",
|
||||
expiresAt: 1768000000000,
|
||||
scopes: ["user:inference"],
|
||||
};
|
||||
const merged = { ...existingDoc, claudeAiOauth: newOauthBlock };
|
||||
|
||||
// mcpOAuth is preserved
|
||||
|
||||
@@ -50,7 +50,9 @@ interface CreateConnectionOptions {
|
||||
overwriteExisting?: boolean;
|
||||
}
|
||||
|
||||
function parseClaudeAuth(raw: unknown): ParsedClaudeAuth | { error: string; code: string; status: number } {
|
||||
function parseClaudeAuth(
|
||||
raw: unknown
|
||||
): ParsedClaudeAuth | { error: string; code: string; status: number } {
|
||||
const doc = toRecord(raw);
|
||||
const oauthBlock = toRecord(doc.claudeAiOauth);
|
||||
|
||||
@@ -58,11 +60,19 @@ function parseClaudeAuth(raw: unknown): ParsedClaudeAuth | { error: string; code
|
||||
const refreshToken = toNonEmptyString(oauthBlock.refreshToken);
|
||||
|
||||
if (!accessToken) {
|
||||
return { error: "accessToken is missing or empty in claudeAiOauth", code: "missing_access_token", status: 400 };
|
||||
return {
|
||||
error: "accessToken is missing or empty in claudeAiOauth",
|
||||
code: "missing_access_token",
|
||||
status: 400,
|
||||
};
|
||||
}
|
||||
|
||||
if (!refreshToken) {
|
||||
return { error: "refreshToken is missing or empty in claudeAiOauth", code: "missing_refresh_token", status: 400 };
|
||||
return {
|
||||
error: "refreshToken is missing or empty in claudeAiOauth",
|
||||
code: "missing_refresh_token",
|
||||
status: 400,
|
||||
};
|
||||
}
|
||||
|
||||
let expiresAt: string | null = null;
|
||||
@@ -97,7 +107,8 @@ function checkCreateConnectionPreconditions(
|
||||
): { error: string; code: string; status: number } | null {
|
||||
if (enriched.accountUUID && existingByAccountUUID && !options.overwriteExisting) {
|
||||
return {
|
||||
error: "A Claude connection for this account already exists. Pass overwriteExisting: true to replace it.",
|
||||
error:
|
||||
"A Claude connection for this account already exists. Pass overwriteExisting: true to replace it.",
|
||||
code: "duplicate_account",
|
||||
status: 409,
|
||||
};
|
||||
@@ -105,7 +116,8 @@ function checkCreateConnectionPreconditions(
|
||||
|
||||
if (!enriched.email && !enriched.accountUUID && !options.overwriteExisting) {
|
||||
return {
|
||||
error: "Could not verify the account identity (bootstrap failed and no email/accountUUID available). Pass overwriteExisting: true to import anyway.",
|
||||
error:
|
||||
"Could not verify the account identity (bootstrap failed and no email/accountUUID available). Pass overwriteExisting: true to import anyway.",
|
||||
code: "identity_unverified",
|
||||
status: 409,
|
||||
};
|
||||
|
||||
@@ -404,10 +404,7 @@ test("mergeGoogleAccountsFile: noop when active already equals newEmail", async
|
||||
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "gemini-test-"));
|
||||
const accountsPath = path.join(tmpDir, "google_accounts.json");
|
||||
try {
|
||||
await fs.writeFile(
|
||||
accountsPath,
|
||||
JSON.stringify({ active: "same@google.com", old: [] }) + "\n"
|
||||
);
|
||||
await fs.writeFile(accountsPath, JSON.stringify({ active: "same@google.com", old: [] }) + "\n");
|
||||
const result = await mergeGoogleAccountsFile(accountsPath, "same@google.com");
|
||||
assert.equal(result.updated, false);
|
||||
assert.equal(result.savedBakPath, null);
|
||||
|
||||
@@ -245,12 +245,15 @@ test("enrichWithLoadCodeAssist: returns projectId on success", async () => {
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), 8000);
|
||||
try {
|
||||
const response = await mockFetch("https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist", {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${p.accessToken}`, "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ metadata: { ideType: "GEMINI_CLI", platform: "linux" } }),
|
||||
signal: controller.signal,
|
||||
});
|
||||
const response = await mockFetch(
|
||||
"https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist",
|
||||
{
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${p.accessToken}`, "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ metadata: { ideType: "GEMINI_CLI", platform: "linux" } }),
|
||||
signal: controller.signal,
|
||||
}
|
||||
);
|
||||
if (!response.ok) return { ...p, projectId: null };
|
||||
const data = (await response.json()) as Record<string, unknown>;
|
||||
const projectId =
|
||||
@@ -268,7 +271,7 @@ test("enrichWithLoadCodeAssist: returns projectId on success", async () => {
|
||||
({
|
||||
ok: true,
|
||||
json: async () => ({ projectId: "my-gcp-project-123" }),
|
||||
} as Response);
|
||||
}) as Response;
|
||||
|
||||
const enriched = await enrichWithMockFetch(parsed, mockFetch as typeof fetch);
|
||||
assert.equal(enriched.projectId, "my-gcp-project-123");
|
||||
@@ -293,12 +296,15 @@ test("enrichWithLoadCodeAssist: returns projectId null on 401 (best-effort)", as
|
||||
const controller = new AbortController();
|
||||
const timer = setTimeout(() => controller.abort(), 8000);
|
||||
try {
|
||||
const response = await mockFetch("https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist", {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${p.accessToken}`, "Content-Type": "application/json" },
|
||||
body: "{}",
|
||||
signal: controller.signal,
|
||||
});
|
||||
const response = await mockFetch(
|
||||
"https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist",
|
||||
{
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${p.accessToken}`, "Content-Type": "application/json" },
|
||||
body: "{}",
|
||||
signal: controller.signal,
|
||||
}
|
||||
);
|
||||
if (!response.ok) return { ...p, projectId: null };
|
||||
const data = (await response.json()) as Record<string, unknown>;
|
||||
const projectId = typeof data.projectId === "string" ? data.projectId || null : null;
|
||||
@@ -310,7 +316,7 @@ test("enrichWithLoadCodeAssist: returns projectId null on 401 (best-effort)", as
|
||||
}
|
||||
}
|
||||
|
||||
const mockFetch = async () => ({ ok: false, status: 401 } as Response);
|
||||
const mockFetch = async () => ({ ok: false, status: 401 }) as Response;
|
||||
|
||||
const enriched = await enrichWithMockFetch(parsed, mockFetch as typeof fetch);
|
||||
assert.equal(enriched.projectId, null);
|
||||
@@ -332,12 +338,15 @@ test("enrichWithLoadCodeAssist: returns projectId null on network error (best-ef
|
||||
mockFetch: typeof fetch
|
||||
): Promise<{ projectId: string | null } & ParsedGeminiAuth> {
|
||||
try {
|
||||
const response = await mockFetch("https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist", {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${p.accessToken}`, "Content-Type": "application/json" },
|
||||
body: "{}",
|
||||
signal: new AbortController().signal,
|
||||
});
|
||||
const response = await mockFetch(
|
||||
"https://cloudcode-pa.googleapis.com/v1internal:loadCodeAssist",
|
||||
{
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${p.accessToken}`, "Content-Type": "application/json" },
|
||||
body: "{}",
|
||||
signal: new AbortController().signal,
|
||||
}
|
||||
);
|
||||
if (!response.ok) return { ...p, projectId: null };
|
||||
return { ...p, projectId: null };
|
||||
} catch {
|
||||
@@ -389,10 +398,7 @@ test("createConnectionFromAuthFile: allows update when exists + overwrite=true",
|
||||
});
|
||||
|
||||
test("createConnectionFromAuthFile: throws 409 identity_unverified when no email + overwrite=false", () => {
|
||||
function checkIdentity(
|
||||
resolvedEmail: string | null,
|
||||
overwriteExisting: boolean
|
||||
): string | null {
|
||||
function checkIdentity(resolvedEmail: string | null, overwriteExisting: boolean): string | null {
|
||||
if (!resolvedEmail && !overwriteExisting) return "identity_unverified";
|
||||
return null;
|
||||
}
|
||||
@@ -401,10 +407,7 @@ test("createConnectionFromAuthFile: throws 409 identity_unverified when no email
|
||||
});
|
||||
|
||||
test("createConnectionFromAuthFile: allows create without email when overwrite=true", () => {
|
||||
function checkIdentity(
|
||||
resolvedEmail: string | null,
|
||||
overwriteExisting: boolean
|
||||
): string | null {
|
||||
function checkIdentity(resolvedEmail: string | null, overwriteExisting: boolean): string | null {
|
||||
if (!resolvedEmail && !overwriteExisting) return "identity_unverified";
|
||||
return null;
|
||||
}
|
||||
@@ -416,7 +419,10 @@ test("createConnectionFromAuthFile: email from options.email takes precedence ov
|
||||
const resolveEmail = (optionsEmail: string | undefined, enrichedEmail: string | null) =>
|
||||
optionsEmail || enrichedEmail;
|
||||
|
||||
assert.equal(resolveEmail("override@example.com", "original@example.com"), "override@example.com");
|
||||
assert.equal(
|
||||
resolveEmail("override@example.com", "original@example.com"),
|
||||
"override@example.com"
|
||||
);
|
||||
assert.equal(resolveEmail(undefined, "original@example.com"), "original@example.com");
|
||||
assert.equal(resolveEmail(undefined, null), null);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user