Skip to content

feat(批处理支持): 添加 Batch API 支持,包括文件上传、任务创建、状态查询和结果下载 - #96

Closed
devhunk wants to merge 4 commits into
Eric-Terminal:mainfrom
devhunk:main
Closed

feat(批处理支持): 添加 Batch API 支持,包括文件上传、任务创建、状态查询和结果下载#96
devhunk wants to merge 4 commits into
Eric-Terminal:mainfrom
devhunk:main

Conversation

@devhunk

@devhunk devhunk commented Jul 5, 2026

Copy link
Copy Markdown
Contributor

改动内容

  • 扩展基础协议:在 APIAdapterSupport.swift 中扩展了 APIAdapter 协议,新增了与 Batch 生命周期相关的 4 个标准接口(包含文件上传、创建批处理、查询状态、下载结果)。
  • 新增数据模型
    • 新增 BatchModels.swift,定义了应用级的 BatchJobBatchJobStatus 以及 JSONL 请求响应相关的 BatchRequestItemBatchResponseItem
    • OpenAIAdapterModels.swift 中补充了针对 OpenAI 响应的 OpenAIFileUploadResponseOpenAIBatchJobResponse
  • 提供商适配器实现:新增 OpenAIAdapterBatchSupport.swift,基于 OpenAI Batch API 的标准完全实现了上传 JSONL、创建 Batch 任务、状态解析与结果下载的逻辑。
  • 状态持久化存储:新增 BatchJobStore.swift,采用基于本地 JSON (jobs.json) 的轻量级存储,管理正在运行和已完成的 Batch 任务状态,确保应用重启不丢失状态。
  • 核心业务管理器:新增 BatchService.swift。该服务负责将用户的常规聊天消息 ([ChatMessage]) 自动打包为临时 JSONL 格式发送上传,发起 Batch 调用后使用异步任务静默轮询更新状态,并在任务最终完成(或失败)时自动下载解析生成结果。

验证

  • 检查新增加的核心文件代码在 Xcode 中无编译错误。
  • 模拟提交单条或多条独立消息给 BatchService.shared.submitBatch(),验证能否正确生成符合 {"custom_id":..., "method": "POST", "url": "/v1/chat/completions", "body": ...} 规范的临时 JSONL 文件。
  • 配合 OpenAI 真实 API Key 或兼容服务端点,验证后台轮询服务能在创建 Batch 任务后正常挂起并在 API 端任务完成后成功触发结果下载。
  • 本地终止应用重启后,确认之前为 in_progress 状态的 Batch 任务可以通过 BatchJobStore 被正确加载并恢复轮询。

截图或录屏

  • (暂无 UI 交互,主要是底层架构能力补充)

CLA

  • I have read the CLA Document and I hereby sign the CLA.

Summary by Sourcery

Add end-to-end batch processing support, including protocol extensions, models, service layer, OpenAI adapter integration, and persistence of batch job state.

New Features:

  • Add Batch API support to the core adapter protocol for file upload, batch creation, status queries, and result downloads.
  • Introduce batch-related domain models for jobs, statuses, and JSONL request/response items.
  • Add a BatchService to orchestrate batch job submission, polling, and result processing based on chat messages.
  • Implement OpenAI Batch API support in the OpenAI adapter, including file upload, batch job management, and result download.

Enhancements:

  • Persist batch job state locally via a lightweight JSON-backed BatchJobStore to survive app restarts.

@sourcery-ai

sourcery-ai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds end-to-end Batch API support for OpenAI-based providers, including protocol extensions, OpenAI adapter implementations, batch lifecycle management service, and a JSON-file-based store for batch job persistence and result handling.

Sequence diagram for BatchService submitBatch and polling lifecycle

