From 9c5822a1fe9ca7cc199dbff6554769cd6b72a190 Mon Sep 17 00:00:00 2001 From: Arvin Xu Date: Sat, 11 Jan 2025 06:47:36 +0800 Subject: [PATCH] =?UTF-8?q?=F0=9F=90=9B=20fix:=20fix=20released=20at=20for?= =?UTF-8?q?=20undefined=20condition=20(#5391)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/ar/modelProvider.json | 2 +- locales/bg-BG/modelProvider.json | 2 +- locales/de-DE/modelProvider.json | 2 +- locales/en-US/modelProvider.json | 2 +- locales/es-ES/modelProvider.json | 2 +- locales/fa-IR/modelProvider.json | 2 +- locales/fr-FR/modelProvider.json | 2 +- locales/it-IT/modelProvider.json | 2 +- locales/ja-JP/modelProvider.json | 2 +- locales/ko-KR/modelProvider.json | 2 +- locales/nl-NL/modelProvider.json | 2 +- locales/pl-PL/modelProvider.json | 2 +- locales/pt-BR/modelProvider.json | 2 +- locales/ru-RU/modelProvider.json | 2 +- locales/tr-TR/modelProvider.json | 2 +- locales/vi-VN/modelProvider.json | 2 +- locales/zh-CN/modelProvider.json | 2 +- locales/zh-TW/modelProvider.json | 2 +- .../features/CreateNewProvider/index.tsx | 2 +- .../openai/__snapshots__/index.test.ts.snap | 6 + .../openaiCompatibleFactory/index.test.ts | 117 ++++++++++++++---- .../utils/openaiCompatibleFactory/index.ts | 26 +++- src/locales/default/modelProvider.ts | 2 +- 23 files changed, 140 insertions(+), 49 deletions(-) diff --git a/locales/ar/modelProvider.json b/locales/ar/modelProvider.json index 7b106c8f4670d..4335d8e28aea4 100644 --- a/locales/ar/modelProvider.json +++ b/locales/ar/modelProvider.json @@ -79,7 +79,7 @@ }, "id": { "desc": "معرف فريد لمزود الخدمة، لا يمكن تعديله بعد الإنشاء", - "format": "يمكن أن يحتوي فقط على أحرف صغيرة، وشرطات (-) وشرطات سفلية (_)", + "format": "يمكن أن يحتوي فقط على أرقام، أحرف صغيرة، شرطات (-) وشرطات سفلية (_) ", "placeholder": "يفضل أن يكون بالكامل بحروف صغيرة، مثل openai، لن يمكن تعديله بعد الإنشاء", "required": "يرجى إدخال معرف المزود", "title": "معرف المزود" diff --git a/locales/bg-BG/modelProvider.json b/locales/bg-BG/modelProvider.json index 38f0dad8091a1..221f9956f77e5 100644 --- a/locales/bg-BG/modelProvider.json +++ b/locales/bg-BG/modelProvider.json @@ -79,7 +79,7 @@ }, "id": { "desc": "Уникален идентификатор за доставчика на услуги, който не може да бъде променян след създаването му", - "format": "Може да съдържа само малки букви, тирета (-) и долни черти (_)", + "format": "Може да съдържа само цифри, малки букви, тирета (-) и долни черти (_) ", "placeholder": "Препоръчително изцяло с малки букви, например openai, след създаването не може да се промени", "required": "Моля, въведете ID на доставчика", "title": "ID на доставчика" diff --git a/locales/de-DE/modelProvider.json b/locales/de-DE/modelProvider.json index a6ae80f9549bc..72361ef0ceab8 100644 --- a/locales/de-DE/modelProvider.json +++ b/locales/de-DE/modelProvider.json @@ -79,7 +79,7 @@ }, "id": { "desc": "Eindeutige Kennung des Anbieters, die nach der Erstellung nicht mehr geändert werden kann", - "format": "Darf nur aus Kleinbuchstaben, Bindestrichen (-) und Unterstrichen (_) bestehen", + "format": "Darf nur aus Zahlen, Kleinbuchstaben, Bindestrichen (-) und Unterstrichen (_) bestehen", "placeholder": "Empfohlen in Kleinbuchstaben, z.B. openai, nach der Erstellung nicht mehr änderbar", "required": "Bitte geben Sie die Anbieter-ID ein", "title": "Anbieter-ID" diff --git a/locales/en-US/modelProvider.json b/locales/en-US/modelProvider.json index d292b02887e25..b67d65f7366ff 100644 --- a/locales/en-US/modelProvider.json +++ b/locales/en-US/modelProvider.json @@ -79,7 +79,7 @@ }, "id": { "desc": "Unique identifier for the service provider, which cannot be modified after creation", - "format": "Can only contain lowercase letters, hyphens (-), and underscores (_) ", + "format": "Can only contain numbers, lowercase letters, hyphens (-), and underscores (_) ", "placeholder": "Suggested all lowercase, e.g., openai, cannot be modified after creation", "required": "Please enter the provider ID", "title": "Provider ID" diff --git a/locales/es-ES/modelProvider.json b/locales/es-ES/modelProvider.json index 587184a429f6e..e1428dbedaf8a 100644 --- a/locales/es-ES/modelProvider.json +++ b/locales/es-ES/modelProvider.json @@ -79,7 +79,7 @@ }, "id": { "desc": "Identificador único del proveedor de servicios, no se puede modificar una vez creado", - "format": "Solo puede contener letras minúsculas, guiones (-) y guiones bajos (_)", + "format": "Solo puede contener números, letras minúsculas, guiones (-) y guiones bajos (_) ", "placeholder": "Se recomienda en minúsculas, por ejemplo openai, no se puede modificar después de crear", "required": "Por favor, introduce el ID del proveedor", "title": "ID del proveedor" diff --git a/locales/fa-IR/modelProvider.json b/locales/fa-IR/modelProvider.json index 4d115094d511d..8b27278dd06af 100644 --- a/locales/fa-IR/modelProvider.json +++ b/locales/fa-IR/modelProvider.json @@ -79,7 +79,7 @@ }, "id": { "desc": "به عنوان شناسه منحصر به فرد ارائه‌دهنده خدمات، پس از ایجاد قابل ویرایش نخواهد بود", - "format": "فقط می‌تواند شامل حروف کوچک، خط تیره (-) و زیرخط (_) باشد", + "format": "فقط می‌تواند شامل اعداد، حروف کوچک، خط تیره (-) و زیرخط (_) باشد", "placeholder": "توصیه می‌شود تماماً با حروف کوچک باشد، مانند openai، پس از ایجاد قابل ویرایش نخواهد بود", "required": "لطفاً شناسه ارائه‌دهنده را وارد کنید", "title": "شناسه ارائه‌دهنده" diff --git a/locales/fr-FR/modelProvider.json b/locales/fr-FR/modelProvider.json index e7ea42050dd17..e4d345630873a 100644 --- a/locales/fr-FR/modelProvider.json +++ b/locales/fr-FR/modelProvider.json @@ -79,7 +79,7 @@ }, "id": { "desc": "Identifiant unique du fournisseur de services, qui ne peut pas être modifié après sa création", - "format": "Ne peut contenir que des lettres minuscules, des tirets (-) et des underscores (_) ", + "format": "Ne peut contenir que des chiffres, des lettres minuscules, des tirets (-) et des underscores (_) ", "placeholder": "Utilisez uniquement des lettres minuscules, par exemple openai, non modifiable après création", "required": "Veuillez entrer l'ID du fournisseur", "title": "ID du fournisseur" diff --git a/locales/it-IT/modelProvider.json b/locales/it-IT/modelProvider.json index 587aebdc93cea..cde356eb0ce69 100644 --- a/locales/it-IT/modelProvider.json +++ b/locales/it-IT/modelProvider.json @@ -79,7 +79,7 @@ }, "id": { "desc": "Identificatore unico del fornitore di servizi, non modificabile dopo la creazione", - "format": "Può contenere solo lettere minuscole, trattini (-) e underscore (_)", + "format": "Può contenere solo numeri, lettere minuscole, trattini (-) e underscore (_) ", "placeholder": "Si consiglia di utilizzare solo lettere minuscole, ad esempio openai, non modificabile dopo la creazione", "required": "Inserisci l'ID del fornitore", "title": "ID del fornitore" diff --git a/locales/ja-JP/modelProvider.json b/locales/ja-JP/modelProvider.json index 8a1bcfaa36629..95668ffe50f8a 100644 --- a/locales/ja-JP/modelProvider.json +++ b/locales/ja-JP/modelProvider.json @@ -79,7 +79,7 @@ }, "id": { "desc": "サービスプロバイダーの一意の識別子であり、作成後は変更できません", - "format": "小文字のアルファベット、ハイフン(-)、およびアンダースコア(_)のみを含むことができます", + "format": "数字、小文字のアルファベット、ハイフン(-)、およびアンダースコア(_)のみを含むことができます", "placeholder": "小文字で入力してください(例: openai)。作成後は変更できません", "required": "サービスプロバイダー ID を入力してください", "title": "サービスプロバイダー ID" diff --git a/locales/ko-KR/modelProvider.json b/locales/ko-KR/modelProvider.json index 80bb003e6ee3e..8b2cb4ca00564 100644 --- a/locales/ko-KR/modelProvider.json +++ b/locales/ko-KR/modelProvider.json @@ -79,7 +79,7 @@ }, "id": { "desc": "서비스 제공자의 고유 식별자로, 생성 후에는 수정할 수 없습니다.", - "format": "소문자, 하이픈(-), 및 언더스코어(_)만 포함할 수 있습니다.", + "format": "숫자, 소문자, 하이픈(-), 및 언더스코어(_)만 포함할 수 있습니다.", "placeholder": "소문자로 입력하세요, 예: openai, 생성 후 수정할 수 없습니다", "required": "서비스 제공자 ID를 입력하세요", "title": "서비스 제공자 ID" diff --git a/locales/nl-NL/modelProvider.json b/locales/nl-NL/modelProvider.json index 551459e878d6a..92dcee1ae7979 100644 --- a/locales/nl-NL/modelProvider.json +++ b/locales/nl-NL/modelProvider.json @@ -79,7 +79,7 @@ }, "id": { "desc": "Een unieke identificatie voor de dienstverlener, kan na creatie niet meer worden gewijzigd", - "format": "Mag alleen kleine letters, koppeltekens (-) en underscores (_) bevatten", + "format": "Mag alleen cijfers, kleine letters, koppeltekens (-) en onderstrepingstekens (_) bevatten", "placeholder": "Gebruik alleen kleine letters, bijvoorbeeld openai, kan niet worden gewijzigd na aanmaak", "required": "Vul de provider ID in", "title": "Provider ID" diff --git a/locales/pl-PL/modelProvider.json b/locales/pl-PL/modelProvider.json index a7c57fb6a4186..73ead421f4517 100644 --- a/locales/pl-PL/modelProvider.json +++ b/locales/pl-PL/modelProvider.json @@ -79,7 +79,7 @@ }, "id": { "desc": "Unikalny identyfikator dostawcy usług, po utworzeniu nie można go zmienić", - "format": "Może zawierać tylko małe litery, myślniki (-) i podkreślenia (_)", + "format": "Może zawierać tylko cyfry, małe litery, myślniki (-) i podkreślenia (_) ", "placeholder": "Zaleca się użycie małych liter, np. openai, po utworzeniu nie można edytować", "required": "Proszę wpisać identyfikator dostawcy", "title": "Identyfikator dostawcy" diff --git a/locales/pt-BR/modelProvider.json b/locales/pt-BR/modelProvider.json index 5b51fdcbd33ba..4886b053233ad 100644 --- a/locales/pt-BR/modelProvider.json +++ b/locales/pt-BR/modelProvider.json @@ -79,7 +79,7 @@ }, "id": { "desc": "Identificador único do provedor de serviços, não pode ser modificado após a criação", - "format": "Pode conter apenas letras minúsculas, hífens (-) e sublinhados (_)", + "format": "Só pode conter números, letras minúsculas, hífens (-) e sublinhados (_) ", "placeholder": "Sugestão: tudo em minúsculas, por exemplo, openai, não poderá ser modificado após a criação", "required": "Por favor, insira o ID do provedor", "title": "ID do Provedor" diff --git a/locales/ru-RU/modelProvider.json b/locales/ru-RU/modelProvider.json index 1b9fb76e8ef03..83ceaaabe95b9 100644 --- a/locales/ru-RU/modelProvider.json +++ b/locales/ru-RU/modelProvider.json @@ -79,7 +79,7 @@ }, "id": { "desc": "Уникальный идентификатор для поставщика услуг, который нельзя изменить после создания", - "format": "Может содержать только строчные буквы, дефисы (-) и подчеркивания (_)", + "format": "Может содержать только цифры, строчные буквы, дефисы (-) и подчеркивания (_) ", "placeholder": "Рекомендуется использовать строчные буквы, например, openai, после создания изменить нельзя", "required": "Пожалуйста, введите ID провайдера", "title": "ID провайдера" diff --git a/locales/tr-TR/modelProvider.json b/locales/tr-TR/modelProvider.json index daaf1b9244bc8..626aed4d2089e 100644 --- a/locales/tr-TR/modelProvider.json +++ b/locales/tr-TR/modelProvider.json @@ -79,7 +79,7 @@ }, "id": { "desc": "Hizmet sağlayıcının benzersiz kimliği, oluşturulduktan sonra değiştirilemez", - "format": "Sadece küçük harfler, tire (-) ve alt çizgi (_) içerebilir", + "format": "Sadece rakamlar, küçük harfler, tire (-) ve alt çizgi (_) içerebilir", "placeholder": "Küçük harflerle yazılması önerilir, örneğin openai, oluşturduktan sonra değiştirilemez", "required": "Lütfen hizmet sağlayıcı ID'sini girin", "title": "Hizmet Sağlayıcı ID" diff --git a/locales/vi-VN/modelProvider.json b/locales/vi-VN/modelProvider.json index b7f620d5bcc47..fd02d84255aee 100644 --- a/locales/vi-VN/modelProvider.json +++ b/locales/vi-VN/modelProvider.json @@ -79,7 +79,7 @@ }, "id": { "desc": "Là định danh duy nhất của nhà cung cấp dịch vụ, không thể sửa đổi sau khi tạo", - "format": "Chỉ có thể chứa chữ cái thường, dấu gạch ngang (-) và dấu gạch dưới (_)", + "format": "Chỉ có thể chứa số, chữ cái thường, dấu gạch ngang (-) và dấu gạch dưới (_) ", "placeholder": "Nên viết toàn bộ bằng chữ thường, ví dụ openai, không thể sửa sau khi tạo", "required": "Vui lòng nhập ID nhà cung cấp", "title": "ID nhà cung cấp" diff --git a/locales/zh-CN/modelProvider.json b/locales/zh-CN/modelProvider.json index 413fdabfc5d48..eb3e84ddab440 100644 --- a/locales/zh-CN/modelProvider.json +++ b/locales/zh-CN/modelProvider.json @@ -79,7 +79,7 @@ }, "id": { "desc": "作为服务商唯一标识,创建后将不可修改", - "format": "只能包含小写字母、连字符(-)和下划线(_)", + "format": "只能包含数字、小写字母、连字符(-)和下划线(_)", "placeholder": "例如 openai、gemini 等", "required": "请填写服务商 ID", "title": "服务商 ID" diff --git a/locales/zh-TW/modelProvider.json b/locales/zh-TW/modelProvider.json index 016d6b9fa3ea7..b3f4e40fe7bc0 100644 --- a/locales/zh-TW/modelProvider.json +++ b/locales/zh-TW/modelProvider.json @@ -79,7 +79,7 @@ }, "id": { "desc": "作為服務商唯一標識,創建後將不可修改", - "format": "只能包含小寫字母、連字符(-)和下劃線(_)", + "format": "只能包含數字、小寫字母、連字符(-)和底線(_)", "placeholder": "建議全小寫,例如 openai,創建後將不可修改", "required": "請填寫服務商 ID", "title": "服務商 ID" diff --git a/src/app/(main)/settings/provider/features/CreateNewProvider/index.tsx b/src/app/(main)/settings/provider/features/CreateNewProvider/index.tsx index e7284d2457899..a1ee4e2f34577 100644 --- a/src/app/(main)/settings/provider/features/CreateNewProvider/index.tsx +++ b/src/app/(main)/settings/provider/features/CreateNewProvider/index.tsx @@ -51,7 +51,7 @@ const CreateNewProvider = memo(({ onClose, open }) => { { message: t('createNewAiProvider.id.required'), required: true }, { message: t('createNewAiProvider.id.format'), - pattern: /^[_a-z-]+$/, + pattern: /^[\d_a-z-]+$/, }, ], }, diff --git a/src/libs/agent-runtime/openai/__snapshots__/index.test.ts.snap b/src/libs/agent-runtime/openai/__snapshots__/index.test.ts.snap index f0aea17d21603..e15fbcca3aa74 100644 --- a/src/libs/agent-runtime/openai/__snapshots__/index.test.ts.snap +++ b/src/libs/agent-runtime/openai/__snapshots__/index.test.ts.snap @@ -16,15 +16,19 @@ exports[`LobeOpenAI > models > should get models 1`] = ` }, { "id": "gpt-3.5-turbo-16k", + "releasedAt": "2023-05-10", }, { "id": "gpt-3.5-turbo-16k-0613", + "releasedAt": "2023-05-30", }, { "id": "gpt-4-1106-vision-preview", + "releasedAt": "2024-03-26", }, { "id": "gpt-3.5-turbo-instruct-0914", + "releasedAt": "2023-09-07", }, { "contextWindowTokens": 128000, @@ -63,9 +67,11 @@ exports[`LobeOpenAI > models > should get models 1`] = ` }, { "id": "gpt-3.5-turbo-0301", + "releasedAt": "2023-03-01", }, { "id": "gpt-3.5-turbo-0613", + "releasedAt": "2023-06-12", }, { "contextWindowTokens": 16385, diff --git a/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.test.ts b/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.test.ts index d95009f484fe9..bcedb624c6601 100644 --- a/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.test.ts +++ b/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.test.ts @@ -1,7 +1,6 @@ // @vitest-environment node import OpenAI from 'openai'; import type { Stream } from 'openai/streaming'; - import { Mock, afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { @@ -11,6 +10,7 @@ import { LobeOpenAICompatibleRuntime, ModelProvider, } from '@/libs/agent-runtime'; +import officalOpenAIModels from '@/libs/agent-runtime/openai/fixtures/openai-models.json'; import { sleep } from '@/utils/sleep'; import * as debugStreamModule from '../debugStream'; @@ -802,26 +802,29 @@ describe('LobeOpenAICompatibleFactory', () => { it('should use custom stream handler when provided', async () => { // Create a custom stream handler that handles both ReadableStream and OpenAI Stream - const customStreamHandler = vi.fn((stream: ReadableStream | Stream) => { - const readableStream = stream instanceof ReadableStream ? stream : stream.toReadableStream(); - return new ReadableStream({ - start(controller) { - const reader = readableStream.getReader(); - const process = async () => { - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - controller.enqueue(value); + const customStreamHandler = vi.fn( + (stream: ReadableStream | Stream) => { + const readableStream = + stream instanceof ReadableStream ? stream : stream.toReadableStream(); + return new ReadableStream({ + start(controller) { + const reader = readableStream.getReader(); + const process = async () => { + try { + while (true) { + const { done, value } = await reader.read(); + if (done) break; + controller.enqueue(value); + } + } finally { + controller.close(); } - } finally { - controller.close(); - } - }; - process(); - }, - }); - }); + }; + process(); + }, + }); + }, + ); const LobeMockProvider = LobeOpenAICompatibleFactory({ baseURL: 'https://api.test.com/v1', @@ -897,10 +900,10 @@ describe('LobeOpenAICompatibleFactory', () => { choices: [ { index: 0, - message: { - role: 'assistant', + message: { + role: 'assistant', content: 'Test response', - refusal: null + refusal: null, }, logprobs: null, finish_reason: 'stop', @@ -969,4 +972,72 @@ describe('LobeOpenAICompatibleFactory', () => { }); }); }); + + describe('models', () => { + it('should get models with third party model list', async () => { + vi.spyOn(instance['client'].models, 'list').mockResolvedValue({ + data: [ + { id: 'gpt-4o', object: 'model', created: 1698218177 }, + { id: 'claude-3-haiku-20240307', object: 'model' }, + { id: 'gpt-4o-mini', object: 'model', created: 1698318177 * 1000 }, + { id: 'gemini', object: 'model', created: 1736499509125 }, + ], + } as any); + + const list = await instance.models(); + + expect(list).toEqual([ + { + contextWindowTokens: 128000, + releasedAt: '2023-10-25', + description: + 'ChatGPT-4o 是一款动态模型,实时更新以保持当前最新版本。它结合了强大的语言理解与生成能力,适合于大规模应用场景,包括客户服务、教育和技术支持。', + displayName: 'GPT-4o', + enabled: true, + functionCall: true, + id: 'gpt-4o', + pricing: { + input: 2.5, + output: 10, + }, + vision: true, + }, + { + contextWindowTokens: 200000, + description: + 'Claude 3 Haiku 是 Anthropic 的最快且最紧凑的模型,旨在实现近乎即时的响应。它具有快速且准确的定向性能。', + displayName: 'Claude 3 Haiku', + functionCall: true, + id: 'claude-3-haiku-20240307', + maxOutput: 4096, + pricing: { + input: 0.25, + output: 1.25, + }, + releasedAt: '2024-03-07', + vision: true, + }, + { + contextWindowTokens: 128000, + description: + 'GPT-4o mini是OpenAI在GPT-4 Omni之后推出的最新模型,支持图文输入并输出文本。作为他们最先进的小型模型,它比其他近期的前沿模型便宜很多,并且比GPT-3.5 Turbo便宜超过60%。它保持了最先进的智能,同时具有显著的性价比。GPT-4o mini在MMLU测试中获得了 82% 的得分,目前在聊天偏好上排名高于 GPT-4。', + displayName: 'GPT-4o mini', + enabled: true, + functionCall: true, + id: 'gpt-4o-mini', + maxOutput: 16385, + pricing: { + input: 0.15, + output: 0.6, + }, + releasedAt: '2023-10-26', + vision: true, + }, + { + id: 'gemini', + releasedAt: '2025-01-10', + }, + ]); + }); + }); }); diff --git a/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.ts b/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.ts index e2a00164b886e..111b3bf42afbf 100644 --- a/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.ts +++ b/src/libs/agent-runtime/utils/openaiCompatibleFactory/index.ts @@ -279,20 +279,34 @@ export const LobeOpenAICompatibleFactory = = any> return models.transformModel(item); } + const toReleasedAt = () => { + if (!item.created) return; + + // guarantee item.created in Date String format + if ( + typeof (item.created as any) === 'string' || + // or in milliseconds + item.created.toFixed(0).length === 13 + ) { + return dayjs.utc(item.created).format('YYYY-MM-DD'); + } + + // by default, the created time is in seconds + return dayjs.utc(item.created * 1000).format('YYYY-MM-DD'); + }; + // TODO: should refactor after remove v1 user/modelList code const knownModel = LOBE_DEFAULT_MODEL_LIST.find((model) => model.id === item.id); if (knownModel) { dayjs.extend(utc); - return { - ...knownModel, - releasedAt: - knownModel.releasedAt ?? dayjs.utc(item.created * 1000).format('YYYY-MM-DD'), - }; + const releasedAt = knownModel.releasedAt ?? toReleasedAt(); + + return { ...knownModel, releasedAt }; } - return { id: item.id }; + return { id: item.id, releasedAt: toReleasedAt() }; }) .filter(Boolean) as ChatModelCard[]; diff --git a/src/locales/default/modelProvider.ts b/src/locales/default/modelProvider.ts index 613df7996b5eb..3355a8416eba1 100644 --- a/src/locales/default/modelProvider.ts +++ b/src/locales/default/modelProvider.ts @@ -80,7 +80,7 @@ export default { }, id: { desc: '作为服务商唯一标识,创建后将不可修改', - format: '只能包含小写字母、连字符(-)和下划线(_)', + format: '只能包含数字、小写字母、连字符(-)和下划线(_)', placeholder: '例如 openai、gemini 等', required: '请填写服务商 ID', title: '服务商 ID',