Browse Source

fix: resolve TypeScript any type errors and add Gemini provider type

- Replace all explicit 'any' types with proper TypeScript types:
  - gemini/types.ts: Record<string, unknown> for function args/responses
  - gemini/auth.ts: Typed OAuth response object
  - gemini/adapter.ts: Define ContentPart, InputMessage, TransformInput, OpenAICompatibleResponse interfaces
  - proxy/forwarder.ts: Record<string, unknown> for message body
- Add 'gemini' to ProviderType union (alongside existing 'gemini-cli')
- Add OpenAIResponsesResponse type definition for API test response handling
- Add gemini config to PROVIDER_TYPE_CONFIG for UI consistency
- Refactor adapter.ts array mapping to avoid type predicate issues
ding113 3 months ago
parent
commit
c0b8d2162a

+ 24 - 0
src/actions/providers.ts

@@ -850,6 +850,30 @@ type OpenAIChatResponse = {
   };
 };
 
+// OpenAI Responses API 响应类型
+type OpenAIResponsesResponse = {
+  id: string;
+  object: "response";
+  created_at: number;
+  model: string;
+  output: Array<{
+    type: "message";
+    id: string;
+    status: string;
+    role: "assistant";
+    content: Array<{
+      type: "output_text";
+      text: string;
+      annotations?: unknown[];
+    }>;
+  }>;
+  usage: {
+    input_tokens: number;
+    output_tokens: number;
+    total_tokens: number;
+  };
+};
+
 // Gemini API 响应类型
 type GeminiResponse = {
   candidates?: Array<{

+ 66 - 27
src/app/v1/_lib/gemini/adapter.ts

@@ -1,19 +1,59 @@
-import {
-  GeminiRequest,
-  GeminiContent,
-  GeminiResponse,
-  GeminiCandidate,
-  GeminiUsageMetadata,
-} from "./types";
+import { GeminiRequest, GeminiContent, GeminiResponse } from "./types";
+
+// Define input message types for request transformation
+interface ContentPart {
+  type?: string;
+  text?: string;
+  source?: { media_type?: string; data?: string };
+  image_url?: { media_type?: string; data?: string };
+}
+
+interface InputMessage {
+  role: string;
+  content: string | ContentPart[];
+}
+
+interface TransformInput {
+  messages?: InputMessage[];
+  system?: string;
+  temperature?: number;
+  top_p?: number;
+  max_tokens?: number;
+  stop_sequences?: string[];
+}
+
+// Define OpenAI-compatible response types
+interface OpenAIUsage {
+  prompt_tokens: number;
+  completion_tokens: number;
+  total_tokens: number;
+}
+
+interface OpenAICompatibleResponse {
+  id: string;
+  object: string;
+  created: number;
+  model: string;
+  choices: Array<{
+    index: number;
+    delta?: { content: string };
+    message?: { role: string; content: string };
+    finish_reason: string | null;
+  }>;
+  usage?: OpenAIUsage;
+}
 
 export class GeminiAdapter {
   /**
    * Convert generic chat request (OpenAI/Claude style) to Gemini format
    */
-  static transformRequest(input: any, providerType: "gemini" | "gemini-cli"): GeminiRequest {
+  static transformRequest(
+    input: TransformInput,
+    providerType: "gemini" | "gemini-cli"
+  ): GeminiRequest {
     const messages = input.messages || [];
     const contents: GeminiContent[] = [];
-    let systemInstructionParts: { text: string }[] = [];
+    const systemInstructionParts: { text: string }[] = [];
 
     // Handle system message(s)
     // Some formats allow multiple system messages or system message in "messages"
@@ -30,34 +70,33 @@ export class GeminiAdapter {
           typeof msg.content === "string"
             ? msg.content
             : Array.isArray(msg.content)
-              ? msg.content.map((c: any) => c.text).join("")
+              ? msg.content.map((c: ContentPart) => c.text || "").join("")
               : "";
         if (text) systemInstructionParts.push({ text });
         continue;
       }
 
       const role = msg.role === "assistant" ? "model" : "user";
-      let parts: { text: string; inlineData?: any }[] = [];
+      const parts: { text: string; inlineData?: { mimeType: string; data: string } }[] = [];
 
       if (typeof msg.content === "string") {
         parts.push({ text: msg.content });
       } else if (Array.isArray(msg.content)) {
-        parts = msg.content
-          .map((c: any) => {
-            if (c.type === "text") return { text: c.text };
-            if (c.type === "image" || c.type === "image_url") {
-              // Minimal support for image if base64 provided
-              // This needs more robust handling for real implementation
-              const source = c.source || c.image_url;
-              if (source && source.data) {
-                return {
-                  inlineData: { mimeType: source.media_type || "image/jpeg", data: source.data },
-                };
-              }
+        for (const c of msg.content) {
+          if (c.type === "text" && c.text) {
+            parts.push({ text: c.text });
+          } else if (c.type === "image" || c.type === "image_url") {
+            // Minimal support for image if base64 provided
+            // This needs more robust handling for real implementation
+            const source = c.source || c.image_url;
+            if (source && source.data) {
+              parts.push({
+                text: "",
+                inlineData: { mimeType: source.media_type || "image/jpeg", data: source.data },
+              });
             }
-            return { text: "" };
-          })
-          .filter((p: any) => p.text || p.inlineData);
+          }
+        }
       }
 
       if (parts.length > 0) {
@@ -102,7 +141,7 @@ export class GeminiAdapter {
   /**
    * Convert Gemini response to OpenAI-compatible chunks or full response
    */
-  static transformResponse(response: GeminiResponse, isStream: boolean): any {
+  static transformResponse(response: GeminiResponse, isStream: boolean): OpenAICompatibleResponse {
     // Extract content
     let content = "";
     const candidate = response.candidates?.[0];

+ 1 - 1
src/app/v1/_lib/gemini/auth.ts

@@ -60,7 +60,7 @@ export class GeminiAuth {
           throw new Error(`Failed to refresh token: ${response.statusText}`);
         }
 
-        const data = (await response.json()) as any;
+        const data = (await response.json()) as { access_token?: string };
         if (data.access_token) {
           // Note: We are not persisting the new token back to DB here.
           // This means we might refresh more often than needed if the DB is not updated.

+ 3 - 3
src/app/v1/_lib/gemini/types.ts

@@ -8,11 +8,11 @@ export interface GeminiContent {
     };
     functionCall?: {
       name: string;
-      args: Record<string, any>;
+      args: Record<string, unknown>;
     };
     functionResponse?: {
       name: string;
-      response: Record<string, any>;
+      response: Record<string, unknown>;
     };
   }[];
 }
@@ -36,7 +36,7 @@ export interface GeminiTool {
   functionDeclarations?: {
     name: string;
     description?: string;
-    parameters?: Record<string, any>; // Officially parameters, but translated to parametersJsonSchema for CLI if needed? No, official uses parameters.
+    parameters?: Record<string, unknown>; // Officially parameters, but translated to parametersJsonSchema for CLI if needed? No, official uses parameters.
   }[];
 }
 

+ 1 - 1
src/app/v1/_lib/proxy/forwarder.ts

@@ -589,7 +589,7 @@ export class ProxyForwarder {
 
       // Detect streaming from original request
       try {
-        const originalBody = session.request.message as any;
+        const originalBody = session.request.message as Record<string, unknown>;
         isStreaming = originalBody.stream === true;
       } catch {
         isStreaming = false;

+ 5 - 0
src/lib/provider-type-utils.tsx

@@ -34,6 +34,11 @@ export const PROVIDER_TYPE_CONFIG: Record<
     iconColor: "text-blue-600",
     bgColor: "bg-blue-500/15",
   },
+  gemini: {
+    icon: Gemini.Color,
+    iconColor: "text-emerald-600",
+    bgColor: "bg-emerald-500/15",
+  },
   "gemini-cli": {
     icon: Gemini.Color,
     iconColor: "text-emerald-600",

+ 7 - 1
src/types/provider.ts

@@ -1,5 +1,11 @@
 // 供应商类型枚举
-export type ProviderType = "claude" | "claude-auth" | "codex" | "gemini-cli" | "openai-compatible";
+export type ProviderType =
+  | "claude"
+  | "claude-auth"
+  | "codex"
+  | "gemini"
+  | "gemini-cli"
+  | "openai-compatible";
 
 // Codex Instructions 策略枚举
 export type CodexInstructionsStrategy = "auto" | "force_official" | "keep_original";