-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
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
base: main
Are you sure you want to change the base?
Conversation
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.
WalkthroughThis 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 Changes
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
Poem
📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
✨ Finishing Touches
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed 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)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this 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
andstrings.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
⛔ 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 theAPITypeDummy
marker and follows naming conventions.
102-103
: LGTM! Correct channel type mapping.The mapping correctly associates
common.ChannelTypeCustomPass
(value 51) with the newAPITypeCustomPass
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. TheChannelName
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 theHasSuffix
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 viacommon.SysError
rather than returning an error is appropriate sinceGenRelayInfo
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 onTokenId
will enable efficient queries, andvarchar(48)
forTokenKey
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 torenderType
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 likePath2RelaySuno
andPath2RelayKling
.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
andrequestBody
prevent potential nil pointer dereferences and improve the robustness of the request handling code.Also applies to: 282-286
8000service/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.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.
var httpResp *http.Response | ||
if resp != nil { | ||
httpResp = resp.(*http.Response) | ||
if httpResp.StatusCode != http.StatusOK { | ||
openaiErr = service.RelayErrorHandler(httpResp, false) | ||
return openaiErr | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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.
controller/task.go
Outdated
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 | ||
} |
There was a problem hiding this comment.
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.
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() | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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.
// 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 | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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.
There was a problem hiding this 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
📒 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.
if responseData.Error != nil && task.FailReason != *responseData.Error { | ||
return true | ||
} | ||
|
||
// 如果错误信息发生变化,需要更新 | ||
if responseData.Error != nil && task.FailReason != *responseData.Error { | ||
return true | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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.
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.
// 如果action包含"submit"关键词,则认为是提交任务 | ||
isSubmitAction := a.isSubmitAction(info.Action, responseBody) | ||
|
||
if !isSubmitAction { | ||
// 普通action,直接返回上游响应 | ||
c.Data(resp.StatusCode, resp.Header.Get("Content-Type"), responseBody) | ||
return "", responseBody, nil |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
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.
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 | ||
} |
There was a problem hiding this comment.
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.
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.
- 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.
🚀 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
CustomPass
channel type (ID: 51) supporting custom API pass-through.CUSTOM_PASS_HEADER_KEY
.🎨 Frontend Features
CustomPass
icon component for visual identification.⚙️ Configuration Options
CUSTOM_PASS_HEADER_KEY
- Configures the custom header key for upstream requests.submit
tasks).🔄 Technical Implementation
Core Components
CustomPass Adapter (
relay/channel/custompass/adaptor.go
)Pass-Through Handler (
relay/custompass_handler.go
)Task Scheduling Support (
relay/channel/task/custompass/
)Routing Configuration
/pass/{model}
route for pass-through requests.📁 File Change Statistics
Major Changed Files
🔒 Security Considerations
🎯 Use Cases
⚙️ 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
submit
tasks.submit
tasksPass-Through Mode Comparison
submit
Task Returntask_id
Future Feature Plans
🚦 Deployment Instructions
CUSTOM_PASS_HEADER_KEY
as needed.CustomPass
type channel in the management interface.📊 System Flowchart
CustomPass Processing Flow
CustomPass System Architecture Diagram
📝 API Examples
🔄 Task API Example (Asynchronous Processing)
1. Task Submission
Request Example
Upstream API Response Example
System Response to Client
Default Mode (Returns only
task_id
)Full Pass-Through Mode (Returns upstream full response)
Billing Processing
2. Task Query
Request Example
Upstream API Response Example
3. Task Error Handling
Task Submission Failure
Task Query Failure
⚡ Regular API Example (Synchronous Processing)
1. Text Generation API
Request Example
Upstream API Response Example (with Usage)
Billing Processing
2. Image Generation API
Request Example
Upstream API Response Example (Per-Call Billing)
Billing Processing
3. Speech-to-Text API
Request Example
Upstream API Response Example
Billing Processing
4. Error Handling
Upstream API Error
System Error Handling
📊 Logging Instructions
Cases Where Consumption Logs Are NOT Recorded
In the following situations, the system will not write requests to the consumption log:
Cases Where Consumption Logs ARE 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:
Task Success Judgment Conditions
completed
.result
field contains valid result content.error
field is empty or null.Task Failure Judgment Conditions
error
orfailed
.error
field contains an error description.result
array contains error information.Failure Compensation Mechanism
When a task fails, the system automatically compensates for the cost:
💰 CustomPass Billing Method Details
Billing Priority
CustomPass adopts a three-tier billing priority:
1. Usage-Based Billing
API Response Example with Usage
Usage Billing Logic
Billing Log Example
2. Per-Call Billing
API Response Example without Usage
Per-Call Billing Logic
Billing Log Example
3. Zero Cost
Zero Cost Conditions
Zero Cost Log Example
Billing Configuration Example
Model Price Configuration
Group Multiplier Configuration
Summary by CodeRabbit
New Features
/pass
for task submission and general API calls.User Interface
Bug Fixes
Documentation
Tests