8000 feat: Custom Pass-Through Channel by simplty · Pull Request #1320 · QuantumNous/new-api · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

feat: Custom Pass-Through Channel #1320

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open

Conversation

simplty
Copy link
@simplty simplty commented Jun 30, 2025

🚀 New Feature: CustomPass Custom Pass-Through Channel

📋 Feature Overview

This submission introduces the CustomPass custom pass-through channel type. It allows users to configure custom APIs for pass-through proxying, providing the system with more flexible API integration capabilities.

✨ Key Features

🔧 Backend Features

  • New Channel Type: Added CustomPass channel type (ID: 51) supporting custom API pass-through.
  • Pass-Through Handler: Implemented complete pass-through request handling logic, supporting various HTTP methods.
  • Flexible Configuration: Supports configuring custom request headers via the environment variable CUSTOM_PASS_HEADER_KEY.
  • Task Scheduling: Full support for task scheduling and asynchronous processing.
  • Billing Support: Integrated with the existing billing system, supporting usage-based and per-call billing.

🎨 Frontend Features

  • Channel Selection: Updated the channel selection modal with pagination and filtering capabilities.
  • Icon Component: Added a dedicated CustomPass icon component for visual identification.
  • UI Integration: Fully integrated into the existing channel management interface.

⚙️ Configuration Options

  • Environment Variable: CUSTOM_PASS_HEADER_KEY - Configures the custom header key for upstream requests.
  • BaseURL: Allows users to configure the base URL of custom APIs through the frontend interface.
  • API Key: Supports configuring API keys for authentication.
  • Full Pass-Through: Supports configuring whether to fully pass through upstream responses (currently only effective for submit tasks).

🔄 Technical Implementation

Core Components

  1. CustomPass Adapter (relay/channel/custompass/adaptor.go)

    • Handles request URL construction and header settings.
    • Supports dynamic model name routing.
    • Integrates authentication mechanisms.
  2. Pass-Through Handler (relay/custompass_handler.go)

    • Unified entry point for pass-through request processing.
    • Complete error handling and response forwarding.
    • Supports streaming and non-streaming responses.
  3. Task Scheduling Support (relay/channel/task/custompass/)

    • Asynchronous task processing capabilities.
    • Complete task lifecycle management.

Routing Configuration

  • Added /pass/{model} route for pass-through requests.
  • Supports dynamic model names and operation types.

📁 File Change Statistics

  • New Files: 8 core feature files.
  • Modified Files: 25 existing files enhanced with new functionality.
  • Total Changes: 1939 lines added, 127 lines deleted.

Major Changed Files

New Files:
- relay/channel/custompass/adaptor.go
- relay/channel/custompass/constants.go
- relay/channel/task/custompass/adaptor.go
- relay/channel/task/custompass/models.go
- relay/custompass_handler.go
- web/src/components/common/logo/CustomPassIcon.js
- dev-rebuild.sh

Modified Files:
- .env.example (Added environment variable configuration)
- common/constants.go (Added channel constants)
- controller/relay.go (Integrated pass-through handling)
- middleware/distributor.go (Routing distribution support)
- web/src/components/settings/ChannelSelectorModal.js (UI enhancements)
- and 20+ other related files

🔒 Security Considerations

  • Optional header configuration to avoid unnecessary token leakage.
  • Complete error handling and status code forwarding.
  • Supports existing authentication and authorization mechanisms.

🎯 Use Cases

  • Integrating third-party custom API services.
  • Proxying internal API interfaces.
  • Unified API gateway functionality.
  • Aggregating multi-vendor API services.

⚙️ Detailed Configuration Instructions

Environment Variable Configuration

# Configure the header key for the custom pass-through channel
CUSTOM_PASS_HEADER_KEY=x-api-token

Channel Configuration Options

Full Pass-Through Configuration

  • Functionality: Controls the response behavior when submitting tasks.
  • Current Support: Only effective for submit tasks.
  • Future Expansion: Other pass-through interfaces may support custom key-value transformations.
Configuration Item Description Default Value Scope of Impact
Full Pass-Through Whether to fully return the upstream API response Off submit tasks
BaseURL Base address of the upstream API Required All requests
API Key Authentication key for the upstream API Required All requests
Custom Headers Additional headers controlled by env var Optional All requests

Pass-Through Mode Comparison

Mode submit Task Return Other API Return Applicable Scenarios
Default Mode Returns only task_id Full Pass-Through Standard asynchronous task processing
Full Pass-Through Returns upstream full response Full Pass-Through When detailed submission information is needed

Future Feature Plans

  • Key-Value Transformation: Support for custom request/response field mapping.
  • Data Filtering: Support for configuring filtering rules for returned fields.
  • Format Conversion: Support for automatic conversion between different API formats.

🚦 Deployment Instructions

  1. Update the environment variable configuration file and set CUSTOM_PASS_HEADER_KEY as needed.
  2. Rebuild and deploy the application.
  3. Create a CustomPass type channel in the management interface.
  4. Configure the target API's BaseURL and related parameters.

📊 System Flowchart

CustomPass Processing Flow

flowchart TD
    A["Client Request"] --> B{"Path Matching"}
    B -->|"/pass/{model}"| C["CustomPass Route Handling"]
    B -->|"Other Paths"| D["Regular API Handling"]

    C --> E["Generate RelayInfo"]
    E --> F["Get CustomPass Adapter"]
    F --> G{"Adapter Initialization Success?"}

    G -->|"No"| H["Return Error: invalid_api_type"]
    G -->|"Yes"| I["Read Request Body"]

    I --> J{"HTTP Method Check"}
    J -->|"GET"| K["No Request Body Reading Needed"]
    J -->|"POST/PUT/PATCH"| L["Read Request Body Content"]

    K --> M["Construct Upstream Request URL"]
    L --> M
    M --> N{"BaseURL Configuration Check"}

    N -->|"Not Configured"| O["Return Error: base_url required"]
    N -->|"Configured"| P["Set Request Headers"]

    P --> Q{"CUSTOM_PASS_HEADER_KEY Check"}
    Q -->|"Configured"| R["Add Custom Header"]
    Q -->|"Not Configured"| S["Add Only Authorization Header"]

    R --> T["Execute Upstream API Request"]
    S --> T

    T --> U{"Upstream Response Status"}
    U -->|"Success 200"| V["Process Response Content"]
    U -->|"Error"| W["Error Handling and Forwarding"]

    V --> X{"Response Type"}
    X -->|"Streaming Response"| Y["Stream Response Forwarding"]
    X -->|"Normal Response"| Z["JSON Response Forwarding"]

    Y --> AA["Return to Client"]
    Z --> AA
    W --> AA

    AA --> BB["Record Usage and Billing"]
    BB --> CC["Complete Request Processing"]

    style C fill:#e1f5fe
    style F fill:#f3e5f5
    style T fill:#fff3e0
    style AA fill:#e8f5e8
Loading

CustomPass System Architecture Diagram

graph TB
    subgraph Frontend["Frontend Interface"]
        UI["Management UI"]
        MODAL["Channel Selection Modal"]
        ICON["CustomPass Icon"]
    end

    subgraph Gateway["API Gateway Layer"]
        ROUTER["Route Distributor"]
        MIDDLEWARE["Middleware"]
    end

    subgraph Core["CustomPass Core"]
        HANDLER["CustomPass Handler"]
        ADAPTOR["CustomPass Adapter"]
        CONSTANTS["Constant Definitions"]
    end

    subgraph Task["Task Scheduling"]
        TASK_ADAPTOR["Task Adapter"]
        TASK_MODEL["Task Model"]
        SCHEDULER["Task Scheduler"]
    end

    subgraph Services["General Services"]
        AUTH["Authentication"]
        BILLING["Billing Service"]
        QUOTA["Quota Management"]
        LOG["Logging"]
    end

    subgraph Config["Configuration Management"]
        ENV["Environment Variables"]
        CHANNEL["Channel Configuration"]
        MODEL["Model Configuration"]
    end

    subgraph Upstream["Upstream API"]
        CUSTOM_API["Custom API Service"]
    end

    UI --> MODAL
    MODAL --> ICON
    UI --> ROUTER

    ROUTER --> MIDDLEWARE
    MIDDLEWARE --> HANDLER

    HANDLER --> ADAPTOR
    ADAPTOR --> CONSTANTS
    HANDLER --> AUTH
    HANDLER --> CUSTOM_API

    ADAPTOR --> TASK_ADAPTOR
    TASK_ADAPTOR --> TASK_MODEL
    TASK_MODEL --> SCHEDULER

    HANDLER --> BILLING
    HANDLER --> QUOTA
    HANDLER --> LOG

    ADAPTOR --> ENV
    ADAPTOR --> CHANNEL
    ADAPTOR --> MODEL

    style HANDLER fill:#ff9999
    style ADAPTOR fill:#99ccff
    style CUSTOM_API fill:#99ff99
    style ENV fill:#ffcc99
