feat(providers): support custom API protocols

This commit is contained in:
Bohdan Triapitsyn
2026-08-20 18:39:13 +03:00
parent d80a011cbd
commit 647a949369
16 changed files with 180 additions and 20 deletions
@@ -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,
@@ -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',