sequenceDiagram
    actor User
    participant BatchService
    participant ChatService
    participant APIAdapter
    participant BatchJobStore

    User->>BatchService: submitBatch(messages, model, sessionID)
    BatchService->>ChatService: adapters[model.provider.apiFormat]
    BatchService->>APIAdapter: buildChatRequest(for, commonPayload, messages, tools, audioAttachments, imageAttachments, fileAttachments)
    BatchService->>APIAdapter: buildBatchFileUploadRequest(for, jsonlData, purpose)
    BatchService->>ChatService: fetchData(for: uploadReq, provider)
    ChatService-->>BatchService: uploadData
    BatchService->>APIAdapter: parseBatchFileUploadResponse(data)
    BatchService->>APIAdapter: buildBatchCreateRequest(for, fileId, endpoint, metadata)
    BatchService->>ChatService: fetchData(for: createReq, provider)
    ChatService-->>BatchService: createData
    BatchService->>APIAdapter: parseBatchCreateResponse(data)
    BatchService->>BatchJobStore: saveJob(newJob)
    BatchService->>BatchService: startPolling(for: newJob)

    loop periodic polling
        BatchService->>BatchJobStore: getJob(id)
        BatchService->>ChatService: providers
        BatchService->>APIAdapter: buildBatchStatusRequest(for, batchId)
        BatchService->>ChatService: fetchData(for: statusReq, provider)
        ChatService-->>BatchService: statusData
        BatchService->>APIAdapter: parseBatchStatusResponse(data)
        BatchService->>BatchJobStore: saveJob(updatedJob)
        alt job completed
            BatchService->>APIAdapter: buildBatchResultDownloadRequest(for, fileId)
            BatchService->>ChatService: fetchData(for: resultReq, provider)
            ChatService-->>BatchService: resultData
            BatchService->>APIAdapter: parseBatchResultDownloadResponse(data)
            BatchService->>APIAdapter: parseResponse(data)
        end
    end
Loading

File-Level Changes

Change Details Files
Extend the generic APIAdapter protocol to support Batch lifecycle operations with default unsupported implementations.
  • Add methods to build and parse requests for batch file upload, batch creation, batch status, and batch result download
  • Provide default implementations that return nil for request builders and throw localized errors for parsers when a provider does not support Batch APIs
ETOSCore/ETOSCore/Providers/Adapters/APIAdapterSupport.swift
Introduce shared Batch domain models for jobs and JSONL request/response payloads.
  • Define BatchJobStatus enum matching OpenAI batch status strings
  • Define BatchJob struct carrying provider/model IDs, timestamps, file IDs and endpoint metadata
  • Define BatchRequestItem, BatchResponseItem, and BatchResponsePayload structs with coding keys matching OpenAI JSONL format
ETOSCore/ETOSCore/Batch/BatchModels.swift
Implement a simple JSON-file-backed BatchJobStore to persist and retrieve batch job state across app restarts.
  • Maintain an in-memory dictionary of BatchJob keyed by id with a serial dispatch queue for thread safety
  • Load jobs from jobs.json in a BatchJobs subdirectory under the app documents directory at initialization
  • Persist job mutations by encoding the dictionary to JSON and atomically writing to disk
ETOSCore/ETOSCore/Batch/BatchJobStore.swift
Add BatchService to orchestrate batch submission from chat messages, periodic status polling, and result download/decoding.
  • On init, load existing jobs from BatchJobStore and restart polling for unfinished jobs
  • Provide submitBatch(messages:model:sessionID:) to serialize messages into OpenAI-compatible JSONL, upload file, create batch job, normalize job metadata, persist, and start polling
  • Implement startPolling to schedule per-job async tasks that query status roughly every minute and stop when terminal states are reached
  • Implement checkStatus to rebuild RunnableModel from stored provider/model IDs, refresh job status, persist updates, and trigger result download on completion
  • Implement downloadAndProcessResults to fetch JSONL output file, decode each line into BatchResponseItem, convert bodies to Data, and reuse adapter.parseResponse to reconstruct ChatMessage results
ETOSCore/ETOSCore/Batch/BatchService.swift
Implement OpenAI-specific Batch support on top of OpenAIAdapter, mapping OpenAI Batch and Files APIs to the new protocol methods.
  • Add OpenAIFileUploadResponse and OpenAIBatchJobResponse decodable structs to model OpenAI API responses for file upload and batch jobs
  • Implement buildBatchFileUploadRequest as a multipart/form-data POST to /files with purpose and JSONL file parts, including auth and header overrides
  • Implement buildBatchCreateRequest as a JSON POST to /batches with input_file_id, endpoint, completion_window, and optional metadata
  • Implement buildBatchStatusRequest as a GET to /batches/{id} and buildBatchResultDownloadRequest as a GET to /files/{fileId}/content with appropriate timeouts and headers
  • Implement parseBatchFileUploadResponse and parseBatchResultDownloadResponse to return file id and raw data respectively
  • Implement shared parseBatchStatus(from:) to decode OpenAIBatchJobResponse, map status strings to BatchJobStatus, and construct a BatchJob with placeholder provider/model IDs and timestamps to be corrected by the caller