Loading

📝 API Examples

🔄 Task API Example (Asynchronous Processing)

Description:

  • Task Submission: The system will pass all query parameters and body parameters from the client to the upstream API.
  • Default Mode: Only returns task_id to the client.
  • Full Pass-Through Mode: Can be configured to fully return the upstream API's original response.
  • Task Query: The system will return all information returned by the upstream API as is to the client, achieving full pass-through.

1. Task Submission

Request Example
# Submit a task to the upstream API
# Task submission URL format is fixed: /pass/xxx/submit, where `xxx/submit` is the model name
POST /pass/xxx/submit
Authorization: Bearer sk-xxx
Content-Type: application/json

{
  "prompt": "Please write a poem about spring",
  "max_tokens": 1000,
  "temperature": 0.7,
  "stream": false
}
Upstream API Response Example
{
  "code": 0,
  "msg": "success",
  "data": {
    "task_id": "task_abc123def456",
    "status": "SUBMITTED",
    "created_at": "2025-06-30T19:24:13+08:00"
  },
  "usage": {
    "prompt_tokens": 15,
    "completion_tokens": 0,
    "total_tokens": 15
  }
}
System Response to Client

Default Mode (Returns only task_id)

{
  "task_id": "task_abc123def456"
}

Full Pass-Through Mode (Returns upstream full response)

{
  "code": 0,
  "msg": "success",
  "data": {
    "task_id": "task_abc123def456",
    "status": "SUBMITTED",
    "created_at": "2025-06-30T19:24:13+08:00"
  },
  "usage": {
    "prompt_tokens": 15,
    "completion_tokens": 0,
    "total_tokens": 15
  }
}
Billing Processing
  • Billing Method: Usage-based billing (upon task submission).
  • Cost Calculation: 15 × 1.0 × 1.0 = 15 tokens.
  • Log Record: "CustomPass usage billing: prompt_tokens=15, completion_tokens=0, model_multiplier=1.00, completion_multiplier=1.00, group_multiplier=1.00"

2. Task Query

Request Example
# Query task status
# Task query URL format is fixed: /pass/xxx/task/list-by-condition
POST /pass/xxx/task/list-by-condition
Authorization: Bearer sk-xxx
Content-Type: application/json

{
  "task_ids": ["task_abc123def456", "task_xyz789ghi012"],
  "status": ["IN_PROGRESS", "SUCCESS"]
}
Upstream API Response Example
{
  "code": 0,
  "msg": "success",
  "data": [
    {
      "task_id": "task_abc123def456",
      "status": "SUCCESS",
      "progress": "100%",
      "result": [
        {
          "content": "Spring breeze gently blows willow catkins,\nPeach blossoms fill the trees reflecting morning glow.\nSwallows return to build new nests,\nAll things revive and show vitality.",
          "finish_reason": "stop"
        }
      ],
      "error": null
    },
    {
      "task_id": "task_xyz789ghi012",
      "status": "IN_PROGRESS",
      "progress": "60%",
      "result": null,
      "error": null
    }
  ]
}

3. Task Error Handling

Task Submission Failure
{
  "code": 400,
  "msg": "Invalid request parameters",
  "data": {
    "error_type": "validation_error",
    "details": "max_tokens must be between 1 and 4096"
  }
}
Task Query Failure
{
  "code": 404,
  "msg": "Task not found",
  "data": [
    {
      "task_id": "task_notfound123",
      "status": "UNKNOWN",
      "progress": "0%",
      "result": null,
      "error": "Task with ID task_notfound123 not found"
    }
  ]
}

⚡ Regular API Example (Synchronous Processing)

Description: When submitting a request, the system will pass all query parameters and body parameters from the client to the upstream API. When returning, it will return all information from the upstream API as is to the client, achieving full pass-through.

1. Text Generation API

Request Example
# Directly call the upstream text generation API
# {your_path_to_api} is the API path, e.g., image/crop, text/generate/xx, etc.
POST /pass/{your_path_to_api}
Authorization: Bearer sk-xxx
Content-Type: application/json

{
  "messages": [
    {
      "role": "user",
      "content": "Please explain what machine learning is"
    }
  ],
  "max_tokens": 500,
  "temperature": 0.7,
  "stream": false
}
Upstream API Response Example (with Usage)
{
  "success": true,
  "data": {
    "content": "Machine learning is an important branch of artificial intelligence that enables computers to learn and improve without being explicitly programmed. By analyzing large amounts of data, machine learning algorithms can identify patterns, make predictions, and optimize performance automatically.",
    "status": "completed"
  },
  "usage": {
    "prompt_tokens": 15,
    "completion_tokens": 85,
    "total_tokens": 100
  }
}
Billing Processing
  • Billing Method: Usage-based billing.
  • Cost Calculation: (15 + 85 × 1.0) × 1.0 × 1.0 = 100 tokens.
  • Log Record: "CustomPass API call: model={your_path_to_api}, prompt_tokens=15, completion_tokens=85, total_tokens=100"

2. Image Generation API

Request Example
# Call the image generation API
# {your_path_to_api} is the API path, e.g., image/crop, text/generate/xx, etc.
POST /pass/{your_path_to_api}
Authorization: Bearer sk-xxx
Content-Type: application/json

{
  "prompt": "A cute kitten playing in a garden",
  "size": "1024x1024",
  "quality": "standard",
  "n": 1
}
Upstream API Response Example (Per-Call Billing)
{
  "success": true,
  "result": {
    "image_url": "https://example.com/generated-image-123.png",
    "width": 1024,
    "height": 1024,
    "format": "png"
  }
}
Billing Processing
  • Billing Method: Per-call billing (no Usage information).
  • Cost Calculation: 0.02 × 1.0 × 500 = 10 quota.
  • Log Record: "CustomPass per-call billing: model={your_path_to_api}, price=0.0200, group_ratio=1.00"

3. Speech-to-Text API

Request Example
# Call the speech recognition API
# {your_path_to_api} is the API path, e.g., image/crop, text/generate/xx, etc.
POST /pass/{your_path_to_api}
Authorization: Bearer sk-xxx
Content-Type: multipart/form-data

--boundary123
Content-Disposition: form-data; name="file"; filename="audio.mp3"
Content-Type: audio/mpeg

[Audio file binary data]
--boundary123
Content-Disposition: form-data; name="model"

whisper-1
--boundary123--
Upstream API Response Example
{
  "success": true,
  "transcript": "This is a transcription of a test audio, used to demonstrate the effect of speech recognition.",
  "confidence": 0.95,
  "duration": 12.5
}
Billing Processing
  • Billing Method: 0 cost (no price configured).
  • Cost Calculation: 0 quota.
  • Log Record: "CustomPass 0 cost: Model {your_path_to_api} has no usage and no per-call billing price configured."

4. Error Handling

Upstream API Error
{
  "success": false,
  "error": {
    "code": "AUTH_FAILED",
    "message": "Invalid API key provided"
  }
}
System Error Handling
  • Status Code: 401 Unauthorized.
  • Error Forwarding: Upstream error information is forwarded directly.
  • Billing Processing: No billing is performed.
  • Log Record: Error information is logged without charging.

📊 Logging Instructions

Cases Where Consumption Logs Are NOT Recorded

In the following situations, the system will not write requests to the consumption log:

  1. Request Failure: Upstream API returns an error status code (4xx, 5xx).
  2. Authentication Failure: API key is invalid or insufficient permissions.
  3. Network Errors: Unable to connect to the upstream API service.
  4. Request Timeout: Upstream API response timeout.
  5. System Internal Errors: Exceptions occurring during system processing.
  6. Zero Cost and No Billing Configured: No usage information and no per-call billing price configured.

Cases Where Consumption Logs ARE Recorded

  1. Successful Call with Usage: Billed and recorded based on token usage.
  2. Successful Call with Per-Call Billing Configured: Billed and recorded at a fixed price.
  3. Successful Task Submission: Even if the final task fails, the cost at submission time is recorded.

