mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
refactor: improve type safety and add cloud agent providers
- Update types in several files to reduce usage of `any` - Fix `fetch` body type error in `AntigravityExecutor` by returning `ReadableStream` - Add `CLOUD_AGENT_PROVIDERS` constants Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -42,11 +42,20 @@ const CREDITS_EXHAUSTED_TTL_MS = 5 * 60 * 60 * 1000; // 5 hours
|
||||
|
||||
const BARE_PRO_IDS = new Set(["gemini-3.1-pro"]);
|
||||
|
||||
function getChunkedOrFixedBody(bodyStr: string, stream: boolean) {
|
||||
interface AntigravityContent {
|
||||
role: string;
|
||||
parts: unknown[];
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
function getChunkedOrFixedBody(bodyStr: string, stream: boolean): BodyInit {
|
||||
if (stream) {
|
||||
return (async function* () {
|
||||
yield new TextEncoder().encode(bodyStr);
|
||||
})();
|
||||
return new ReadableStream({
|
||||
async start(controller) {
|
||||
controller.enqueue(new TextEncoder().encode(bodyStr));
|
||||
controller.close();
|
||||
},
|
||||
});
|
||||
}
|
||||
return bodyStr;
|
||||
}
|
||||
@@ -453,7 +462,7 @@ export class AntigravityExecutor extends BaseExecutor {
|
||||
return { ...c, role, parts };
|
||||
}) || [];
|
||||
|
||||
const contents: any[] = [];
|
||||
const contents: AntigravityContent[] = [];
|
||||
for (const c of normalizedContents) {
|
||||
if (!Array.isArray(c.parts) || c.parts.length === 0) continue;
|
||||
if (contents.length > 0 && contents[contents.length - 1].role === c.role) {
|
||||
@@ -802,7 +811,7 @@ export class AntigravityExecutor extends BaseExecutor {
|
||||
let response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: finalHeaders,
|
||||
body: getChunkedOrFixedBody(serializedRequest.bodyString, stream) as any,
|
||||
body: getChunkedOrFixedBody(serializedRequest.bodyString, stream),
|
||||
...(stream ? { duplex: "half" } : {}),
|
||||
signal,
|
||||
});
|
||||
@@ -814,7 +823,7 @@ export class AntigravityExecutor extends BaseExecutor {
|
||||
response = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: retryHeaders,
|
||||
body: getChunkedOrFixedBody(serializedRequest.bodyString, stream) as any,
|
||||
body: getChunkedOrFixedBody(serializedRequest.bodyString, stream),
|
||||
...(stream ? { duplex: "half" } : {}),
|
||||
signal,
|
||||
});
|
||||
@@ -884,7 +893,7 @@ export class AntigravityExecutor extends BaseExecutor {
|
||||
const creditsResp = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: finalCreditsHeaders,
|
||||
body: getChunkedOrFixedBody(serializedCreditsRequest.bodyString, stream) as any,
|
||||
body: getChunkedOrFixedBody(serializedCreditsRequest.bodyString, stream),
|
||||
...(stream ? { duplex: "half" } : {}),
|
||||
signal,
|
||||
});
|
||||
|
||||
@@ -495,12 +495,21 @@ export function getModelLockoutInfo(provider, connectionId, model) {
|
||||
};
|
||||
}
|
||||
|
||||
type ModelLockoutInfo = {
|
||||
provider: string;
|
||||
connectionId: string;
|
||||
model: string;
|
||||
reason: string;
|
||||
remainingMs: number;
|
||||
failureCount: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get all active model lockouts (for dashboard)
|
||||
*/
|
||||
export function getAllModelLockouts() {
|
||||
export function getAllModelLockouts(): ModelLockoutInfo[] {
|
||||
const now = Date.now();
|
||||
const active: any[] = [];
|
||||
const active: ModelLockoutInfo[] = [];
|
||||
for (const key of modelLockouts.keys()) {
|
||||
cleanupModelLockKey(key, now);
|
||||
}
|
||||
@@ -908,7 +917,7 @@ export function checkFallbackError(
|
||||
backoffLevel: number = 0,
|
||||
_model: string | null = null,
|
||||
provider: string | null = null,
|
||||
headers: any = null,
|
||||
headers: Headers | Record<string, string> | null = null,
|
||||
profileOverride: ProviderProfile | null = null
|
||||
): {
|
||||
shouldFallback: boolean;
|
||||
|
||||
@@ -644,10 +644,13 @@ async function getGlmUsage(apiKey: string, providerSpecificData?: Record<string,
|
||||
resetAt,
|
||||
displayName: getGlmQuotaDisplayName(quotaName),
|
||||
details: Array.isArray(src.models)
|
||||
? src.models.map((m: any) => ({
|
||||
name: String(m.model || ""),
|
||||
used: toNumber(m.percentage, 0),
|
||||
}))
|
||||
? (src.models as unknown[]).map((m) => {
|
||||
const modelInfo = toRecord(m);
|
||||
return {
|
||||
name: String(modelInfo.model || ""),
|
||||
used: toNumber(modelInfo.percentage, 0),
|
||||
};
|
||||
})
|
||||
: [],
|
||||
unlimited: false,
|
||||
};
|
||||
|
||||
@@ -89,52 +89,58 @@ export const DEFAULT_SAFETY_SETTINGS = [
|
||||
];
|
||||
|
||||
// Convert OpenAI content to Gemini parts
|
||||
export function convertOpenAIContentToParts(content: any) {
|
||||
const parts: any[] = [];
|
||||
export function convertOpenAIContentToParts(content: unknown) {
|
||||
const parts: Record<string, unknown>[] = [];
|
||||
|
||||
if (typeof content === "string") {
|
||||
parts.push({ text: content });
|
||||
} else if (Array.isArray(content)) {
|
||||
for (const item of content) {
|
||||
if (item.type === "text") {
|
||||
parts.push({ text: item.text });
|
||||
const rec = toRecord(item);
|
||||
if (rec.type === "text") {
|
||||
parts.push({ text: rec.text });
|
||||
} else {
|
||||
// 1. Handle Gemini native inline_data injected into OpenAI arrays (e.g. Cherry Studio)
|
||||
const geminiInline = item.inline_data || item.inlineData;
|
||||
const geminiInline = toRecord(rec.inline_data || rec.inlineData);
|
||||
if (geminiInline?.data) {
|
||||
parts.push({
|
||||
inlineData: {
|
||||
mimeType: geminiInline.mime_type || geminiInline.mimeType || "application/pdf",
|
||||
data: geminiInline.data.replace(/^data:[a-zA-Z0-9/+-]+;base64,/, ""),
|
||||
mimeType: String(
|
||||
geminiInline.mime_type || geminiInline.mimeType || "application/pdf"
|
||||
),
|
||||
data: String(geminiInline.data).replace(/^data:[a-zA-Z0-9/+-]+;base64,/, ""),
|
||||
},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// 2. Handle Claude-style source blocks commonly used by AI clients
|
||||
if (item.source?.type === "base64" && item.source?.data) {
|
||||
const source = toRecord(rec.source);
|
||||
if (source?.type === "base64" && source?.data) {
|
||||
parts.push({
|
||||
inlineData: {
|
||||
mimeType: item.source.media_type || "application/pdf",
|
||||
data: item.source.data.replace(/^data:[a-zA-Z0-9/+-]+;base64,/, ""),
|
||||
mimeType: String(source.media_type || "application/pdf"),
|
||||
data: String(source.data).replace(/^data:[a-zA-Z0-9/+-]+;base64,/, ""),
|
||||
},
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// 3. Handle raw data strings (e.g. {"type": "file", "data": "JVBER...", "mime_type": "..."})
|
||||
const rawDataStr = item.data || item.file?.data || item.document?.data;
|
||||
const file = toRecord(rec.file);
|
||||
const doc = toRecord(rec.document);
|
||||
const rawDataStr = rec.data || file?.data || doc?.data;
|
||||
const mimeTypeFallback =
|
||||
item.mime_type ||
|
||||
item.media_type ||
|
||||
item.file?.mime_type ||
|
||||
item.document?.mime_type ||
|
||||
rec.mime_type ||
|
||||
rec.media_type ||
|
||||
file?.mime_type ||
|
||||
doc?.mime_type ||
|
||||
"application/octet-stream";
|
||||
if (typeof rawDataStr === "string" && !rawDataStr.startsWith("http")) {
|
||||
const rawData = rawDataStr.replace(/^data:[a-zA-Z0-9/+-]+;base64,/, "");
|
||||
parts.push({
|
||||
inlineData: {
|
||||
mimeType: mimeTypeFallback,
|
||||
mimeType: String(mimeTypeFallback),
|
||||
data: rawData,
|
||||
},
|
||||
});
|
||||
@@ -142,8 +148,11 @@ export function convertOpenAIContentToParts(content: any) {
|
||||
}
|
||||
|
||||
// 4. Standard OpenAI Data URIs
|
||||
const fileData =
|
||||
item.image_url?.url || item.file_url?.url || item.file?.url || item.document?.url;
|
||||
const imageUrl = toRecord(rec.image_url);
|
||||
const fileUrl = toRecord(rec.file_url);
|
||||
const fileObj = toRecord(rec.file);
|
||||
const docObj = toRecord(rec.document);
|
||||
const fileData = imageUrl?.url || fileUrl?.url || fileObj?.url || docObj?.url;
|
||||
if (typeof fileData === "string" && fileData.startsWith("data:")) {
|
||||
const commaIndex = fileData.indexOf(",");
|
||||
if (commaIndex !== -1) {
|
||||
|
||||
@@ -8,6 +8,14 @@ import type { ModelCooldownErrorPayload } from "@/types";
|
||||
* Strips stack traces and internal file paths from error messages before they
|
||||
* reach the client.
|
||||
*/
|
||||
interface ErrorResponseBody {
|
||||
error: {
|
||||
message: string;
|
||||
type?: string;
|
||||
code?: string;
|
||||
};
|
||||
}
|
||||
|
||||
function sanitizeErrorMessage(message: unknown): string {
|
||||
const str = typeof message === "string" ? message : String(message ?? "");
|
||||
// If the message contains a stack trace (lines starting with " at "),
|
||||
@@ -22,7 +30,7 @@ function sanitizeErrorMessage(message: unknown): string {
|
||||
* @param {string} message - Error message
|
||||
* @returns {object} Error response object
|
||||
*/
|
||||
export function buildErrorBody(statusCode, message) {
|
||||
export function buildErrorBody(statusCode: number, message: string): ErrorResponseBody {
|
||||
const errorInfo = getErrorInfo(statusCode);
|
||||
|
||||
return {
|
||||
@@ -220,10 +228,10 @@ export function createErrorResult(
|
||||
) {
|
||||
const body = buildErrorBody(statusCode, message);
|
||||
if (errorCode) {
|
||||
(body.error as any).code = errorCode;
|
||||
body.error.code = errorCode;
|
||||
}
|
||||
if (errorType) {
|
||||
(body.error as any).type = errorType;
|
||||
body.error.type = errorType;
|
||||
}
|
||||
|
||||
const result: {
|
||||
|
||||
@@ -185,5 +185,6 @@ export function createLogger(requestId: string | null = null): RequestScopedLogg
|
||||
* Module-level default logger (no requestId — for startup/config messages).
|
||||
*/
|
||||
export const defaultLogger = createLogger();
|
||||
export const log = defaultLogger;
|
||||
|
||||
export default logger;
|
||||
|
||||
@@ -403,7 +403,7 @@ function parseRateLimits(value: unknown): RateLimitRule[] | null {
|
||||
const parsed = JSON.parse(value);
|
||||
if (!Array.isArray(parsed)) return null;
|
||||
return parsed.filter(
|
||||
(rule: any) =>
|
||||
(rule: RateLimitRule) =>
|
||||
typeof rule === "object" &&
|
||||
rule !== null &&
|
||||
typeof rule.limit === "number" &&
|
||||
|
||||
@@ -272,7 +272,10 @@ export async function getPricingWithSources(): Promise<{
|
||||
export async function getPricingForModel(provider: string, model: string) {
|
||||
const pricing = await getPricing();
|
||||
|
||||
const findKeyInsensitive = (obj: Record<string, any> | undefined | null, key: string) => {
|
||||
const findKeyInsensitive = <T>(
|
||||
obj: Record<string, T> | undefined | null,
|
||||
key: string
|
||||
): T | undefined => {
|
||||
if (!obj || !key) return undefined;
|
||||
const lowerKey = key.toLowerCase();
|
||||
for (const [k, v] of Object.entries(obj)) {
|
||||
|
||||
@@ -1894,6 +1894,19 @@ export const SYSTEM_PROVIDERS = {
|
||||
},
|
||||
};
|
||||
|
||||
// Cloud Agent Providers
|
||||
export const CLOUD_AGENT_PROVIDERS = {
|
||||
jules: { id: "jules", alias: "jules", name: "Jules AI", icon: "smart_toy", color: "#6366F1" },
|
||||
devin: { id: "devin", alias: "devin", name: "Devin AI", icon: "auto_fix_high", color: "#10B981" },
|
||||
"codex-cloud": {
|
||||
id: "codex-cloud",
|
||||
alias: "codex-cloud",
|
||||
name: "Codex Cloud",
|
||||
icon: "cloud",
|
||||
color: "#3B82F6",
|
||||
},
|
||||
};
|
||||
|
||||
// All providers (combined)
|
||||
export const AI_PROVIDERS = {
|
||||
...FREE_PROVIDERS,
|
||||
|
||||
Reference in New Issue
Block a user