ETOSCore/ETOSCore/Providers/Adapters/OpenAIAdapterModels.swift
ETOSCore/ETOSCore/Providers/Adapters/OpenAIAdapterBatchSupport.swift

Possibly linked issues

  • #[App反馈][WATCHOS] 建议支持 batch 批量调用模型: 该PR在核心实现完整Batch API和OpenAI集成,为watchOS提供用户期望的批量调用基础能力。

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Hey - I've found 2 issues, and left some high level feedback:

  • In BatchService.submitBatch, failing to build the request body for a specific ChatMessage simply skips that message; consider surfacing this as an error or at least logging which messages were dropped to avoid silent data loss.
  • The OpenAIAdapter.parseBatchStatus helper returns BatchJob objects with placeholder providerID and modelID that are corrected later by callers; you may want to pass these in as parameters or avoid embedding them at all to reduce the risk of accidentally persisting placeholder values.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `BatchService.submitBatch`, failing to build the request body for a specific `ChatMessage` simply skips that message; consider surfacing this as an error or at least logging which messages were dropped to avoid silent data loss.
- The `OpenAIAdapter.parseBatchStatus` helper returns `BatchJob` objects with placeholder `providerID` and `modelID` that are corrected later by callers; you may want to pass these in as parameters or avoid embedding them at all to reduce the risk of accidentally persisting placeholder values.

## Individual Comments