🔍 Task Status Judgment Mechanism

Task Status Definitions

The new-api defines the following task statuses:

  • NOT_START: Task has not started.
  • SUBMITTED: Task has been submitted.
  • QUEUED: Task is in queue.
  • IN_PROGRESS: Task is in progress.
  • SUCCESS: Task has completed successfully.
  • FAILURE: Task has failed.
  • UNKNOWN: Unknown status.

CustomPass Task Status Mapping

The system maps the status returned by the upstream API to internal statuses:

func convertCustomPassStatus(status string) model.TaskStatus {
    switch status {
    case "completed":
        return model.TaskStatusSuccess
    case "error", "failed":
        return model.TaskStatusFailure
    case "pendding", "processing":
        return model.TaskStatusInProgress
    default:
        return model.TaskStatusUnknown
    }
}

Task Success Judgment Conditions

  1. Status is SUCCESS: Upstream API returns status completed.
  2. Progress is 100%: Task progress reaches 100%.
  3. Result Data Exists: The result field contains valid result content.
  4. No Error Information: The error field is empty or null.

Task Failure Judgment Conditions

  1. Status is FAILURE: Upstream API returns status error or failed.
  2. Error Information Exists: The error field contains an error description.
  3. Error in Results: The result array contains error information.
  4. Timeout Judgment: Task has been running for more than 1 hour and progress is not 100%.

Failure Compensation Mechanism

When a task fails, the system automatically compensates for the cost:

if task.Status == model.TaskStatusFailure {
    quota := task.Quota
    if quota != 0 {
        err = model.IncreaseUserQuota(task.UserId, quota, false)
        logContent := fmt.Sprintf("Asynchronous task execution failed %s, compensating %s", task.TaskID, common.LogQuota(quota))
        model.RecordLog(task.UserId, model.LogTypeSystem, logContent)
    }
}

💰 CustomPass Billing Method Details

Billing Priority

CustomPass adopts a three-tier billing priority:

  1. Usage-Based Billing (Highest Priority)
  2. Per-Call Billing (Medium Priority)
  3. Zero Cost (Lowest Priority)

1. Usage-Based Billing

API Response Example with Usage

{
  "code": 0,
  "msg": "success",
  "data": {
    "task_id": "task_usage_billing_123",
    "content": "This is the AI-generated content...",
    "finish_reason": "stop"
  },
  "usage": {
    "prompt_tokens": 25,
    "completion_tokens": 150,
    "total_tokens": 175
  }
}

Usage Billing Logic

// If usage information is present, usage-based billing is prioritized.
if usage != nil && usage.TotalTokens > 0 {
    // Method 1: Fixed price billing
    if usePrice && modelPrice > 0 {
        quota = modelPrice * groupRatio * QuotaPerUnit
    } else {
        // Method 2: Multiplier-based billing
        quota = (promptTokens + completionTokens * completionRatio) * modelRatio * groupRatio
    }
}

Billing Log Example

CustomPass usage billing: prompt_tokens=25, completion_tokens=150, model_multiplier=1.00, completion_multiplier=1.00, group_multiplier=1.00

2. Per-Call Billing

API Response Example without Usage

{
  "code": 0,
  "msg": "success",
  "data": {
    "task_id": "task_fixed_price_123",
    "content": "This is the AI-generated content...",
    "finish_reason": "stop"
  }
}

Per-Call Billing Logic

// If no usage is present but a per-call price is configured
if usePrice && modelPrice > 0 {
    quota = modelPrice * groupRatio * QuotaPerUnit
}

Billing Log Example

CustomPass per-call billing: model_price=0.0200, group_ratio=1.00

3. Zero Cost

Zero Cost Conditions

  • The upstream API does not return usage information.
  • The model does not have a per-call billing price configured.
  • Or the configured price is 0.

Zero Cost Log Example

CustomPass 0 cost: Model gpt-4/submit has no usage and no per-call billing price configured.

Billing Configuration Example

Model Price Configuration

{
  "gpt-4/submit": {
    "price": 0.02,        // Per-call billing price
    "ratio": 1.0,         // Model multiplier
    "completion_ratio": 1.0  // Completion multiplier
  },
  "claude-3/generate": {
    "ratio": 1.5,         // Multiplier configured only, supports usage-based billing
    "completion_ratio": 1.2
  }
}

Group Multiplier Configuration

{
  "default": 1.0,        // Default group multiplier
  "premium": 0.8,        // Discount for premium user group
  "enterprise": 0.6      // Discount for enterprise user group
}

Summary by CodeRabbit

  • New Features

    • Introduced a "CustomPass" channel and platform, enabling customizable task submission and API pass-through with flexible model and action support.
    • Added support for dynamic quota calculation and detailed billing based on usage or fixed pricing for CustomPass tasks.
    • Added new environment variable options for CustomPass configuration.
    • Implemented new API endpoints under /pass for task submission and general API calls.
  • User Interface

    • Added a CustomPass icon and visual tags for CustomPass tasks and channels in the web dashboard.
    • Updated task logs to display CustomPass platform and model details.
    • Added "自定义透传渠道" (CustomPass Channel) to the channel selection options.
  • Bug Fixes

    • Improved robustness in API request handling for custom channels, including safer body and query parameter management.
  • Documentation

    • Updated environment variable examples to include CustomPass-related options.
  • Tests

    • Added unit tests for CustomPass task adaptor, focusing on usage extraction and response handling.

This commit introduces comprehensive support for a new "CustomPass" channel, enabling flexible integration with various external APIs.

Key features and changes include:

- **Constants and Aliases**: Added `ChannelTypeCustomPass` to `common/constants.go` and `constant/env.go`. Defined `TaskPlatformCustomPass` in `constant/task.go`.
- **Relay Logic**:
    - Introduced `RelayModeCustomPassSubmit` and `RelayModeCustomPassAPI` in `relay/constant/relay_mode.go`.
    - Implemented `CustomPassHelper` in `relay/custompass_handler.go` to handle general API requests.
    - Updated `controller.Relay` to support `RelayModeCustomPassAPI`.
    - Modified `controller.RelayTask` to handle CustomPass task submissions.
    - Added routing for `/pass/*` paths in `router/relay-router.go`.
- **Task Management**:
    - Implemented `TaskAdaptor` for CustomPass in `relay/channel/task/custompass/adaptor.go`, including request/response handling, task submission, querying, and error management.
    - Added `UpdateCustomPassTaskAll` and related functions in `controller/task.go` to manage task status updates.
    - Saved model name and actual consumed quota in `model.Task` properties for CustomPass.
- **Request/Response Handling**:
    - Updated `relay/channel/api_request.go` to handle GET requests for CustomPass by appending query parameters.
    - Created `relay/channel/custompass/adaptor.go` for general API calls, handling request/response conversion, header setup, and usage extraction for billing.
- **Billing and Logging**:
    - Implemented `service.CalculateCustomPassQuota` and related functions in `service/quota.go` to handle dynamic billing based on usage, per-request pricing, and group ratios.
    - Enhanced `relay/relay_task.go` to correctly calculate and record CustomPass task costs, including detailed logging.
- **UI Integration**:
    - Added a new `CustomPassIcon` component in `web/src/components/common/logo/CustomPassIcon.js`.
    - Integrated CustomPass into `ChannelSelectorModal` and `TaskLogsTable` in the web UI for better usability and visibility.
    - Updated `channel.constants.js` with the new channel option.

The `CustomPassAPI` allows for flexible integration of services that do not conform to standard OpenAI API formats. The `CustomPassSubmit` functionality enables task-based workflows. The implementation includes robust error handling, usage tracking, and flexible billing mechanisms.
Copy link
Contributor
coderabbitai bot commented Jun 30, 2025

Walkthrough

This update introduces a new "CustomPass" relay channel and task platform, enabling flexible pass-through of API requests and task processing with dynamic quota and billing support. It adds new backend adaptors, environment variables, quota calculation logic, and routing for /pass endpoints. The web interface is enhanced with icons and display logic for the CustomPass channel and platform.

Changes

