diff --git a/packages/ui/src/components/sections/providers/CustomProviderForm.tsx b/packages/ui/src/components/sections/providers/CustomProviderForm.tsx index 6fce317e..557151d2 100644 --- a/packages/ui/src/components/sections/providers/CustomProviderForm.tsx +++ b/packages/ui/src/components/sections/providers/CustomProviderForm.tsx @@ -10,9 +10,11 @@ import { } from '@/components/sections/shared/SettingsSection'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Icon } from '@/components/icon/Icon'; import { useI18n } from '@/lib/i18n'; import { + CUSTOM_PROVIDER_PROTOCOLS, createEmptyCustomProviderForm, createHeaderRow, createModelRow, @@ -161,6 +163,31 @@ export const CustomProviderForm: React.FC = ({ {err.providerID ?

{err.providerID}

: null} + + + + key; const baseForm = (overrides: Partial = {}): CustomProviderFormState => ({ providerID: 'custom-provider', name: 'Custom Provider', + protocol: 'openai-chat', baseURL: 'https://api.example.com/v1', apiKey: 'sk-test', models: [{ row: 'm0', id: 'model-a', name: 'Model A' }], @@ -96,6 +97,16 @@ describe('validateCustomProvider', () => { expect(result.result?.config.env).toEqual(['CUSTOM_PROVIDER_KEY']); }); + test('uses the selected OpenCode provider adapter', () => { + const result = validateCustomProvider({ + form: baseForm({ protocol: 'openai-responses' }), + t, + existingProviderIDs: new Set(), + }); + + expect(result.result?.config.npm).toBe('@ai-sdk/openai'); + }); + test('rejects missing credentials', () => { const result = validateCustomProvider({ form: baseForm({ apiKey: ' ' }), @@ -300,10 +311,21 @@ describe('provider edit helpers', () => { expect(state.name).toBe('Campus LLM'); expect(state.baseURL).toBe('https://llm.example.edu/v1'); expect(state.apiKey).toBe('{env:CAMPUS_KEY}'); + expect(state.protocol).toBe('openai-chat'); expect(state.models[0]).toEqual({ row: state.models[0].row, id: 'fast', name: 'Fast' }); expect(state.headers[0]).toEqual({ row: state.headers[0].row, key: 'X-Campus', value: '1' }); }); + test('prefills the protocol from a custom provider model', () => { + const state = providerToCustomFormState({ + id: 'responses-api', + options: { baseURL: 'https://api.example.com/v1' }, + models: [{ id: 'gpt', name: 'GPT', api: { npm: '@ai-sdk/openai' } }], + }); + + expect(state.protocol).toBe('openai-responses'); + }); + test('requires a config-layer source before treating a provider as editable custom', () => { const catalogLike = { id: 'openai', diff --git a/packages/ui/src/components/sections/providers/custom-provider-form.ts b/packages/ui/src/components/sections/providers/custom-provider-form.ts index 49178ece..355ee6db 100644 --- a/packages/ui/src/components/sections/providers/custom-provider-form.ts +++ b/packages/ui/src/components/sections/providers/custom-provider-form.ts @@ -1,10 +1,16 @@ /** - * Custom / Other OpenAI-compatible provider form helpers. + * Custom provider form helpers. * Mirrors OpenCode web UI validation and request construction so a provider * can be defined from Settings without code changes. */ -export const CUSTOM_PROVIDER_NPM = '@ai-sdk/openai-compatible'; +export const CUSTOM_PROVIDER_PROTOCOLS = { + 'openai-chat': '@ai-sdk/openai-compatible', + 'openai-responses': '@ai-sdk/openai', + 'anthropic-messages': '@ai-sdk/anthropic', +} as const; +export type CustomProviderProtocol = keyof typeof CUSTOM_PROVIDER_PROTOCOLS; +export type CustomProviderNpm = (typeof CUSTOM_PROVIDER_PROTOCOLS)[CustomProviderProtocol]; export const CUSTOM_PROVIDER_ID = '__custom_provider__'; const PROVIDER_ID_PATTERN = /^[a-z0-9][a-z0-9-_]*$/; const BASE_URL_PATTERN = /^https?:\/\//; @@ -30,6 +36,7 @@ export type HeaderRow = { export type CustomProviderFormState = { providerID: string; name: string; + protocol: CustomProviderProtocol; baseURL: string; apiKey: string; models: ModelRow[]; @@ -54,7 +61,7 @@ export type HeaderFieldErrors = { }; export type CustomProviderConfig = { - npm: typeof CUSTOM_PROVIDER_NPM; + npm: CustomProviderNpm; name: string; env?: string[]; options: { @@ -120,12 +127,24 @@ export const createHeaderRow = (): HeaderRow => ({ export const createEmptyCustomProviderForm = (): CustomProviderFormState => ({ providerID: '', name: '', + protocol: 'openai-chat', baseURL: '', apiKey: '', models: [createModelRow()], headers: [createHeaderRow()], }); +function protocolFromNpm(npm: string | undefined): CustomProviderProtocol { + switch (npm) { + case '@ai-sdk/openai': + return 'openai-responses'; + case '@ai-sdk/anthropic': + return 'anthropic-messages'; + default: + return 'openai-chat'; + } +} + function parseEnvApiKey(apiKey: string): { env?: string; key?: string } { const trimmed = apiKey.trim(); if (!trimmed) { @@ -159,7 +178,7 @@ export function isCustomOpenAICompatibleProvider(provider: ProviderLikeForCustom const api = 'api' in model && model.api && typeof model.api === 'object' ? model.api as { npm?: unknown } : null; - return typeof api?.npm === 'string' && api.npm === CUSTOM_PROVIDER_NPM; + return typeof api?.npm === 'string' && new Set(Object.values(CUSTOM_PROVIDER_PROTOCOLS)).has(api.npm); }); } @@ -238,9 +257,14 @@ export function providerToCustomFormState(provider: ProviderLikeForCustomForm): ? provider.env.find((entry) => typeof entry === 'string' && entry.trim().length > 0)?.trim() : undefined; + const modelWithApi = modelEntries.find( + (model): model is { id?: string; name?: string; api?: { npm?: string } } => 'api' in model, + ); + return { providerID: provider.id, name: typeof provider.name === 'string' && provider.name.trim() ? provider.name : provider.id, + protocol: protocolFromNpm(modelWithApi?.api?.npm), baseURL, apiKey: envName ? `{env:${envName}}` : '', models, @@ -360,7 +384,7 @@ export function validateCustomProvider(input: ValidateCustomProviderInput): Vali name, apiKey: key, config: { - npm: CUSTOM_PROVIDER_NPM, + npm: CUSTOM_PROVIDER_PROTOCOLS[input.form.protocol], name, ...(env ? { env: [env] } : {}), options: { diff --git a/packages/ui/src/lib/i18n/messages/de.settings.ts b/packages/ui/src/lib/i18n/messages/de.settings.ts index 02ee5eeb..8029b28b 100644 --- a/packages/ui/src/lib/i18n/messages/de.settings.ts +++ b/packages/ui/src/lib/i18n/messages/de.settings.ts @@ -1309,13 +1309,18 @@ export const settingsDict = { 'settings.providers.page.custom.optionLabel': 'Andere / Benutzerdefiniert', 'settings.providers.page.custom.title': 'Benutzerdefinierter Anbieter', 'settings.providers.page.custom.editTitle': 'Benutzerdefinierten Anbieter bearbeiten', - 'settings.providers.page.custom.description': 'Fügen Sie einen OpenAI-kompatiblen Anbieter mit Basis-URL, Anmeldedaten und Modellliste hinzu. Wird in der OpenCode-Konfiguration gespeichert und steht im Chat wie jeder andere Anbieter zur Verfügung.', + 'settings.providers.page.custom.description': 'Fügen Sie einen Anbieter mit Basis-URL, Anmeldedaten, Modellliste und unterstütztem API-Protokoll hinzu. Wird zur Nutzung im Chat in der OpenCode-Konfiguration gespeichert.', 'settings.providers.page.custom.field.providerID.label': 'Anbieter-ID', 'settings.providers.page.custom.field.providerID.placeholder': 'mein-anbieter', 'settings.providers.page.custom.field.providerID.info': 'Kleinbuchstaben, Zahlen, Bindestriche und Unterstriche. Wird als OpenCode-Anbieter-ID verwendet.', 'settings.providers.page.custom.field.name.label': 'Anzeigename', 'settings.providers.page.custom.field.name.placeholder': 'Mein Anbieter', 'settings.providers.page.custom.field.name.info': 'Wird in den Anbieter- und Modellauswahlen angezeigt.', + 'settings.providers.page.custom.field.protocol.label': 'API-Protokoll', + 'settings.providers.page.custom.field.protocol.info': 'Wählen Sie das Anfrageformat, das diese API implementiert.', + 'settings.providers.page.custom.field.protocol.openaiChat': 'OpenAI Chat Completions', + 'settings.providers.page.custom.field.protocol.openaiResponses': 'OpenAI Responses', + 'settings.providers.page.custom.field.protocol.anthropicMessages': 'Anthropic Messages', 'settings.providers.page.custom.field.baseURL.label': 'Basis-URL', 'settings.providers.page.custom.field.baseURL.placeholder': 'https://api.example.com/v1', 'settings.providers.page.custom.field.baseURL.info': 'OpenAI-kompatible API-Basis-URL. Muss mit http:// oder https:// beginnen.', diff --git a/packages/ui/src/lib/i18n/messages/en.settings.ts b/packages/ui/src/lib/i18n/messages/en.settings.ts index 5e58a605..00a788ad 100644 --- a/packages/ui/src/lib/i18n/messages/en.settings.ts +++ b/packages/ui/src/lib/i18n/messages/en.settings.ts @@ -1371,13 +1371,18 @@ export const settingsDict = { 'settings.providers.page.custom.optionLabel': 'Other / Custom', 'settings.providers.page.custom.title': 'Custom provider', 'settings.providers.page.custom.editTitle': 'Edit custom provider', - 'settings.providers.page.custom.description': 'Add an OpenAI-compatible provider with a base URL, credentials, and model list. Saved to OpenCode config so it works in chat like any other provider.', + 'settings.providers.page.custom.description': 'Add a provider with a base URL, credentials, model list, and supported API protocol. Saved to OpenCode config for use in chat.', 'settings.providers.page.custom.field.providerID.label': 'Provider ID', 'settings.providers.page.custom.field.providerID.placeholder': 'my-provider', 'settings.providers.page.custom.field.providerID.info': 'Lowercase letters, numbers, hyphens, and underscores. Used as the OpenCode provider id.', 'settings.providers.page.custom.field.name.label': 'Display name', 'settings.providers.page.custom.field.name.placeholder': 'My Provider', 'settings.providers.page.custom.field.name.info': 'Shown in the provider and model pickers.', + 'settings.providers.page.custom.field.protocol.label': 'API protocol', + 'settings.providers.page.custom.field.protocol.info': 'Choose the request format implemented by this API.', + 'settings.providers.page.custom.field.protocol.openaiChat': 'OpenAI Chat Completions', + 'settings.providers.page.custom.field.protocol.openaiResponses': 'OpenAI Responses', + 'settings.providers.page.custom.field.protocol.anthropicMessages': 'Anthropic Messages', 'settings.providers.page.custom.field.baseURL.label': 'Base URL', 'settings.providers.page.custom.field.baseURL.placeholder': 'https://api.example.com/v1', 'settings.providers.page.custom.field.baseURL.info': 'OpenAI-compatible API base URL. Must start with http:// or https://.', diff --git a/packages/ui/src/lib/i18n/messages/es.settings.ts b/packages/ui/src/lib/i18n/messages/es.settings.ts index bd06384c..13dc9ce9 100644 --- a/packages/ui/src/lib/i18n/messages/es.settings.ts +++ b/packages/ui/src/lib/i18n/messages/es.settings.ts @@ -1340,13 +1340,18 @@ export const settingsDict = { "settings.providers.page.custom.title": "Proveedor personalizado", "settings.providers.page.custom.editTitle": "Editar proveedor personalizado", - "settings.providers.page.custom.description": "Añade un proveedor compatible con OpenAI con URL base, credenciales y lista de modelos. Se guarda en la configuración de OpenCode para usarlo en el chat como cualquier otro proveedor.", + "settings.providers.page.custom.description": "Añade un proveedor con URL base, credenciales, lista de modelos y un protocolo de API compatible. Se guarda en la configuración de OpenCode para usarlo en el chat.", "settings.providers.page.custom.field.providerID.label": "ID del proveedor", "settings.providers.page.custom.field.providerID.placeholder": "mi-proveedor", "settings.providers.page.custom.field.providerID.info": "Minúsculas, números, guiones y guiones bajos. Se usa como ID de proveedor de OpenCode.", "settings.providers.page.custom.field.name.label": "Nombre visible", "settings.providers.page.custom.field.name.placeholder": "Mi proveedor", "settings.providers.page.custom.field.name.info": "Se muestra en los selectores de proveedor y modelo.", + "settings.providers.page.custom.field.protocol.label": "Protocolo de API", + "settings.providers.page.custom.field.protocol.info": "Elige el formato de solicitud que implementa esta API.", + "settings.providers.page.custom.field.protocol.openaiChat": "OpenAI Chat Completions", + "settings.providers.page.custom.field.protocol.openaiResponses": "OpenAI Responses", + "settings.providers.page.custom.field.protocol.anthropicMessages": "Anthropic Messages", "settings.providers.page.custom.field.baseURL.label": "URL base", "settings.providers.page.custom.field.baseURL.placeholder": "https://api.example.com/v1", "settings.providers.page.custom.field.baseURL.info": "URL base de la API compatible con OpenAI. Debe empezar por http:// o https://.", diff --git a/packages/ui/src/lib/i18n/messages/fr.settings.ts b/packages/ui/src/lib/i18n/messages/fr.settings.ts index b75e1c35..477cb651 100644 --- a/packages/ui/src/lib/i18n/messages/fr.settings.ts +++ b/packages/ui/src/lib/i18n/messages/fr.settings.ts @@ -1258,13 +1258,18 @@ export const settingsDict = { 'settings.providers.page.custom.title': 'Fournisseur personnalisé', 'settings.providers.page.custom.editTitle': 'Modifier le fournisseur personnalisé', - 'settings.providers.page.custom.description': 'Ajoutez un fournisseur compatible OpenAI avec une URL de base, des identifiants et une liste de modèles. Enregistré dans la configuration OpenCode pour l’utiliser dans le chat comme les autres fournisseurs.', + 'settings.providers.page.custom.description': 'Ajoutez un fournisseur avec une URL de base, des identifiants, une liste de modèles et un protocole API pris en charge. Enregistré dans la configuration OpenCode pour le chat.', 'settings.providers.page.custom.field.providerID.label': 'ID du fournisseur', 'settings.providers.page.custom.field.providerID.placeholder': 'mon-fournisseur', 'settings.providers.page.custom.field.providerID.info': 'Minuscules, chiffres, tirets et underscores. Utilisé comme ID de fournisseur OpenCode.', 'settings.providers.page.custom.field.name.label': 'Nom affiché', 'settings.providers.page.custom.field.name.placeholder': 'Mon fournisseur', 'settings.providers.page.custom.field.name.info': 'Affiché dans les sélecteurs de fournisseur et de modèle.', + 'settings.providers.page.custom.field.protocol.label': 'Protocole API', + 'settings.providers.page.custom.field.protocol.info': 'Choisissez le format de requête implémenté par cette API.', + 'settings.providers.page.custom.field.protocol.openaiChat': 'OpenAI Chat Completions', + 'settings.providers.page.custom.field.protocol.openaiResponses': 'OpenAI Responses', + 'settings.providers.page.custom.field.protocol.anthropicMessages': 'Anthropic Messages', 'settings.providers.page.custom.field.baseURL.label': 'URL de base', 'settings.providers.page.custom.field.baseURL.placeholder': 'https://api.example.com/v1', 'settings.providers.page.custom.field.baseURL.info': 'URL de base de l’API compatible OpenAI. Doit commencer par http:// ou https://.', diff --git a/packages/ui/src/lib/i18n/messages/ja.settings.ts b/packages/ui/src/lib/i18n/messages/ja.settings.ts index 00319089..0f6d90f2 100644 --- a/packages/ui/src/lib/i18n/messages/ja.settings.ts +++ b/packages/ui/src/lib/i18n/messages/ja.settings.ts @@ -1373,13 +1373,18 @@ export const settingsDict = { 'settings.providers.page.custom.title': 'カスタムプロバイダー', 'settings.providers.page.custom.editTitle': 'カスタムプロバイダーを編集', - 'settings.providers.page.custom.description': 'ベース URL・認証情報・モデル一覧を指定して、OpenAI 互換プロバイダーを追加します。OpenCode 設定に保存され、他のプロバイダーと同様にチャットで使えます。', + 'settings.providers.page.custom.description': 'ベース URL、認証情報、モデル一覧、対応 API プロトコルを指定してプロバイダーを追加します。チャットで使えるよう OpenCode 設定に保存されます。', 'settings.providers.page.custom.field.providerID.label': 'プロバイダー ID', 'settings.providers.page.custom.field.providerID.placeholder': 'my-provider', 'settings.providers.page.custom.field.providerID.info': '小文字・数字・ハイフン・アンダースコア。OpenCode のプロバイダー ID として使われます。', 'settings.providers.page.custom.field.name.label': '表示名', 'settings.providers.page.custom.field.name.placeholder': 'My Provider', 'settings.providers.page.custom.field.name.info': 'プロバイダーおよびモデル選択に表示されます。', + 'settings.providers.page.custom.field.protocol.label': 'API プロトコル', + 'settings.providers.page.custom.field.protocol.info': 'この API が実装しているリクエスト形式を選択します。', + 'settings.providers.page.custom.field.protocol.openaiChat': 'OpenAI Chat Completions', + 'settings.providers.page.custom.field.protocol.openaiResponses': 'OpenAI Responses', + 'settings.providers.page.custom.field.protocol.anthropicMessages': 'Anthropic Messages', 'settings.providers.page.custom.field.baseURL.label': 'ベース URL', 'settings.providers.page.custom.field.baseURL.placeholder': 'https://api.example.com/v1', 'settings.providers.page.custom.field.baseURL.info': 'OpenAI 互換 API のベース URL。http:// または https:// で始めてください。', diff --git a/packages/ui/src/lib/i18n/messages/ko.settings.ts b/packages/ui/src/lib/i18n/messages/ko.settings.ts index 74c6bc98..0d56f1bf 100644 --- a/packages/ui/src/lib/i18n/messages/ko.settings.ts +++ b/packages/ui/src/lib/i18n/messages/ko.settings.ts @@ -1340,13 +1340,18 @@ export const settingsDict = { 'settings.providers.page.custom.title': '사용자 정의 제공자', 'settings.providers.page.custom.editTitle': '사용자 지정 공급자 편집', - 'settings.providers.page.custom.description': '기본 URL, 자격 증명, 모델 목록으로 OpenAI 호환 제공자를 추가합니다. OpenCode 설정에 저장되어 다른 제공자와 같이 채팅에서 사용할 수 있습니다.', + 'settings.providers.page.custom.description': '기본 URL, 자격 증명, 모델 목록 및 지원되는 API 프로토콜로 제공자를 추가합니다. 채팅에 사용할 수 있도록 OpenCode 설정에 저장됩니다.', 'settings.providers.page.custom.field.providerID.label': '제공자 ID', 'settings.providers.page.custom.field.providerID.placeholder': 'my-provider', 'settings.providers.page.custom.field.providerID.info': '소문자, 숫자, 하이픈, 밑줄. OpenCode 제공자 ID로 사용됩니다.', 'settings.providers.page.custom.field.name.label': '표시 이름', 'settings.providers.page.custom.field.name.placeholder': '내 제공자', 'settings.providers.page.custom.field.name.info': '제공자 및 모델 선택기에 표시됩니다.', + 'settings.providers.page.custom.field.protocol.label': 'API 프로토콜', + 'settings.providers.page.custom.field.protocol.info': '이 API가 구현하는 요청 형식을 선택하세요.', + 'settings.providers.page.custom.field.protocol.openaiChat': 'OpenAI Chat Completions', + 'settings.providers.page.custom.field.protocol.openaiResponses': 'OpenAI Responses', + 'settings.providers.page.custom.field.protocol.anthropicMessages': 'Anthropic Messages', 'settings.providers.page.custom.field.baseURL.label': '기본 URL', 'settings.providers.page.custom.field.baseURL.placeholder': 'https://api.example.com/v1', 'settings.providers.page.custom.field.baseURL.info': 'OpenAI 호환 API 기본 URL. http:// 또는 https://로 시작해야 합니다.', diff --git a/packages/ui/src/lib/i18n/messages/pl.settings.ts b/packages/ui/src/lib/i18n/messages/pl.settings.ts index 9092163f..6053bf59 100644 --- a/packages/ui/src/lib/i18n/messages/pl.settings.ts +++ b/packages/ui/src/lib/i18n/messages/pl.settings.ts @@ -1419,13 +1419,18 @@ export const settingsDict = { 'settings.providers.page.custom.title': 'Niestandardowy dostawca', 'settings.providers.page.custom.editTitle': 'Edytuj niestandardowego dostawcę', - 'settings.providers.page.custom.description': 'Dodaj dostawcę zgodnego z OpenAI, podając adres bazowy, poświadczenia i listę modeli. Zapisuje się w konfiguracji OpenCode i działa w czacie jak każdy inny dostawca.', + 'settings.providers.page.custom.description': 'Dodaj dostawcę z adresem bazowym, poświadczeniami, listą modeli i obsługiwanym protokołem API. Zapisuje się w konfiguracji OpenCode do użycia w czacie.', 'settings.providers.page.custom.field.providerID.label': 'ID dostawcy', 'settings.providers.page.custom.field.providerID.placeholder': 'moj-dostawca', 'settings.providers.page.custom.field.providerID.info': 'Małe litery, cyfry, myślniki i podkreślenia. Używane jako ID dostawcy OpenCode.', 'settings.providers.page.custom.field.name.label': 'Nazwa wyświetlana', 'settings.providers.page.custom.field.name.placeholder': 'Mój dostawca', 'settings.providers.page.custom.field.name.info': 'Widoczna w selektorach dostawcy i modelu.', + 'settings.providers.page.custom.field.protocol.label': 'Protokół API', + 'settings.providers.page.custom.field.protocol.info': 'Wybierz format żądania obsługiwany przez to API.', + 'settings.providers.page.custom.field.protocol.openaiChat': 'OpenAI Chat Completions', + 'settings.providers.page.custom.field.protocol.openaiResponses': 'OpenAI Responses', + 'settings.providers.page.custom.field.protocol.anthropicMessages': 'Anthropic Messages', 'settings.providers.page.custom.field.baseURL.label': 'Adres bazowy', 'settings.providers.page.custom.field.baseURL.placeholder': 'https://api.example.com/v1', 'settings.providers.page.custom.field.baseURL.info': 'Bazowy URL API zgodnego z OpenAI. Musi zaczynać się od http:// lub https://.', diff --git a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts index d85f354c..bae5d5c7 100644 --- a/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts +++ b/packages/ui/src/lib/i18n/messages/pt-BR.settings.ts @@ -1340,13 +1340,18 @@ export const settingsDict = { "settings.providers.page.custom.title": "Provedor personalizado", "settings.providers.page.custom.editTitle": "Editar provedor personalizado", - "settings.providers.page.custom.description": "Adicione um provedor compatível com OpenAI com URL base, credenciais e lista de modelos. Salvo na configuração do OpenCode para uso no chat como qualquer outro provedor.", + "settings.providers.page.custom.description": "Adicione um provedor com URL base, credenciais, lista de modelos e protocolo de API compatível. Ele é salvo na configuração do OpenCode para uso no chat.", "settings.providers.page.custom.field.providerID.label": "ID do provedor", "settings.providers.page.custom.field.providerID.placeholder": "meu-provedor", "settings.providers.page.custom.field.providerID.info": "Letras minúsculas, números, hífens e sublinhados. Usado como ID de provedor do OpenCode.", "settings.providers.page.custom.field.name.label": "Nome de exibição", "settings.providers.page.custom.field.name.placeholder": "Meu provedor", "settings.providers.page.custom.field.name.info": "Mostrado nos seletores de provedor e modelo.", + "settings.providers.page.custom.field.protocol.label": "Protocolo de API", + "settings.providers.page.custom.field.protocol.info": "Escolha o formato de solicitação implementado por esta API.", + "settings.providers.page.custom.field.protocol.openaiChat": "OpenAI Chat Completions", + "settings.providers.page.custom.field.protocol.openaiResponses": "OpenAI Responses", + "settings.providers.page.custom.field.protocol.anthropicMessages": "Anthropic Messages", "settings.providers.page.custom.field.baseURL.label": "URL base", "settings.providers.page.custom.field.baseURL.placeholder": "https://api.example.com/v1", "settings.providers.page.custom.field.baseURL.info": "URL base da API compatível com OpenAI. Deve começar com http:// ou https://.", diff --git a/packages/ui/src/lib/i18n/messages/uk.settings.ts b/packages/ui/src/lib/i18n/messages/uk.settings.ts index efb4bfbe..c8637731 100644 --- a/packages/ui/src/lib/i18n/messages/uk.settings.ts +++ b/packages/ui/src/lib/i18n/messages/uk.settings.ts @@ -1340,13 +1340,18 @@ export const settingsDict = { "settings.providers.page.custom.title": "Власний провайдер", "settings.providers.page.custom.editTitle": "Редагувати власного провайдера", - "settings.providers.page.custom.description": "Додайте OpenAI-сумісного провайдера з базовою URL-адресою, обліковими даними та списком моделей. Зберігається в конфігурації OpenCode й працює в чаті як будь-який інший провайдер.", + "settings.providers.page.custom.description": "Додайте провайдера з базовою URL-адресою, обліковими даними, списком моделей і підтримуваним протоколом API. Зберігається в конфігурації OpenCode для використання в чаті.", "settings.providers.page.custom.field.providerID.label": "ID провайдера", "settings.providers.page.custom.field.providerID.placeholder": "mij-provider", "settings.providers.page.custom.field.providerID.info": "Малі літери, цифри, дефіси та підкреслення. Використовується як ID провайдера OpenCode.", "settings.providers.page.custom.field.name.label": "Відображувана назва", "settings.providers.page.custom.field.name.placeholder": "Мій провайдер", "settings.providers.page.custom.field.name.info": "Показується у виборі провайдера та моделі.", + "settings.providers.page.custom.field.protocol.label": "Протокол API", + "settings.providers.page.custom.field.protocol.info": "Виберіть формат запиту, який реалізує цей API.", + "settings.providers.page.custom.field.protocol.openaiChat": "OpenAI Chat Completions", + "settings.providers.page.custom.field.protocol.openaiResponses": "OpenAI Responses", + "settings.providers.page.custom.field.protocol.anthropicMessages": "Anthropic Messages", "settings.providers.page.custom.field.baseURL.label": "Базова URL-адреса", "settings.providers.page.custom.field.baseURL.placeholder": "https://api.example.com/v1", "settings.providers.page.custom.field.baseURL.info": "Базова URL-адреса OpenAI-сумісного API. Має починатися з http:// або https://.", diff --git a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts index ee4a87bc..b37a906e 100644 --- a/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-CN.settings.ts @@ -1340,13 +1340,18 @@ export const settingsDict = { 'settings.providers.page.custom.title': '自定义提供商', 'settings.providers.page.custom.editTitle': '编辑自定义提供商', - 'settings.providers.page.custom.description': '通过指定基础 URL、凭据和模型列表,添加兼容 OpenAI 的提供商。会写入 OpenCode 配置,可像其他提供商一样在聊天中使用。', + 'settings.providers.page.custom.description': '通过指定基础 URL、凭据、模型列表和支持的 API 协议添加提供商。会写入 OpenCode 配置以供聊天使用。', 'settings.providers.page.custom.field.providerID.label': '提供商 ID', 'settings.providers.page.custom.field.providerID.placeholder': 'my-provider', 'settings.providers.page.custom.field.providerID.info': '小写字母、数字、连字符和下划线。用作 OpenCode 提供商 ID。', 'settings.providers.page.custom.field.name.label': '显示名称', 'settings.providers.page.custom.field.name.placeholder': '我的提供商', 'settings.providers.page.custom.field.name.info': '显示在提供商和模型选择器中。', + 'settings.providers.page.custom.field.protocol.label': 'API 协议', + 'settings.providers.page.custom.field.protocol.info': '选择此 API 实现的请求格式。', + 'settings.providers.page.custom.field.protocol.openaiChat': 'OpenAI Chat Completions', + 'settings.providers.page.custom.field.protocol.openaiResponses': 'OpenAI Responses', + 'settings.providers.page.custom.field.protocol.anthropicMessages': 'Anthropic Messages', 'settings.providers.page.custom.field.baseURL.label': '基础 URL', 'settings.providers.page.custom.field.baseURL.placeholder': 'https://api.example.com/v1', 'settings.providers.page.custom.field.baseURL.info': '兼容 OpenAI 的 API 基础 URL。必须以 http:// 或 https:// 开头。', diff --git a/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts b/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts index 8a7241ae..cc9db44c 100644 --- a/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts +++ b/packages/ui/src/lib/i18n/messages/zh-TW.settings.ts @@ -1247,13 +1247,18 @@ export const settingsDict = { 'settings.providers.page.custom.title': '自訂供應商', 'settings.providers.page.custom.editTitle': '編輯自訂提供者', - 'settings.providers.page.custom.description': '透過指定基礎 URL、憑證與模型清單,新增相容 OpenAI 的供應商。會寫入 OpenCode 設定,可像其他供應商一樣在聊天中使用。', + 'settings.providers.page.custom.description': '透過指定基礎 URL、憑證、模型清單與支援的 API 通訊協定新增供應商。會寫入 OpenCode 設定以供聊天使用。', 'settings.providers.page.custom.field.providerID.label': '供應商 ID', 'settings.providers.page.custom.field.providerID.placeholder': 'my-provider', 'settings.providers.page.custom.field.providerID.info': '小寫字母、數字、連字號與底線。用作 OpenCode 供應商 ID。', 'settings.providers.page.custom.field.name.label': '顯示名稱', 'settings.providers.page.custom.field.name.placeholder': '我的供應商', 'settings.providers.page.custom.field.name.info': '顯示於供應商與模型選擇器。', + 'settings.providers.page.custom.field.protocol.label': 'API 通訊協定', + 'settings.providers.page.custom.field.protocol.info': '選擇此 API 實作的請求格式。', + 'settings.providers.page.custom.field.protocol.openaiChat': 'OpenAI Chat Completions', + 'settings.providers.page.custom.field.protocol.openaiResponses': 'OpenAI Responses', + 'settings.providers.page.custom.field.protocol.anthropicMessages': 'Anthropic Messages', 'settings.providers.page.custom.field.baseURL.label': '基礎 URL', 'settings.providers.page.custom.field.baseURL.placeholder': 'https://api.example.com/v1', 'settings.providers.page.custom.field.baseURL.info': '相容 OpenAI 的 API 基礎 URL。必須以 http:// 或 https:// 開頭。', diff --git a/packages/web/server/lib/opencode/providers.js b/packages/web/server/lib/opencode/providers.js index 050c942c..42600165 100644 --- a/packages/web/server/lib/opencode/providers.js +++ b/packages/web/server/lib/opencode/providers.js @@ -9,6 +9,11 @@ import { const PROVIDER_ID_PATTERN = /^[a-z0-9][a-z0-9-_]*$/; const BASE_URL_PATTERN = /^https?:\/\//; const OPENAI_COMPATIBLE_NPM = '@ai-sdk/openai-compatible'; +const CUSTOM_PROVIDER_NPM_PACKAGES = new Set([ + OPENAI_COMPATIBLE_NPM, + '@ai-sdk/openai', + '@ai-sdk/anthropic', +]); function getProviderSources(providerId, workingDirectory) { const layers = readConfigLayers(workingDirectory); @@ -42,7 +47,7 @@ function getProviderSources(providerId, workingDirectory) { } /** - * Validate a custom OpenAI-compatible provider config payload before persistence. + * Validate a custom provider config payload before persistence. * Returns { ok: true, value } or { ok: false, error }. * * Credentials: either config.env contains a variable name, or hasStoredAuth is true @@ -63,8 +68,8 @@ function validateCustomProviderConfig(providerId, config, options = {}) { } const npm = typeof config.npm === 'string' ? config.npm.trim() : OPENAI_COMPATIBLE_NPM; - if (npm !== OPENAI_COMPATIBLE_NPM) { - return { ok: false, error: `Custom providers must use npm package ${OPENAI_COMPATIBLE_NPM}` }; + if (!CUSTOM_PROVIDER_NPM_PACKAGES.has(npm)) { + return { ok: false, error: 'Custom providers must use @ai-sdk/openai-compatible, @ai-sdk/openai, or @ai-sdk/anthropic' }; } const optionsBlock = isPlainObject(config.options) ? config.options : null; @@ -102,7 +107,7 @@ function validateCustomProviderConfig(providerId, config, options = {}) { } const normalized = { - npm: OPENAI_COMPATIBLE_NPM, + npm, name, options: { baseURL, diff --git a/packages/web/server/lib/opencode/providers.test.js b/packages/web/server/lib/opencode/providers.test.js index b023bf87..9ce3715a 100644 --- a/packages/web/server/lib/opencode/providers.test.js +++ b/packages/web/server/lib/opencode/providers.test.js @@ -71,6 +71,33 @@ describe('custom provider config persistence', () => { }).ok).toBe(true); }); + test('accepts the OpenCode Responses and Anthropic adapter packages', () => { + for (const npm of ['@ai-sdk/openai', '@ai-sdk/anthropic']) { + const result = validateCustomProviderConfig('ok', { + name: 'X', + npm, + env: ['MY_KEY'], + options: { baseURL: 'https://api.example.com/v1' }, + models: { m: { name: 'M' } }, + }); + expect(result.ok).toBe(true); + expect(result.value.config.npm).toBe(npm); + } + }); + + test('rejects unsupported adapter packages', () => { + const result = validateCustomProviderConfig('ok', { + name: 'X', + npm: '@example/unsupported', + env: ['MY_KEY'], + options: { baseURL: 'https://api.example.com/v1' }, + models: { m: { name: 'M' } }, + }); + + expect(result.ok).toBe(false); + expect(result.error).toContain('@ai-sdk/openai'); + }); + test('upsertProviderConfig writes and round-trips project config', () => { const result = upsertProviderConfig('campus-llm', { name: 'Campus LLM',