### Comment 1
<location path="ETOSCore/ETOSCore/Batch/BatchService.swift" line_range="43-52" />
<code_context>
+        for (index, msg) in messages.enumerated() {
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Silently skipping messages that fail request-body construction can lead to partial, surprising batch submissions.

When `buildChatRequest` returns `nil` or the body can't be converted to `JSONValue`, the loop just `continue`s, producing fewer `BatchRequestItem`s than input messages and giving no indication of which ones were dropped. Prefer failing fast (throwing) when any message cannot be transformed, or at minimum log skipped messages to make the behavior observable and predictable.

Suggested implementation:

```
        // 1. 构建 BatchRequestItems
        var batchItems: [BatchRequestItem] = []
        for (index, msg) in messages.enumerated() {
            let customId = "req-\(sessionID.uuidString)-\(msg.id.uuidString)-\(index)"

            // 构造请求体:由于 APIAdapter 没有暴露暴露纯 JSON 构造,
            // 我们可以利用 buildChatRequest 并截获其 httpBody
            let request = adapter.buildChatRequest(
                for: model,
                commonPayload: [:],
                messages: [msg],
                tools: nil,
                audioAttachments: [:],

```

```
            guard let httpBody = request.httpBody else {
                throw NSError(
                    domain: "BatchService",
                    code: -2,
                    userInfo: [
                        NSLocalizedDescriptionKey: "无法为消息 \(msg.id) 构造请求体,Batch 提交已中止。"
                    ]
                )
            }

```

```
            guard
                let jsonObject = try? JSONSerialization.jsonObject(with: httpBody),
                let jsonBody = JSONValue(jsonObject)
            else {
                throw NSError(
                    domain: "BatchService",
                    code: -3,
                    userInfo: [
                        NSLocalizedDescriptionKey: "无法将消息 \(msg.id) 的请求体转换为 JSON,Batch 提交已中止。"
                    ]
                )
            }

```

1. If the file defines a custom error type for batch operations (e.g. `BatchError`), prefer throwing that instead of `NSError`, preserving the fail-fast semantics but aligning with existing error handling conventions.
2. Consider adding structured logging where your logging infrastructure exists (e.g. before throwing, log the `sessionID`, `index`, and `msg.id`) so operators can correlate failures with specific messages.
3. If callers rely on partial success, you may want to wrap this function or introduce an alternative API that returns detailed per-message failure information instead of throwing on the first error.
</issue_to_address>

### Comment 2
<location path="ETOSCore/ETOSCore/Batch/BatchJobStore.swift" line_range="21-23" />
<code_context>
+    
+    private var fileURL: URL {
+        let docsDir = Persistence.documentsDirectory
+        let batchDir = docsDir.appendingPathComponent("BatchJobs")
+        if !FileManager.default.fileExists(atPath: batchDir.path) {
+            try? FileManager.default.createDirectory(at: batchDir, withIntermediateDirectories: true)
+        }
+        return batchDir.appendingPathComponent("jobs.json")
</code_context>
<issue_to_address>
**issue (bug_risk):** Directory creation failures are silently ignored, which can cause later persistence operations to fail in non-obvious ways.

If `createDirectory` fails (e.g., permissions, disk issues, invalid path), the error is suppressed and later reads/writes to `jobs.json` will likely fail or misbehave. Please handle this failure explicitly (e.g., log via `logger` or propagate the error) so storage issues are visible and diagnosable rather than silently ignored.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +43 to +52
for (index, msg) in messages.enumerated() {
let customId = "req-\(sessionID.uuidString)-\(msg.id.uuidString)-\(index)"

// 构造请求体:由于 APIAdapter 没有暴露暴露纯 JSON 构造,
// 我们可以利用 buildChatRequest 并截获其 httpBody
let request = adapter.buildChatRequest(
for: model,
commonPayload: [:],
messages: [msg],
tools: nil,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (bug_risk): Silently skipping messages that fail request-body construction can lead to partial, surprising batch submissions.

When buildChatRequest returns nil or the body can't be converted to JSONValue, the loop just continues, producing fewer BatchRequestItems than input messages and giving no indication of which ones were dropped. Prefer failing fast (throwing) when any message cannot be transformed, or at minimum log skipped messages to make the behavior observable and predictable.

Suggested implementation:

        // 1. 构建 BatchRequestItems
        var batchItems: [BatchRequestItem] = []
        for (index, msg) in messages.enumerated() {
            let customId = "req-\(sessionID.uuidString)-\(msg.id.uuidString)-\(index)"

            // 构造请求体:由于 APIAdapter 没有暴露暴露纯 JSON 构造,
            // 我们可以利用 buildChatRequest 并截获其 httpBody
            let request = adapter.buildChatRequest(
                for: model,
                commonPayload: [:],
                messages: [msg],
                tools: nil,
                audioAttachments: [:],

            guard let httpBody = request.httpBody else {
                throw NSError(
                    domain: "BatchService",
                    code: -2,
                    userInfo: [
                        NSLocalizedDescriptionKey: "无法为消息 \(msg.id) 构造请求体,Batch 提交已中止。"
                    ]
                )
            }

            guard
                let jsonObject = try? JSONSerialization.jsonObject(with: httpBody),
                let jsonBody = JSONValue(jsonObject)
            else {
                throw NSError(
                    domain: "BatchService",
                    code: -3,
                    userInfo: [
                        NSLocalizedDescriptionKey: "无法将消息 \(msg.id) 的请求体转换为 JSON,Batch 提交已中止。"
                    ]
                )
            }

  1. If the file defines a custom error type for batch operations (e.g. BatchError), prefer throwing that instead of NSError, preserving the fail-fast semantics but aligning with existing error handling conventions.
  2. Consider adding structured logging where your logging infrastructure exists (e.g. before throwing, log the sessionID, index, and msg.id) so operators can correlate failures with specific messages.
  3. If callers rely on partial success, you may want to wrap this function or introduce an alternative API that returns detailed per-message failure information instead of throwing on the first error.

Comment on lines +21 to +23
let batchDir = docsDir.appendingPathComponent("BatchJobs")
if !FileManager.default.fileExists(atPath: batchDir.path) {
try? FileManager.default.createDirectory(at: batchDir, withIntermediateDirectories: true)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

issue (bug_risk): Directory creation failures are silently ignored, which can cause later persistence operations to fail in non-obvious ways.

If createDirectory fails (e.g., permissions, disk issues, invalid path), the error is suppressed and later reads/writes to jobs.json will likely fail or misbehave. Please handle this failure explicitly (e.g., log via logger or propagate the error) so storage issues are visible and diagnosable rather than silently ignored.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Code Review

This pull request introduces a Batch API processing framework for ETOS LLM Studio, enabling local persistence, lifecycle management, and OpenAI-specific adapter support for batch jobs. The review feedback highlights critical improvements for robustness and extensibility, including: preventing background polling task leaks when jobs are deleted, utilizing the adapter's download response parser for future-proofing, adding error logging for missing configurations or failed batch items, and correctly parsing and using actual API timestamps instead of hardcoding the current date.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +141 to +144
let currentJob = BatchJobStore.shared.getJob(id: job.id)
if let status = currentJob?.status, status == .completed || status == .failed || status == .expired || status == .cancelled {
break
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

high

在轮询任务状态时,如果任务在本地 BatchJobStore 中被删除(例如用户删除了该任务),BatchJobStore.shared.getJob(id:) 将返回 nil。此时 currentJob?.statusnil,导致 if let status = ... 条件不成立,循环不会退出。这会导致后台轮询任务无限期地继续运行,造成后台任务泄漏。

建议使用 guard let 提前解包,如果任务已被删除则直接退出循环。

                    guard let currentJob = BatchJobStore.shared.getJob(id: job.id) else {
                        break
                    }
                    let status = currentJob.status
                    if status == .completed || status == .failed || status == .expired || status == .cancelled {
                        break
                    }

Comment on lines +195 to +196
let data = try await ChatService.shared.fetchData(for: req, provider: model.provider)
let jsonlString = String(data: data, encoding: .utf8) ?? ""

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

在下载并处理 Batch 结果时,代码直接将下载的原始数据转换为字符串,而没有调用适配器的 parseBatchResultDownloadResponse(data:) 方法。

虽然 OpenAI 适配器目前直接返回原始数据,但其他适配器(或未来的自定义适配器)可能会对下载的数据进行解密、解压或格式转换。为了保证协议的完整性和未来的可扩展性,应当调用该方法处理下载的数据。

Suggested change
let data = try await ChatService.shared.fetchData(for: req, provider: model.provider)
let jsonlString = String(data: data, encoding: .utf8) ?? ""
let rawData = try await ChatService.shared.fetchData(for: req, provider: model.provider)
let data = try adapter.parseBatchResultDownloadResponse(data: rawData)
let jsonlString = String(data: data, encoding: .utf8) ?? ""

Comment on lines +161 to +165
let providers = ChatService.shared.providers
guard let provider = providers.first(where: { $0.id == job.providerID }),
let modelDef = provider.models.first(where: { $0.id.uuidString == job.modelID }) else {
return
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

checkStatus 中,如果找不到对应的提供商或模型配置,方法会静默返回。这会导致轮询任务在后台继续运行(因为状态没有更新为终态),但实际上它什么也做不了,且没有任何日志输出,极难排查问题。

建议在返回前记录一条错误日志,以便于调试和维护。

Suggested change
let providers = ChatService.shared.providers
guard let provider = providers.first(where: { $0.id == job.providerID }),
let modelDef = provider.models.first(where: { $0.id.uuidString == job.modelID }) else {
return
}
let providers = ChatService.shared.providers
guard let provider = providers.first(where: { $0.id == job.providerID }),
let modelDef = provider.models.first(where: { $0.id.uuidString == job.modelID }) else {
logger.error("无法为任务 \(job.id) 找到对应的提供商或模型配置 (ProviderID: \(job.providerID), ModelID: \(job.modelID))。")
return
}

Comment on lines +203 to +210
let responseItem = try JSONDecoder().decode(BatchResponseItem.self, from: lineData)
if let payloadBody = responseItem.response?.body {
// 转回 Data 再丢给原先的 adapter 解析
if let rawData = try? JSONEncoder().encode(payloadBody) {
let msg = try adapter.parseResponse(data: rawData)
generatedMessages.append(msg)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

在解析 Batch 结果时,如果某个请求项在云端执行失败(即 responsenil,但 error 字段有值),当前代码会静默跳过该行,没有任何错误提示。

建议在 responsenilerror 存在时,记录相应的错误日志,方便用户或开发者排查批处理中具体请求项的失败原因。

Suggested change
let responseItem = try JSONDecoder().decode(BatchResponseItem.self, from: lineData)
if let payloadBody = responseItem.response?.body {
// 转回 Data 再丢给原先的 adapter 解析
if let rawData = try? JSONEncoder().encode(payloadBody) {
let msg = try adapter.parseResponse(data: rawData)
generatedMessages.append(msg)
}
}
let responseItem = try JSONDecoder().decode(BatchResponseItem.self, from: lineData)
if let payloadBody = responseItem.response?.body {
if let rawData = try? JSONEncoder().encode(payloadBody) {
let msg = try adapter.parseResponse(data: rawData)
generatedMessages.append(msg)
}
} else if let errorPayload = responseItem.error {
logger.error("Batch 请求项 \(responseItem.customId) 失败: \(String(describing: errorPayload))")
}

Comment on lines +110 to +117
struct OpenAIBatchJobResponse: Decodable {
let id: String
let status: String
let input_file_id: String?
let output_file_id: String?
let error_file_id: String?
let endpoint: String?
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

OpenAI Batch API 响应中实际上包含了 created_atcompleted_atfailed_at 等 Unix 时间戳。当前的数据模型 OpenAIBatchJobResponse 遗漏了这些字段,导致在解析状态时只能使用当前时间 Date() 作为替代,这会导致任务的创建、完成和失败时间极不准确(每次轮询都会被更新为当前时间)。

建议在模型中补充这些时间戳字段。

    struct OpenAIBatchJobResponse: Decodable {
        let id: String
        let status: String
        let input_file_id: String?
        let output_file_id: String?
        let error_file_id: String?
        let endpoint: String?
        let created_at: Int?
        let completed_at: Int?
        let failed_at: Int?
    }

Comment on lines +134 to +146
return BatchJob(
id: response.id,
providerID: UUID(), // Caller should correct this
modelID: "", // Caller should correct this
status: status,
createdAt: Date(),
completedAt: status == .completed ? Date() : nil,
failedAt: status == .failed ? Date() : nil,
inputFileId: response.input_file_id,
outputFileId: response.output_file_id,
errorFileId: response.error_file_id,
endpoint: response.endpoint
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

配合 OpenAIBatchJobResponse 中新增的时间戳字段,在解析 BatchJob 时应当使用 API 返回的真实时间戳,而不是硬编码为当前时间 Date()

Suggested change
return BatchJob(
id: response.id,
providerID: UUID(), // Caller should correct this
modelID: "", // Caller should correct this
status: status,
createdAt: Date(),
completedAt: status == .completed ? Date() : nil,
failedAt: status == .failed ? Date() : nil,
inputFileId: response.input_file_id,
outputFileId: response.output_file_id,
errorFileId: response.error_file_id,
endpoint: response.endpoint
)
return BatchJob(
id: response.id,
providerID: UUID(), // Caller should correct this
modelID: "", // Caller should correct this
status: status,
createdAt: response.created_at.map { Date(timeIntervalSince1970: TimeInterval($0)) } ?? Date(),
completedAt: response.completed_at.map { Date(timeIntervalSince1970: TimeInterval($0)) },
failedAt: response.failed_at.map { Date(timeIntervalSince1970: TimeInterval($0)) },
inputFileId: response.input_file_id,
outputFileId: response.output_file_id,
errorFileId: response.error_file_id,
endpoint: response.endpoint
)

devhunk added 3 commits July 5, 2026 14:47
本次提交为应用底层核心增加了对服务端大批量离线异步生成(Batch API)的支持,大幅降低大批量文本处理场景的调用成本(享官方折扣),并在后台实现全自动化管理与轮询。

主要改动:
- feat(Protocol): 在 `APIAdapter` 协议中新增 Batch 生命周期的 4 个必须接口(上传、创建、轮询、下载)。
- feat(OpenAI): 增加针对 OpenAI `/v1/batches` 与 `/v1/files` 的全套适配及 `Multipart/form-data` JSONL 的上传构建逻辑。
- feat(Service): 新增 `BatchService` 作为核心处理器,全自动化完成普通 `[ChatMessage]` 转 JSONL 的打包、上传、静默轮询挂起,并在云端完结后自动拉取解析。
- feat(Persistence): 新增基于轻量级 JSON 的持久化 `BatchJobStore`,确保应用重新启动后后台未完结的 Batch 任务仍能恢复追踪。
- fix(CI): 修复 `.github/workflows/pr-build-check.yml` 中 `xcodebuild` 使用 `-workspace` 解析依赖时缺少 `-scheme` 导致工作流失败的问题。
- fix(Build): 解决 Swift 6 严格并发检查对于后台异步状态挂起所引发的 `@Sendable` 闭包隔离报错;并将新编写的辅助逻辑代码合并入已有的工程追踪文件(`APIAdapterSupport.swift`等)中,规避 CI 缺失 `.pbxproj` 引用的编译异常。
@Eric-Terminal

Copy link
Copy Markdown
Owner

我自己来吧

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