File(s) Change Summary
common/constants.go, constant/env.go, constant/task.go, relay/constant/api_type.go, relay/constant/relay_mode.go, relay/common/relay_info.go, relay/relay_adaptor.go, relay/custompass_handler.go, controller/relay.go, controller/task.go, service/quota.go, relay/relay_task.go Added constants, environment variables, task platform, relay modes, API type, relay info validation, relay adaptor support, CustomPass helper, task update logic, and dynamic quota calculation for CustomPass.
relay/channel/custompass/constants.go, relay/channel/custompass/adaptor.go, relay/channel/task/custompass/models.go, relay/channel/task/custompass/adaptor.go, relay/channel/task/custompass/adaptor_test.go Added CustomPass channel and task adaptors, constants, model lists, and unit tests for usage extraction and response parsing.
relay/channel/api_request.go Enhanced GET request handling for CustomPass channel to preserve query parameters; improved request body and GetBody handling for robustness.
model/task.go Extended Task struct with TokenId and TokenKey; added Model field to Properties struct; updated task initialization accordingly.
router/relay-router.go, router/web-router.go Added /pass route group with middleware and handlers for task submission and API relay; updated NoRoute handler to include /pass prefix.
web/src/components/common/logo/CustomPassIcon.js, web/src/helpers/render.js Added CustomPassIcon React component and integrated it into channel icon rendering logic.
web/src/components/table/TaskLogsTable.js Updated task logs table to render CustomPass platform and model info with new icons and tags; modified renderType signature.
web/src/constants/channel.constants.js Added CustomPass channel option with value 51, orange color, and label "自定义透传渠道".
.env.example Documented new environment variables for CustomPass header key, full passthrough mode, and status mappings with defaults.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant WebServer
    participant RelayController
    participant CustomPassAdaptor
    participant UpstreamAPI
    participant TaskDB

    Client->>WebServer: POST /pass/{model}/submit (task submission)
    WebServer->>RelayController: Route to RelayTask
    RelayController->>CustomPassAdaptor: Validate & build request
    CustomPassAdaptor->>UpstreamAPI: Forward submission request
    UpstreamAPI-->>CustomPassAdaptor: Response (task_id, usage)
    CustomPassAdaptor->>RelayController: Return task_id, usage
    RelayController->>TaskDB: Insert task with usage and quota
    RelayController-->>Client: Respond with task_id

    Client->>WebServer: GET /pass/{model}/status (task status query)
    WebServer->>RelayController: Route to Relay
    RelayController->>CustomPassAdaptor: Build status query
    CustomPassAdaptor->>UpstreamAPI: Forward status request
    UpstreamAPI-->>CustomPassAdaptor: Response (task status, progress)
    CustomPassAdaptor->>RelayController: Return status info
    RelayController-->>Client: Respond with status info
Loading

Poem

🥕
A custom pass for every need,
Through new routes, your tasks proceed.
Icons shine and logs now tell,
Of quotas used and tasks that dwell.
With tokens counted, models shown—
The rabbit hops where code has grown!


📜 Recent review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 62f602c and e796826.

📒 Files selected for processing (1)
  • relay/constant/relay_mode.go (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • relay/constant/relay_mode.go
✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Contributor
@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

🧹 Nitpick comments (6)
relay/channel/api_request.go (1)

43-48: Consider using channel type constants instead of string comparison.

The query parameter preservation for CustomPass GET requests is appropriate for pass-through functionality. However, consider using a more robust channel type check instead of string comparison:

-if a.GetChannelName() == "custompass" && c.Request.Method == "GET" {
+if a.GetChannelName() == "custompass" && c.Request.Method == "GET" {

While the current implementation works, using constants would be more maintainable and less prone to typos.

Also applies to: 269-274

middleware/distributor.go (1)

190-208: Good implementation of CustomPass path handling with room for validation improvement.

The logic correctly extracts the subpath after /pass/ and sets the appropriate context for CustomPass requests. The logging provides good visibility into the routing process.

Consider adding validation for the extracted path to ensure it's not empty:

 if path != "" {
     modelRequest.Model = path
+} else {
+    return nil, false, errors.New("invalid CustomPass path: model name cannot be empty")
 }

This would provide clearer error messages for malformed requests.

service/quota.go (1)

424-474: Robust quota calculation with comprehensive logic and minor optimization opportunity.

The three-tier priority system (usage-based → fixed-price → zero-cost) is well-designed and handles various billing scenarios appropriately. The use of decimal arithmetic prevents precision issues, and the logging provides excellent traceability.

Consider simplifying the minimum quota condition for better readability:

-if !modelRatio.IsZero() && !groupRatio.IsZero() && quota.LessThanOrEqual(decimal.Zero) {
+if quota.LessThanOrEqual(decimal.Zero) && (!modelRatio.IsZero() || !groupRatio.IsZero()) {
     quota = decimal.NewFromInt(1)
 }

This ensures minimum quota of 1 when any ratio is configured but the calculated quota is non-positive, which seems more aligned with the intent.

relay/relay_task.go (1)

124-178: Complex but well-implemented dynamic quota calculation.

The deferred quota adjustment logic correctly handles the difference between pre-deducted quota and actual usage-based quota. The implementation properly:

  • Calculates final quota based on actual usage
  • Adjusts the quota difference
  • Records detailed logs with token usage
  • Handles different billing scenarios

Consider adding a comment block explaining the quota adjustment flow for future maintainers.

relay/channel/task/custompass/adaptor.go (2)

235-283: Consider simplifying the task_id extraction logic.

The nested switch/if logic for extracting task_id is complex and could be simplified for better maintainability.

Consider extracting this logic into a separate helper method:

+func extractTaskID(data interface{}) (string, error) {
+    dataMap, ok := data.(map[string]interface{})
+    if !ok {
+        if _, isString := data.(string); isString {
+            return "", fmt.Errorf("data field is string type, but task_id must be provided in data object")
+        }
+        return "", fmt.Errorf("unsupported data type for task_id extraction: %T, data must be an object containing task_id", data)
+    }
+    
+    taskIdInterface, exists := dataMap["task_id"]
+    if !exists {
+        return "", fmt.Errorf("task_id not found in response")
+    }
+    
+    taskIdStr, ok := taskIdInterface.(string)
+    if !ok {
+        return "", fmt.Errorf("invalid task_id format")
+    }
+    
+    return taskIdStr, nil
+}

-// From line 235 onwards, replace with:
+taskID, err = extractTaskID(submitResp.Data)
+if err != nil {
+    taskErr = service.TaskErrorWrapper(err, err.Error(), http.StatusBadRequest)
+    taskErr.Data = map[string]interface{}{
+        "raw_response": string(responseBody),
+    }
+    return
+}

407-410: Ensure model name manipulation is safe.

The logic to remove "/submit" suffix could incorrectly modify model names that contain this pattern in the middle (e.g., "model/submit/v2").

The current implementation using strings.HasSuffix and strings.TrimSuffix is actually safe and will only remove the suffix. The comment is to ensure this behavior is intentional and documented.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d389bef and 6c08ce7.

⛔ Files ignored due to path filters (1)
  • web/bun.lockb is excluded by !**/bun.lockb
📒 Files selected for processing (27)
  • common/constants.go (2 hunks)
  • constant/env.go (2 hunks)
  • constant/task.go (1 hunks)
  • controller/relay.go (2 hunks)
  • controller/task.go (2 hunks)
  • middleware/distributor.go (2 hunks)
  • model/task.go (3 hunks)
  • relay/channel/api_request.go (3 hunks)
  • relay/channel/custompass/adaptor.go (1 hunks)
  • relay/channel/custompass/constants.go (1 hunks)
  • relay/channel/task/custompass/adaptor.go (1 hunks)
  • relay/channel/task/custompass/adaptor_test.go (1 hunks)
  • relay/channel/task/custompass/models.go (1 hunks)
  • relay/common/relay_info.go (1 hunks)
  • relay/constant/api_type.go (2 hunks)
  • relay/constant/relay_mode.go (2 hunks)
  • relay/custompass_handler.go (1 hunks)
  • relay/relay_adaptor.go (4 hunks)
  • relay/relay_task.go (4 hunks)
  • router/relay-router.go (2 hunks)
  • router/web-router.go (1 hunks)
  • service/quota.go (1 hunks)
  • web/src/components/common/logo/CustomPassIcon.js (1 hunks)
  • web/src/components/settings/ChannelSelectorModal.js (2 hunks)
  • web/src/components/table/TaskLogsTable.js (4 hunks)
  • web/src/constants/channel.constants.js (1 hunks)
  • web/src/helpers/render.js (2 hunks)
🧰 Additional context used
🧠 Learnings (7)
router/relay-router.go (2)
Learnt from: feitianbubu
PR: QuantumNous/new-api#1228
File: router/main.go:28-36
Timestamp: 2025-06-15T12:38:11.806Z
Learning: gin.Context implements context.Context interface since Gin v1.8.0, providing the methods Deadline(), Done(), Err(), and Value(). When using Gin v1.8.0 or later, gin.Context can be passed directly to functions expecting context.Context without needing to extract c.Request.Context().
Learnt from: feitianbubu
PR: QuantumNous/new-api#1228
File: router/main.go:28-36
Timestamp: 2025-06-15T12:38:11.806Z
Learning: gin.Context implements context.Context interface since Gin v1.8.0, providing the methods Deadline(), Done(), Err(), and Value(). When using Gin v1.8.0 or later, gin.Context can be passed directly to functions expecting context.Context without needing to extract c.Request.Context().
model/task.go (1)
Learnt from: 9Ninety
PR: QuantumNous/new-api#1273
File: relay/channel/gemini/relay-gemini.go:97-116
Timestamp: 2025-06-21T03:37:41.726Z
Learning: In relay/channel/gemini/relay-gemini.go, the thinking budget calculation logic (including the MaxOutputTokens multiplication) was introduced in PR #1247. PR #1273 focused specifically on decoupling the thoughts summary feature from thinking budget settings and did not modify the existing thinking budget behavior.
relay/constant/relay_mode.go (1)
Learnt from: 9Ninety
PR: QuantumNous/new-api#1273
File: relay/channel/gemini/relay-gemini.go:97-116
Timestamp: 2025-06-21T03:37:41.726Z
Learning: In relay/channel/gemini/relay-gemini.go, the thinking budget calculation logic (including the MaxOutputTokens multiplication) was introduced in PR #1247. PR #1273 focused specifically on decoupling the thoughts summary feature from thinking budget settings and did not modify the existing thinking budget behavior.
service/quota.go (1)
Learnt from: 9Ninety
PR: QuantumNous/new-api#1273
File: relay/channel/gemini/relay-gemini.go:97-116
Timestamp: 2025-06-21T03:37:41.726Z
Learning: In relay/channel/gemini/relay-gemini.go, the thinking budget calculation logic (including the MaxOutputTokens multiplication) was introduced in PR #1247. PR #1273 focused specifically on decoupling the thoughts summary feature from thinking budget settings and did not modify the existing thinking budget behavior.
controller/relay.go (3)
Learnt from: 9Ninety
PR: QuantumNous/new-api#1273
File: relay/channel/gemini/relay-gemini.go:97-116
Timestamp: 2025-06-21T03:37:41.726Z
Learning: In relay/channel/gemini/relay-gemini.go, the thinking budget calculation logic (including the MaxOutputTokens multiplication) was introduced in PR #1247. PR #1273 focused specifically on decoupling the thoughts summary feature from thinking budget settings and did not modify the existing thinking budget behavior.
Learnt from: feitianbubu
PR: QuantumNous/new-api#1228
File: router/main.go:28-36
Timestamp: 2025-06-15T12:38:11.806Z
Learning: gin.Context implements context.Context interface since Gin v1.8.0, providing the methods Deadline(), Done(), Err(), and Value(). When using Gin v1.8.0 or later, gin.Context can be passed directly to functions expecting context.Context without needing to extract c.Request.Context().
Learnt from: feitianbubu
PR: QuantumNous/new-api#1228
File: router/main.go:28-36
Timestamp: 2025-06-15T12:38:11.806Z
Learning: gin.Context implements context.Context interface since Gin v1.8.0, providing the methods Deadline(), Done(), Err(), and Value(). When using Gin v1.8.0 or later, gin.Context can be passed directly to functions expecting context.Context without needing to extract c.Request.Context().
relay/relay_task.go (1)
Learnt from: 9Ninety
PR: QuantumNous/new-api#1273
File: relay/channel/gemini/relay-gemini.go:97-116
Timestamp: 2025-06-21T03:37:41.726Z
Learning: In relay/channel/gemini/relay-gemini.go, the thinking budget calculation logic (including the MaxOutputTokens multiplication) was introduced in PR #1247. PR #1273 focused specifically on decoupling the thoughts summary feature from thinking budget settings and did not modify the existing thinking budget behavior.
relay/channel/custompass/adaptor.go (1)
Learnt from: 9Ninety
PR: QuantumNous/new-api#1273
File: relay/channel/gemini/relay-gemini.go:97-116
Timestamp: 2025-06-21T03:37:41.726Z
Learning: In relay/channel/gemini/relay-gemini.go, the thinking budget calculation logic (including the MaxOutputTokens multiplication) was introduced in PR #1247. PR #1273 focused specifically on decoupling the thoughts summary feature from thinking budget settings and did not modify the existing thinking budget behavior.
🧬 Code Graph Analysis (8)
web/src/helpers/render.js (1)
web/src/components/common/logo/CustomPassIcon.js (1)
  • CustomPassIcon (4-28)
web/src/components/table/TaskLogsTable.js (1)
web/src/components/table/ChannelsTable.js (1)
  • renderType (73-91)
relay/common/relay_info.go (2)
common/constants.go (1)
  • ChannelTypeCustomPass (245-245)
common/logger.go (1)
  • SysError (55-58)
relay/constant/api_type.go (1)
common/constants.go (1)
  • ChannelTypeCustomPass (245-245)
service/quota.go (4)
dto/openai_response.go (1)
  • Usage (170-181)
setting/ratio_setting/model_ratio.go (1)
  • CompletionRatio (262-262)
common/logger.go (1)
  • SysLog (50-53)
common/constants.go (1)
  • QuotaPerUnit (21-21)
controller/relay.go (2)
relay/constant/relay_mode.go (2)
  • RelayModeCustomPassAPI (45-45)
  • Path2RelayMode (56-90)
relay/custompass_handler.go (1)
  • CustomPassHelper (16-60)
constant/env.go (1)
common/env.go (2)
  • GetEnvOrDefaultString (21-26)
  • GetEnvOrDefaultBool (28-38)
relay/channel/task/custompass/adaptor_test.go (2)
relay/channel/task/custompass/adaptor.go (2)
  • TaskAdaptor (21-23)
  • SubmitResponse (34-39)
dto/openai_response.go (1)
  • Usage (170-181)
🔇 Additional comments (37)
relay/channel/custompass/constants.go (1)

1-6: LGTM! Well-structured constants for CustomPass channel.

The empty ModelList is appropriate for a pass-through channel that should accept arbitrary model names, and the channel name follows proper naming conventions.

constant/task.go (1)

10-10: LGTM! Consistent task platform constant addition.

The new TaskPlatformCustomPass follows the established pattern and uses a value consistent with the channel name.

relay/constant/api_type.go (2)

37-37: LGTM! Proper API type constant addition.

The new APITypeCustomPass constant is correctly placed before the APITypeDummy marker and follows naming conventions.


102-103: LGTM! Correct channel type mapping.

The mapping correctly associates common.ChannelTypeCustomPass (value 51) with the new APITypeCustomPass constant, following the established pattern.

web/src/helpers/render.js (2)

52-52: LGTM! Proper icon import.

The import correctly references the CustomPassIcon component from the expected location.


391-392: LGTM! Correct icon case mapping.

The case for channel type 51 properly returns the CustomPassIcon with the standard iconSize parameter, following the established pattern.

web/src/constants/channel.constants.js (1)

98-98: LGTM! Complete UI channel option integration.

The new channel option properly integrates the CustomPass channel with correct value (51), appropriate color, and descriptive Chinese label for the UI.

common/constants.go (2)

245-245: LGTM! Proper channel type constant addition.

The new ChannelTypeCustomPass = 51 follows the established sequential pattern and integrates well with the existing channel type definitions.


302-302: LGTM! Correct base URL alignment maintained.

The empty string entry at index 51 correctly aligns with the new channel type constant and makes sense for a custom pass-through channel where users will configure their own base URLs.

router/web-router.go (1)

21-24: LGTM! Necessary routing update for CustomPass integration.

Adding /pass to the NoRoute handler ensures that unmatched requests under the new CustomPass router group receive proper API error responses instead of the default HTML page.

relay/channel/task/custompass/models.go (1)

1-6: LGTM! Appropriate model configuration for pass-through channel.

The empty ModelList correctly reflects that the CustomPass channel supports arbitrary model names without restrictions, which is the expected behavior for a pass-through implementation. The ChannelName is consistent with the overall naming convention.

web/src/components/common/logo/CustomPassIcon.js (1)

1-31: LGTM! Well-structured icon component.

The CustomPassIcon component follows standard React patterns with proper SVG implementation, Semi-UI integration, and props spreading for flexibility. The green checkmark design appropriately represents the pass-through concept.

router/relay-router.go (2)

7-7: LGTM! Necessary import for routing logic.

The strings import is correctly added to support the HasSuffix function used in the CustomPass routing logic.


92-108: LGTM! Well-designed CustomPass routing implementation.

The router group effectively differentiates between task submissions and regular API calls using the /submit suffix check. The middleware chain is appropriate, and the wildcard path provides the necessary flexibility for pass-through functionality. The conditional routing logic is clean and maintains consistency with existing patterns.

relay/common/relay_info.go (1)

271-276: Good validation with appropriate error handling approach.

The validation logic correctly ensures CustomPass channels have a required base_url configuration. The approach of logging the error via common.SysError rather than returning an error is appropriate since GenRelayInfo doesn't have an error return type, and the comment clearly explains that the error will be caught during subsequent URL construction.

controller/relay.go (2)

45-46: LGTM: Proper integration of CustomPass relay mode.

The new case follows the established pattern and correctly delegates to the relay.CustomPassHelper function.


74-79: Well-designed relay mode precedence logic.

The prioritization of context-based relay mode over path-based mode is a good design decision that enables special routing scenarios like CustomPass while maintaining backward compatibility through the fallback mechanism.

model/task.go (3)

31-32: Well-structured database fields for token management.

The field definitions are appropriate with proper GORM tags. The index tag on TokenId will enable efficient queries, and varchar(48) for TokenKey is suitable for typical token lengths.


58-58: Appropriate field addition with correct JSON tagging.

The omitempty tag is correctly used since the model field is optional and should not appear in JSON when not set.


91-92: Proper field initialization in constructor.

The new fields are correctly initialized from the relayInfo parameter, maintaining consistency with the existing initialization pattern.

web/src/components/table/TaskLogsTable.js (4)

16-18: Appropriate icon imports for CustomPass UI support.

The new icon imports (Sparkles, Zap, Settings) align well with the visual theme for the CustomPass platform functionality.


198-207: Excellent platform-specific rendering logic.

The special handling for CustomPass platform to display model names instead of action types is a thoughtful UX improvement. The fallback chain record.properties?.model || type || t('未知模型') provides robust error handling, and the use of optional chaining prevents runtime errors.


252-257: Consistent visual design for CustomPass platform.

The purple color scheme and Zap icon provide good visual distinction for the CustomPass platform while following the established pattern of other platform renderers.


390-390: Necessary change to enable platform-specific rendering.

Passing the full record object to renderType is required to enable the CustomPass-specific logic that displays model names. This change maintains backward compatibility since the function handles both signatures gracefully.

constant/env.go (2)

20-21: Well-named configuration variables for CustomPass functionality.

The variable names clearly indicate their purpose and follow the established naming conventions in the codebase.


44-47: Secure and well-documented environment variable initialization.

The implementation demonstrates good practices:

  • Safe default values (empty string for optional header, false for passthrough mode)
  • Clear comments explaining behavior and security implications
  • Uses established utility functions for consistent error handling
  • Conservative defaults that require explicit configuration for potentially sensitive features
relay/relay_adaptor.go (1)

14-14: LGTM - Standard adaptor factory pattern implementation.

The addition of CustomPass channel and task adaptors follows the established factory pattern used by other channel types. The import statements and switch case additions are correctly implemented.

Also applies to: 26-26, 97-98, 111-112

relay/constant/relay_mode.go (1)

44-45: Well-implemented relay mode mapping for CustomPass.

The new constants and Path2RelayCustomPass function correctly distinguish between task submission (/submit POST requests) and general API calls. The logic follows established patterns from other path mapping functions like Path2RelaySuno and Path2RelayKling.

Also applies to: 153-165

relay/channel/api_request.go (1)

255-258: Excellent defensive programming with nil checks.

The added nil checks for req.Body and requestBody prevent potential nil pointer dereferences and improve the robustness of the request handling code.

Also applies to: 282-286

service/quota.go (1)

413-422: Well-designed struct for CustomPass quota calculation.

The CustomPassQuotaInfo struct appropriately encapsulates all necessary parameters for flexible quota calculation, supporting both usage-based and fixed-price billing models.

8000
relay/channel/task/custompass/adaptor_test.go (1)

1-281: Excellent test coverage!

The test file provides comprehensive coverage for the CustomPass task adaptor with well-structured test cases covering:

  • Usage extraction from various response structures
  • Parsing and validation of usage data
  • Flexible data type handling for the response

The tests properly handle edge cases including invalid JSON, missing fields, and different data types.

web/src/components/settings/ChannelSelectorModal.js (1)

1-236: Well-executed UI refactoring!

The migration from Transfer to a paginated table with search functionality is well-implemented:

  • Proper use of forwardRef pattern for parent component interaction
  • Good UX with search highlighting, status indicators, and pagination
  • Mobile-responsive design with adaptive modal sizing
  • Clean separation of rendering logic for different cell types
relay/relay_task.go (2)

42-45: Good use of original model name for CustomPass.

Using the original model name instead of a generic "custompass_submit" ensures accurate quota calculation and logging for different models.


235-258: Clear and informative log content generation.

The function generates appropriate log messages for different billing scenarios (usage-based, per-request, or free), making it easy to understand the billing method in logs.

controller/task.go (1)

79-80: Correct integration of CustomPass platform.

The addition properly extends the platform switch to handle CustomPass task updates.

relay/channel/custompass/adaptor.go (1)

232-233: Ensure headers haven't been sent before writing response.

The response is written directly to the context after potentially complex billing operations. If any of the billing operations write to the response (e.g., error responses), this could cause a "headers already sent" error.

Consider checking if the response has already been written before calling c.Data().

relay/channel/task/custompass/adaptor.go (1)

194-201: Check headers haven't been sent in full passthrough mode.

Similar to the other adaptor, writing the response directly without checking if headers have already been sent could cause issues if any prior operation wrote to the response.

Consider adding a check or ensuring no prior operations write to the response.

Comment on lines +44 to +51
var httpResp *http.Response
if resp != nil {
httpResp = resp.(*http.Response)
if httpResp.StatusCode != http.StatusOK {
openaiErr = service.RelayErrorHandler(httpResp, false)
return openaiErr
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix unsafe type assertion that could cause panic.

The type assertion resp.(*http.Response) on line 46 could panic if resp is not actually an *http.Response. Use a safe type assertion to prevent runtime panics.

Apply this fix:

 var httpResp *http.Response
 if resp != nil {
-	httpResp = resp.(*http.Response)
+	var ok bool
+	httpResp, ok = resp.(*http.Response)
+	if !ok {
+		return service.OpenAIErrorWrapperLocal(fmt.Errorf("invalid response type"), "invalid_response_type", http.StatusInternalServerError)
+	}
 	if httpResp.StatusCode != http.StatusOK {
 		openaiErr = service.RelayErrorHandler(httpResp, false)
 		return openaiErr
 	}
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var httpResp *http.Response
if resp != nil {
httpResp = resp.(*http.Response)
if httpResp.StatusCode != http.StatusOK {
openaiErr = service.RelayErrorHandler(httpResp, false)
return openaiErr
}
}
var httpResp *http.Response
if resp != nil {
var ok bool
httpResp, ok = resp.(*http.Response)
if !ok {
return service.OpenAIErrorWrapperLocal(
fmt.Errorf("invalid response type"),
"invalid_response_type",
http.StatusInternalServerError,
)
}
if httpResp.StatusCode != http.StatusOK {
openaiErr = service.RelayErrorHandler(httpResp, false)
return openaiErr
}
}
🤖 Prompt for AI Agents
In relay/custompass_handler.go around lines 44 to 51, the code uses an unsafe
type assertion resp.(*http.Response) which can cause a panic if resp is not of
type *http.Response. Replace this with a safe type assertion using the two-value
form: assign the assertion to a variable and a boolean indicating success, then
check the boolean before proceeding. If the assertion fails, handle the error or
return appropriately to avoid panics.

Comment on lines 317 to 520
common.LogError(ctx, fmt.Sprintf("Get CustomPass Task parse data array error: %v, data: %s", err, string(baseResponse.Data)))
continue
}

common.LogInfo(ctx, fmt.Sprintf("CustomPass任务查询成功 - 渠道 #%d,返回任务数量: %d", channelId, len(taskDataList)))

// 处理每个任务的状态更新
for _, responseItem := range taskDataList {
task := taskM[responseItem.TaskId]
if task == nil {
continue
}

// 检查任务是否需要更新
if !checkCustomPassTaskNeedUpdate(task, responseItem) {
continue
}

// 记录更新前的状态
oldStatus := task.Status
oldProgress := task.Progress
common.LogInfo(ctx, fmt.Sprintf("CustomPass任务更新前 - TaskID: %s, 旧状态: %s, 旧进度: %s", task.TaskID, oldStatus, oldProgress))

// 更新任务状态
task.Status = convertCustomPassStatus(responseItem.Status)
task.Progress = responseItem.Progress

// 处理失败原因:优先使用Error字段,如果没有则从Result中提取error信息
if responseItem.Error != nil {
task.FailReason = *responseItem.Error
} else if task.Status == model.TaskStatusFailure && len(responseItem.Result) > 0 {
// 当任务失败且没有Error字段时,尝试从Result中提取error信息
for _, resultItem := range responseItem.Result {
if errorMsg, exists := resultItem["error"]; exists {
if errorStr, ok := errorMsg.(string); ok && errorStr != "" {
task.FailReason = errorStr
break
}
}
}
}

// 设置开始时间和结束时间
currentTime := time.Now().Unix()

// 如果任务从未开始状态变为进行中,设置开始时间
if task.StartTime == 0 && (task.Status == model.TaskStatusInProgress || task.Status == model.TaskStatusSuccess || task.Status == model.TaskStatusFailure) {
task.StartTime = currentTime
}

// 如果任务完成或失败,设置结束时间
if task.FinishTime == 0 && (task.Status == model.TaskStatusSuccess || task.Status == model.TaskStatusFailure) {
task.FinishTime = currentTime
}

// 如果任务失败,退回扣费
if task.Status == model.TaskStatusFailure {
// 确保失败任务的进度设置为100%,避免重复处理
task.Progress = "100%"
quota := task.Quota
if quota != 0 {
err = model.IncreaseUserQuota(task.UserId, quota, false)
if err != nil {
common.LogError(ctx, "fail to increase user quota: "+err.Error())
}
modelName := task.Properties.Model
if modelName == "" {
modelName = "自定义透传"
}
logContent := fmt.Sprintf("%s任务执行失败 %s,补偿 %s", modelName, task.TaskID, common.LogQuota(quota))
model.RecordLog(task.UserId, model.LogTypeSystem, logContent)
}
}

if task.Status == model.TaskStatusSuccess {
task.Progress = "100%"
}

// 记录更新后的状态
common.LogInfo(ctx, fmt.Sprintf("CustomPass任务更新后 - TaskID: %s, 新状态: %s, 新进度: %s", task.TaskID, task.Status, task.Progress))

// 设置任务结果数据
if len(responseItem.Result) > 0 {
task.SetData(responseItem.Result)
}

// 更新数据库前记录日志
common.LogInfo(ctx, fmt.Sprintf("CustomPass任务准备更新数据库 - TaskID: %s, ID: %d", task.TaskID, task.ID))

err = task.Update()
if err != nil {
common.SysError("UpdateCustomPassTask task error: " + err.Error())
common.LogError(ctx, fmt.Sprintf("CustomPass任务数据库更新失败 - TaskID: %s, 错误: %s", task.TaskID, err.Error()))
} else {
common.LogInfo(ctx, fmt.Sprintf("CustomPass任务数据库更新成功 - TaskID: %s, 最终状态: %s, 最终进度: %s", task.TaskID, task.Status, task.Progress))
}
}
}
return nil
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Consider refactoring this large function.

The updateCustomPassTaskAll function is over 200 lines long and handles multiple responsibilities. Consider breaking it down into smaller functions for better maintainability.

Suggested refactoring:

  • Extract task grouping logic (lines 342-360) into groupTasksByModel
  • Extract response parsing logic (lines 384-424) into parseCustomPassResponse
  • Extract task update logic (lines 428-517) into updateSingleCustomPassTask
🤖 Prompt for AI Agents
In controller/task.go from lines 317 to 520, the updateCustomPassTaskAll
function is too large and handles multiple responsibilities, making it hard to
maintain. Refactor by extracting the task grouping logic (lines 342-360) into a
new function named groupTasksByModel, the response parsing logic (lines 384-424)
into parseCustomPassResponse, and the task update logic (lines 428-517) into
updateSingleCustomPassTask. Replace the original code blocks with calls to these
new functions to improve readability and maintainability.

Comment on lines +373 to +383
resp, err := adaptor.FetchTask(*channel.BaseURL, channel.Key, requestBody)
if err != nil {
common.SysError(fmt.Sprintf("Get CustomPass Task Do req error: %v", err))
continue
}
if resp.StatusCode != http.StatusOK {
common.LogError(ctx, fmt.Sprintf("Get CustomPass Task status code: %d", resp.StatusCode))
continue
}
defer resp.Body.Close()

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix potential resource leak with defer inside loop.

The defer resp.Body.Close() on line 383 is inside a loop. If the loop continues after an error, the defer won't execute until the function returns, potentially causing resource leaks.

Move the response handling into a separate function or use immediate cleanup:

 resp, err := adaptor.FetchTask(*channel.BaseURL, channel.Key, requestBody)
 if err != nil {
 	common.SysError(fmt.Sprintf("Get CustomPass Task Do req error: %v", err))
 	continue
 }
 if resp.StatusCode != http.StatusOK {
 	common.LogError(ctx, fmt.Sprintf("Get CustomPass Task status code: %d", resp.StatusCode))
+	resp.Body.Close()
 	continue
 }
-defer resp.Body.Close()

 responseBody, err := io.ReadAll(resp.Body)
+resp.Body.Close()
 if err != nil {
 	common.SysError(fmt.Sprintf("Get CustomPass Task parse body error: %v", err))
 	continue
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
resp, err := adaptor.FetchTask(*channel.BaseURL, channel.Key, requestBody)
if err != nil {
common.SysError(fmt.Sprintf("Get CustomPass Task Do req error: %v", err))
continue
}
if resp.StatusCode != http.StatusOK {
common.LogError(ctx, fmt.Sprintf("Get CustomPass Task status code: %d", resp.StatusCode))
continue
}
defer resp.Body.Close()
resp, err := adaptor.FetchTask(*channel.BaseURL, channel.Key, requestBody)
if err != nil {
common.SysError(fmt.Sprintf("Get CustomPass Task Do req error: %v", err))
continue
}
if resp.StatusCode != http.StatusOK {
common.LogError(ctx, fmt.Sprintf("Get CustomPass Task status code: %d", resp.StatusCode))
resp.Body.Close()
continue
}
responseBody, err := io.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
common.SysError(fmt.Sprintf("Get CustomPass Task parse body error: %v", err))
continue
}
🤖 Prompt for AI Agents
In controller/task.go around lines 373 to 383, the defer statement for
resp.Body.Close() is inside a loop, which delays closing the response body until
the function returns and can cause resource leaks. To fix this, remove the defer
from inside the loop and instead close resp.Body immediately after finishing
processing each response within the loop or refactor the response handling into
a separate function where defer can be used safely.

Comment on lines +466 to +478
// extractModelAndActionFromModelAction 从 model_action 中分离出 model 和 action
// 例如:gpt-4/chat -> model: gpt-4, action: chat
func extractModelAndActionFromModelAction(modelAction string) (string, string) {
// 查找最后一个 "/" 的位置,以支持 model 名称中包含 "/"
lastSlashIndex := strings.LastIndex(modelAction, "/")
if lastSlashIndex == -1 || lastSlashIndex == len(modelAction)-1 {
return "", ""
}

model := modelAction[:lastSlashIndex]
action := modelAction[lastSlashIndex+1:]
return model, action
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove unused function.

The extractModelAndActionFromModelAction function is defined but never used in this file.

Consider removing this dead code to improve maintainability.

🤖 Prompt for AI Agents
In relay/channel/task/custompass/adaptor.go around lines 466 to 478, the
function extractModelAndActionFromModelAction is defined but never used in the
file. Remove this entire function to eliminate dead code and improve
maintainability.

simplty added 2 commits July 1, 2025 17:34
This commit refactors the CustomPass channel implementation to improve task handling and response parsing logic.

Key changes include:

- **Simplified Data Structure:** The `Result` field from the `taskDataList` struct in `updateCustomPassTaskAll` has been removed, streamlining the data model.
- **Consolidated Error Handling:** Error handling logic has been simplified to prioritize the `Error` field for failure reasons.
- **Direct Data Assignment:** The `task.SetData` method now directly saves the entire API response item, providing more comprehensive data for tasks.
- **Refined Update Condition:** The `checkCustomPassTaskNeedUpdate` function is updated to accurately reflect changes in the `Error` field for task updates.
- **Adaptor Implementation for Tasks:** A new `TaskAdaptor` type is introduced in `relay/channel/custompass/adaptor.go` to handle task-specific operations.
- **New Task Structures:** Introduced `SubmitRequest`, `SubmitResponse`, `TaskQueryRequest`, `TaskQueryResponse`, and `TaskInfo` for better task data management.
- **Enhanced Request/Response Handling:** Implemented `ValidateRequestAndSetAction`, `BuildRequestURL`, `BuildRequestHeader`, `BuildRequestBody`, `DoRequest`, and `DoResponse` methods for robust task request and response processing.
- **Full Passthrough Mode:** Added support for `constant.CustomPassFullPassthrough` to directly pass through responses when enabled, with added logic to extract usage information.
- **Conditional Action Handling:** Distinguishes between submit actions and other task actions for appropriate response processing.
- **Task ID Extraction:** Improved logic for extracting `task_id` from various `SubmitResponse` data formats.
- **Task Fetching:** Implemented `FetchTask` method for querying task statuses.
- **Model and Action Extraction:** Added `extractModelAndActionFromModelAction` helper function.
- **Usage Extraction:** Improved `extractUsageFromResponse` for parsing usage data from different response structures and added `parseUsageFromInterface` for robust parsing.
- **Logging:** Enhanced logging for clarity during task operations and usage extraction.
Introduces new environment variables to allow customization of status mappings for CustomPass tasks. This enhances flexibility by enabling users to define how different external task statuses translate to internal task statuses (Success, Failure, InProgress).

The following environment variables are added:
- `CUSTOM_PASS_STATUS_SUCCESS`: Maps external success statuses.
- `CUSTOM_PASS_STATUS_FAILURE`: Maps external failure statuses.
- `CUSTOM_PASS_STATUS_IN_PROGRESS`: Maps external in-progress statuses.

These variables support comma-separated values for mapping multiple external statuses to a single internal status. If not provided, default values are used.

This change also refactors the `convertCustomPassStatus` function to utilize these new configurations and introduces a helper function `isStatusMatch` for more robust status checking.
Copy link
Contributor
@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6c08ce7 and cd2f99e.

📒 Files selected for processing (4)
  • .env.example (1 hunks)
  • constant/env.go (2 hunks)
  • controller/task.go (3 hunks)
  • relay/channel/custompass/adaptor.go (1 hunks)
✅ Files skipped from review due to trivial changes (1)
  • .env.example
🚧 Files skipped from review as they are similar to previous changes (1)
  • constant/env.go
🔇 Additional comments (1)
relay/channel/custompass/adaptor.go (1)

71-117: Well-implemented request validation with proper error handling.

The method correctly handles both GET and POST requests, validates the model format, and stores request data in the context for later use.

Comment on lines +561 to +568
if responseData.Error != nil && task.FailReason != *responseData.Error {
return true
}

// 如果错误信息发生变化,需要更新
if responseData.Error != nil && task.FailReason != *responseData.Error {
return true
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Remove duplicate error comparison check.

The same condition checking if responseData.Error has changed is repeated twice.

Remove the duplicate block:

 	// 如果错误信息发生变化,需要更新
 	if responseData.Error != nil && task.FailReason != *responseData.Error {
 		return true
 	}
 
-	// 如果错误信息发生变化,需要更新
-	if responseData.Error != nil && task.FailReason != *responseData.Error {
-		return true
-	}
-
 	// 其他情况不需要更新
 	return false
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if responseData.Error != nil && task.FailReason != *responseData.Error {
return true
}
// 如果错误信息发生变化,需要更新
if responseData.Error != nil && task.FailReason != *responseData.Error {
return true
}
if responseData.Error != nil && task.FailReason != *responseData.Error {
return true
}
// 其他情况不需要更新
return false
🤖 Prompt for AI Agents
In controller/task.go around lines 561 to 568, there is a duplicate conditional
block checking if responseData.Error is not nil and differs from
task.FailReason. Remove the repeated block so that this condition is only
checked once to avoid redundant code.

Comment on lines +203 to +209
// 如果action包含"submit"关键词,则认为是提交任务
isSubmitAction := a.isSubmitAction(info.Action, responseBody)

if !isSubmitAction {
// 普通action,直接返回上游响应
c.Data(resp.StatusCode, resp.Header.Get("Content-Type"), responseBody)
return "", responseBody, nil
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

⚠️ Potential issue

Remove unused parameter and update misleading comment.

The comment suggests checking if the response contains "submit", but the isSubmitAction method only checks the action parameter. The responseBody parameter is never used.

Update the implementation:

 	// 判断是否为提交任务的action
-	// 如果action包含"submit"关键词,则认为是提交任务
-	isSubmitAction := a.isSubmitAction(info.Action, responseBody)
+	// 如果action名称包含"submit"关键词,则认为是提交任务
+	isSubmitAction := a.isSubmitAction(info.Action)

 // isSubmitAction 判断是否为提交任务的action
-func (a *TaskAdaptor) isSubmitAction(action string, responseBody []byte) bool {
+func (a *TaskAdaptor) isSubmitAction(action string) bool {

Also applies to: 299-309

🤖 Prompt for AI Agents
In relay/channel/custompass/adaptor.go around lines 203 to 209 and also lines
299 to 309, the isSubmitAction method is called with two parameters but only
uses the action parameter, making the responseBody parameter unused and the
comment misleading. Remove the responseBody parameter from the isSubmitAction
method signature and its calls, and update the comment to accurately reflect
that only the action parameter is checked for the "submit" keyword.

Comment on lines +467 to +477
func extractModelAndActionFromModelAction(modelAction string) (string, string) {
// 查找最后一个 "/" 的位置,以支持 model 名称中包含 "/"
lastSlashIndex := strings.LastIndex(modelAction, "/")
if lastSlashIndex == -1 || lastSlashIndex == len(modelAction)-1 {
return "", ""
}

model := modelAction[:lastSlashIndex]
action := modelAction[lastSlashIndex+1:]
return model, action
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Remove unused helper function.

The function extractModelAndActionFromModelAction is defined but never used in this file.

Remove the unused function to improve code maintainability:

-// extractModelAndActionFromModelAction 从 model_action 中分离出 model 和 action
-// 例如:gpt-4/chat -> model: gpt-4, action: chat
-func extractModelAndActionFromModelAction(modelAction string) (string, string) {
-	// 查找最后一个 "/" 的位置,以支持 model 名称中包含 "/"
-	lastSlashIndex := strings.LastIndex(modelAction, "/")
-	if lastSlashIndex == -1 || lastSlashIndex == len(modelAction)-1 {
-		return "", ""
-	}
-
-	model := modelAction[:lastSlashIndex]
-	action := modelAction[lastSlashIndex+1:]
-	return model, action
-}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func extractModelAndActionFromModelAction(modelAction string) (string, string) {
// 查找最后一个 "/" 的位置,以支持 model 名称中包含 "/"
lastSlashIndex := strings.LastIndex(modelAction, "/")
if lastSlashIndex == -1 || lastSlashIndex == len(modelAction)-1 {
return "", ""
}
model := modelAction[:lastSlashIndex]
action := modelAction[lastSlashIndex+1:]
return model, action
}
🤖 Prompt for AI Agents
In relay/channel/custompass/adaptor.go around lines 467 to 477, the function
extractModelAndActionFromModelAction is defined but never used in the file.
Remove the entire function definition to clean up unused code and improve
maintainability.

simplty added 4 commits July 1, 2025 20:24
- Handle auto group resolution by updating context with actual group
- Set group in context for both auto and regular group scenarios
- Add debug logging for auto group resolution tracking
Add explicit return statements to ensure relayMode is properly returned in all code paths, preventing potential undefined behavior.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants
0